Răsfoiți Sursa

DAO::vote: verifiably encrypt the Scalar blinds used for the pedersen commits. We select values v ∈ 𝔽ᵥ such that v ∈ 𝔽ₚ also, and then use conversion functions to move between the fields before and after encrypting/decrypting.

zero 2 ani în urmă
părinte
comite
bb3729db92

+ 18 - 7
src/contract/dao/proof/dao-vote-main.zk

@@ -5,6 +5,7 @@ constant "DaoVoteMain" {
     EcFixedPoint VALUE_COMMIT_RANDOM,
     EcFixedPointShort VALUE_COMMIT_VALUE,
     EcFixedPointBase NULLIFIER_K,
+    EcFixedPointBase VALUE_COMMIT_RANDOM_BASE,
 }
 
 witness "DaoVoteMain" {
@@ -26,11 +27,11 @@ witness "DaoVoteMain" {
 
     # Is the vote yes or no
     Base vote_option,
-    Scalar yes_vote_blind,
+    Base yes_vote_blind,
 
     # Total amount of capital allocated to vote
     Base all_vote_value,
-    Scalar all_vote_blind,
+    Base all_vote_blind,
 
     # Check the inputs and this proof are for the same token
     Base gov_token_blind,
@@ -77,14 +78,14 @@ circuit "DaoVoteMain" {
     # Pedersen commitment for vote option
     yes_vote_value = base_mul(vote_option, all_vote_value);
     yes_vote_value_c = ec_mul_short(yes_vote_value, VALUE_COMMIT_VALUE);
-    yes_vote_blind_c = ec_mul(yes_vote_blind, VALUE_COMMIT_RANDOM);
+    yes_vote_blind_c = ec_mul_base(yes_vote_blind, VALUE_COMMIT_RANDOM_BASE);
     yes_vote_commit = ec_add(yes_vote_value_c, yes_vote_blind_c);
     constrain_instance(ec_get_x(yes_vote_commit));
     constrain_instance(ec_get_y(yes_vote_commit));
 
     # Pedersen commitment for vote value
     all_vote_c = ec_mul_short(all_vote_value, VALUE_COMMIT_VALUE);
-    all_vote_blind_c = ec_mul(all_vote_blind, VALUE_COMMIT_RANDOM);
+    all_vote_blind_c = ec_mul_base(all_vote_blind, VALUE_COMMIT_RANDOM_BASE);
     all_vote_commit = ec_add(all_vote_c, all_vote_blind_c);
     constrain_instance(ec_get_x(all_vote_commit));
     constrain_instance(ec_get_y(all_vote_commit));
@@ -108,12 +109,22 @@ circuit "DaoVoteMain" {
     );
     const_1 = witness_base(1);
     const_2 = witness_base(2);
-    # Token ID
+    const_3 = witness_base(3);
+    const_4 = witness_base(4);
+    # Vote option
     shared_secret_1 = poseidon_hash(shared_secret, const_1);
     enc_vote_option = base_add(vote_option, shared_secret_1);
     constrain_instance(enc_vote_option);
-    # Serial
+    # Yes vote blind
     shared_secret_2 = poseidon_hash(shared_secret, const_2);
-    enc_all_vote_value = base_add(all_vote_value, shared_secret_2);
+    enc_yes_vote_blind = base_add(yes_vote_blind, shared_secret_2);
+    constrain_instance(enc_yes_vote_blind);
+    # All vote value
+    shared_secret_3 = poseidon_hash(shared_secret, const_3);
+    enc_all_vote_value = base_add(all_vote_value, shared_secret_3);
     constrain_instance(enc_all_vote_value);
+    # All vote blind
+    shared_secret_4 = poseidon_hash(shared_secret, const_4);
+    enc_all_vote_blind = base_add(all_vote_blind, shared_secret_4);
+    constrain_instance(enc_all_vote_blind);
 }

+ 1 - 1
src/contract/dao/src/client/mod.rs

@@ -32,7 +32,7 @@ pub use propose::{DaoProposeCall, DaoProposeStakeInput};
 /// * `DaoVoteCall` is what creates the call data used on chain.
 /// * `DaoVoteNote` is the secret shared info transmitted between DAO members.
 pub mod vote;
-pub use vote::{DaoVoteCall, DaoVoteInput, DaoVoteNote};
+pub use vote::{DaoVoteCall, DaoVoteInput};
 
 pub mod exec;
 pub use exec::DaoExecCall;

+ 49 - 47
src/contract/dao/src/client/vote.rs

@@ -21,14 +21,11 @@ use darkfi_sdk::{
     bridgetree,
     bridgetree::Hashable,
     crypto::{
-        note::{AeadEncryptedNote, ElGamalEncryptedNote},
-        pasta_prelude::*,
-        pedersen_commitment_u64, poseidon_hash, Keypair, MerkleNode, Nullifier, PublicKey,
-        SecretKey,
+        note::ElGamalEncryptedNote, pasta_prelude::*, pedersen_commitment_u64, poseidon_hash,
+        util::mod_p_r_unsafe, Keypair, MerkleNode, Nullifier, PublicKey, SecretKey,
     },
     pasta::pallas,
 };
-use darkfi_serial::{async_trait, SerialDecodable, SerialEncodable};
 use log::debug;
 use rand::rngs::OsRng;
 
@@ -40,15 +37,6 @@ use darkfi::{
 
 use crate::model::{Dao, DaoProposal, DaoVoteParams, DaoVoteParamsInput, VecAuthCallCommit};
 
-#[derive(SerialEncodable, SerialDecodable)]
-pub struct DaoVoteNote {
-    pub vote_option: bool,
-    pub yes_vote_blind: pallas::Scalar,
-    // yes_vote_value = vote_option * all_vote_value
-    pub all_vote_value: u64,
-    pub all_vote_blind: pallas::Scalar,
-}
-
 pub struct DaoVoteInput {
     pub secret: SecretKey,
     pub note: darkfi_money_contract::client::MoneyNote,
@@ -57,11 +45,10 @@ pub struct DaoVoteInput {
     pub signature_secret: SecretKey,
 }
 
-// Inside ZKproof, check proposal is correct.
+// Inside ZK proof, check proposal is correct.
 pub struct DaoVoteCall {
     pub inputs: Vec<DaoVoteInput>,
     pub vote_option: bool,
-    pub yes_vote_blind: pallas::Scalar,
     pub proposal: DaoProposal,
     pub dao: Dao,
     pub dao_keypair: Keypair,
@@ -85,8 +72,29 @@ impl DaoVoteCall {
         let mut all_vote_value = 0;
         let mut all_vote_blind = pallas::Scalar::from(0);
 
-        for input in self.inputs {
-            let value_blind = pallas::Scalar::random(&mut OsRng);
+        let last_input_idx = self.inputs.len() - 1;
+        for (i, input) in self.inputs.into_iter().enumerate() {
+            // Last input
+            // Choose a blinding factor that can be converted to pallas::Base exactly.
+            // We need this so we can verifiably encrypt the sum of input blinds
+            // in the next section.
+            // TODO: make a generalized widget for this, and also picking blinds in money::transfer()
+            let mut value_blind = pallas::Scalar::random(&mut OsRng);
+
+            if i == last_input_idx {
+                // It's near zero chance it ever loops at all.
+                // P(random 𝔽ᵥ ∉ 𝔽ₚ) = (q - p)/q = 2.99 × 10⁻⁵¹
+                loop {
+                    let av_blind = mod_p_r_unsafe(all_vote_blind + value_blind);
+
+                    if av_blind.is_none().into() {
+                        value_blind = pallas::Scalar::random(&mut OsRng);
+                        continue
+                    }
+
+                    break
+                }
+            }
 
             all_vote_value += input.note.value;
             all_vote_blind += value_blind;
@@ -104,7 +112,7 @@ impl DaoVoteCall {
                 Witness::Base(Value::known(pallas::Base::ZERO)),
                 Witness::Base(Value::known(pallas::Base::from(note.value))),
                 Witness::Base(Value::known(note.token_id.inner())),
-                Witness::Scalar(Value::known(all_vote_blind)),
+                Witness::Scalar(Value::known(value_blind)),
                 Witness::Base(Value::known(gov_token_blind)),
                 Witness::Uint32(Value::known(leaf_pos.try_into().unwrap())),
                 Witness::MerklePath(Value::known(input.merkle_path.clone().try_into().unwrap())),
@@ -141,7 +149,7 @@ impl DaoVoteCall {
 
             let nullifier = poseidon_hash([input.secret.inner(), coin.inner()]);
 
-            let vote_commit = pedersen_commitment_u64(note.value, all_vote_blind);
+            let vote_commit = pedersen_commitment_u64(note.value, value_blind);
             let vote_commit_coords = vote_commit.to_affine().coordinates().unwrap();
 
             let (sig_x, sig_y) = signature_public.xy();
@@ -182,13 +190,25 @@ impl DaoVoteCall {
         let vote_option = self.vote_option as u64;
         assert!(vote_option == 0 || vote_option == 1);
 
-        let yes_vote_commit =
-            pedersen_commitment_u64(vote_option * all_vote_value, self.yes_vote_blind);
+        // Create a random blind b ∈ 𝔽ᵥ, such that b ∈ 𝔽ₚ
+        let yes_vote_blind = loop {
+            let blind = pallas::Scalar::random(&mut OsRng);
+            if mod_p_r_unsafe(blind).is_some().into() {
+                break blind
+            }
+        };
+        let yes_vote_commit = pedersen_commitment_u64(vote_option * all_vote_value, yes_vote_blind);
         let yes_vote_commit_coords = yes_vote_commit.to_affine().coordinates().unwrap();
 
         let all_vote_commit = pedersen_commitment_u64(all_vote_value, all_vote_blind);
+        assert_eq!(all_vote_commit, inputs.iter().map(|i| i.vote_commit).sum());
         let all_vote_commit_coords = all_vote_commit.to_affine().coordinates().unwrap();
 
+        // Convert blinds to 𝔽ₚ, which should work fine since we selected them
+        // to be convertable.
+        let yes_vote_blind = mod_p_r_unsafe(yes_vote_blind).unwrap();
+        let all_vote_blind = mod_p_r_unsafe(all_vote_blind).unwrap();
+
         let vote_option = pallas::Base::from(vote_option);
         let all_vote_value_fp = pallas::Base::from(all_vote_value);
         let ephem_secret = SecretKey::random(&mut OsRng);
@@ -213,10 +233,10 @@ impl DaoVoteCall {
             Witness::Base(Value::known(self.dao.bulla_blind)),
             // Vote
             Witness::Base(Value::known(vote_option)),
-            Witness::Scalar(Value::known(self.yes_vote_blind)),
+            Witness::Base(Value::known(yes_vote_blind)),
             // Total number of gov tokens allocated
             Witness::Base(Value::known(all_vote_value_fp)),
-            Witness::Scalar(Value::known(all_vote_blind)),
+            Witness::Base(Value::known(all_vote_blind)),
             // gov token
             Witness::Base(Value::known(gov_token_blind)),
             // time checks
@@ -228,12 +248,7 @@ impl DaoVoteCall {
         assert_eq!(self.dao.to_bulla(), self.proposal.dao_bulla);
         let proposal_bulla = self.proposal.to_bulla();
 
-        let note = [
-            vote_option,
-            //self.yes_vote_blind,
-            all_vote_value_fp,
-            //all_vote_blind,
-        ];
+        let note = [vote_option, yes_vote_blind, all_vote_value_fp, all_vote_blind];
         let enc_note = ElGamalEncryptedNote::encrypt(note, &ephem_secret, &self.dao_keypair.public);
 
         let public_inputs = vec![
@@ -248,6 +263,8 @@ impl DaoVoteCall {
             ephem_y,
             enc_note.encrypted_values[0],
             enc_note.encrypted_values[1],
+            enc_note.encrypted_values[2],
+            enc_note.encrypted_values[3],
         ];
 
         let circuit = ZkCircuit::new(prover_witnesses, main_zkbin);
@@ -257,23 +274,8 @@ impl DaoVoteCall {
             .expect("DAO::vote() proving error!");
         proofs.push(main_proof);
 
-        let note = DaoVoteNote {
-            vote_option: self.vote_option,
-            yes_vote_blind: self.yes_vote_blind,
-            all_vote_value,
-            all_vote_blind,
-        };
-        let enc_note_old =
-            AeadEncryptedNote::encrypt(&note, &self.dao_keypair.public, &mut OsRng).unwrap();
-
-        let params = DaoVoteParams {
-            token_commit,
-            proposal_bulla,
-            yes_vote_commit,
-            note: enc_note,
-            note_old: enc_note_old,
-            inputs,
-        };
+        let params =
+            DaoVoteParams { token_commit, proposal_bulla, yes_vote_commit, note: enc_note, inputs };
 
         Ok((params, proofs))
     }

+ 2 - 0
src/contract/dao/src/entrypoint/vote.rs

@@ -101,6 +101,8 @@ pub(crate) fn dao_vote_get_metadata(
             ephem_y,
             params.note.encrypted_values[0],
             params.note.encrypted_values[1],
+            params.note.encrypted_values[2],
+            params.note.encrypted_values[3],
         ],
     ));
 

+ 1 - 2
src/contract/dao/src/model.rs

@@ -280,8 +280,7 @@ pub struct DaoVoteParams {
     /// Commitment for yes votes
     pub yes_vote_commit: pallas::Point,
     /// Encrypted note
-    pub note: ElGamalEncryptedNote<2>,
-    pub note_old: AeadEncryptedNote,
+    pub note: ElGamalEncryptedNote<4>,
     /// Inputs for the vote
     pub inputs: Vec<DaoVoteParamsInput>,
 }

+ 38 - 21
src/contract/dao/tests/integration.rs

@@ -18,13 +18,15 @@
 
 use darkfi::Result;
 use darkfi_contract_test_harness::{init_logger, Holder, TestHarness};
-use darkfi_dao_contract::{
-    client::DaoVoteNote,
-    model::{Dao, DaoBlindAggregateVote},
-};
+use darkfi_dao_contract::model::{Dao, DaoBlindAggregateVote};
 use darkfi_money_contract::model::CoinAttributes;
 use darkfi_sdk::{
-    crypto::{pasta_prelude::Field, pedersen_commitment_u64, DAO_CONTRACT_ID, DARK_TOKEN_ID},
+    crypto::{
+        pasta_prelude::*,
+        pedersen_commitment_u64,
+        util::{fp_to_u64, mod_r_p},
+        DAO_CONTRACT_ID, DARK_TOKEN_ID,
+    },
     pasta::pallas,
 };
 use log::info;
@@ -255,12 +257,9 @@ fn integration_test() -> Result<()> {
         }
 
         // Gather and decrypt all vote notes
-        let vote_note_1: DaoVoteNote =
-            alice_vote_params.note_old.decrypt(&dao_keypair.secret).unwrap();
-        let vote_note_2: DaoVoteNote =
-            bob_vote_params.note_old.decrypt(&dao_keypair.secret).unwrap();
-        let vote_note_3: DaoVoteNote =
-            charlie_vote_params.note_old.decrypt(&dao_keypair.secret).unwrap();
+        let vote_note_1 = alice_vote_params.note.decrypt(&dao_keypair.secret);
+        let vote_note_2 = bob_vote_params.note.decrypt(&dao_keypair.secret);
+        let vote_note_3 = charlie_vote_params.note.decrypt(&dao_keypair.secret);
 
         // Count the votes
         let mut total_yes_vote_value = 0;
@@ -269,29 +268,47 @@ fn integration_test() -> Result<()> {
         let mut total_yes_vote_blind = pallas::Scalar::ZERO;
         let mut total_all_vote_blind = pallas::Scalar::ZERO;
 
-        for (i, note) in [vote_note_1, vote_note_2, vote_note_3].iter().enumerate() {
-            total_yes_vote_blind += note.yes_vote_blind;
-            total_all_vote_blind += note.all_vote_blind;
+        for (i, (note, params)) in [
+            (vote_note_1, alice_vote_params),
+            (vote_note_2, bob_vote_params),
+            (vote_note_3, charlie_vote_params),
+        ]
+        .iter()
+        .enumerate()
+        {
+            // Note format: [
+            //   vote_option,
+            //   yes_vote_blind,
+            //   all_vote_value_fp,
+            //   all_vote_blind,
+            // ]
+            let vote_option = fp_to_u64(note[0]).unwrap();
+            let yes_vote_blind = mod_r_p(note[1]);
+            let all_vote_value = fp_to_u64(note[2]).unwrap();
+            let all_vote_blind = mod_r_p(note[3]);
+            assert!(vote_option == 0 || vote_option == 1);
+
+            total_yes_vote_blind += yes_vote_blind;
+            total_all_vote_blind += all_vote_blind;
 
             // Update private values
             // vote_option is either 0 or 1
-            let yes_vote_value = note.vote_option as u64 * note.all_vote_value;
+            let yes_vote_value = vote_option * all_vote_value;
             total_yes_vote_value += yes_vote_value;
-            total_all_vote_value += note.all_vote_value;
+            total_all_vote_value += all_vote_value;
 
             // Update public values
-            let yes_vote_commit = pedersen_commitment_u64(yes_vote_value, note.yes_vote_blind);
-            let all_vote_commit = pedersen_commitment_u64(note.all_vote_value, note.all_vote_blind);
+            let yes_vote_commit = params.yes_vote_commit;
+            let all_vote_commit = params.inputs.iter().map(|i| i.vote_commit).sum();
             let blind_vote = DaoBlindAggregateVote { yes_vote_commit, all_vote_commit };
             blind_total_vote.aggregate(blind_vote);
 
             // Just for the debug
-            let vote_result = match note.vote_option {
+            let vote_result = match vote_option != 0 {
                 true => "yes",
                 false => "no",
             };
-
-            info!("Voter {} voted {} with {} tokens", i, vote_result, note.all_vote_value);
+            info!("Voter {} voted {} with {} tokens", i, vote_result, all_vote_value);
         }
 
         info!("Outcome = {} / {}", total_yes_vote_value, total_all_vote_value);

+ 1 - 3
src/contract/test-harness/src/dao_vote.rs

@@ -30,8 +30,7 @@ use darkfi_dao_contract::{
 };
 use darkfi_money_contract::client::OwnCoin;
 use darkfi_sdk::{
-    crypto::{pasta_prelude::Field, Keypair, SecretKey, DAO_CONTRACT_ID},
-    pasta::pallas,
+    crypto::{Keypair, SecretKey, DAO_CONTRACT_ID},
     ContractCall,
 };
 use darkfi_serial::{serialize, Encodable};
@@ -83,7 +82,6 @@ impl TestHarness {
         let call = DaoVoteCall {
             inputs: vec![input],
             vote_option,
-            yes_vote_blind: pallas::Scalar::random(&mut OsRng),
             proposal: proposal.clone(),
             dao: dao.clone(),
             dao_keypair: dao_keypair.clone(),

+ 2 - 2
src/contract/test-harness/src/vks.rs

@@ -50,8 +50,8 @@ use darkfi_serial::{deserialize, serialize};
 use log::debug;
 
 /// Update this if any circuits are changed
-const VKS_HASH: &str = "191e742f12fe776f76f9cda4e72ac3a4f92cd3873f2ea04e3887ac54d7ca5340";
-const PKS_HASH: &str = "da22afe484b762c6308241c91fe4d399f11b15f62e493c5fef4dea53f72976c1";
+const VKS_HASH: &str = "15c86acb8a96c21d9233e432975c1579c5b3b39c0b7568c4f3ae167783eeb548";
+const PKS_HASH: &str = "3bb43dbb2a5fbb54d406e637681c70bc748f158f4d725e71c1db38e85106966e";
 
 fn pks_path(typ: &str) -> Result<PathBuf> {
     let output = Command::new("git").arg("rev-parse").arg("--show-toplevel").output()?.stdout;

+ 1 - 1
src/sdk/src/crypto/constants/fixed_bases.rs

@@ -111,8 +111,8 @@ pub struct ValueCommitV;
 
 /// ConstBaseFieldElement is used in scalar mul with a base field element.
 #[derive(Clone, Debug, Eq, PartialEq)]
+#[allow(non_snake_case)] // Rust bug: see https://github.com/rust-lang/rust/issues/60681
 pub struct ConstBaseFieldElement {
-    #[allow(non_snake_case)]
     G: pallas::Affine,
     u: Vec<[[u8; 32]; H]>,
     z: Vec<u64>,

+ 28 - 0
src/sdk/src/crypto/util.rs

@@ -16,11 +16,14 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use darkfi_serial::ReadExt;
 use halo2_gadgets::poseidon::primitives as poseidon;
 use pasta_curves::{
     group::ff::{FromUniformBytes, PrimeField},
     pallas,
 };
+use std::io::Cursor;
+use subtle::CtOption;
 
 /// Hash `a` and `b` together with a prefix `persona` and return a `pallas::Scalar`
 /// element from the digest.
@@ -40,6 +43,14 @@ pub fn mod_r_p(x: pallas::Base) -> pallas::Scalar {
     pallas::Scalar::from_repr(x.to_repr()).unwrap()
 }
 
+/// Converts from pallas::Scalar to pallas::Base (aka $x \pmod{r_\mathbb{P}}$).
+///
+/// This call is unsafe and liable to fail. Use with caution.
+/// The Pallas scalar field is bigger than the field we're converting to here.
+pub fn mod_p_r_unsafe(x: pallas::Scalar) -> CtOption<pallas::Base> {
+    pallas::Base::from_repr(x.to_repr())
+}
+
 /// Wrapper around poseidon in `halo2_gadgets`
 pub fn poseidon_hash<const N: usize>(messages: [pallas::Base; N]) -> pallas::Base {
     // TODO: it's possible to make this function simply take a slice, by using the lower level
@@ -50,3 +61,20 @@ pub fn poseidon_hash<const N: usize>(messages: [pallas::Base; N]) -> pallas::Bas
     poseidon::Hash::<_, poseidon::P128Pow5T3, poseidon::ConstantLength<N>, 3, 2>::init()
         .hash(messages)
 }
+
+pub fn fp_to_u64(v: pallas::Base) -> Option<u64> {
+    let repr = v.to_repr();
+    if !repr[8..].iter().all(|&b| b == 0u8) {
+        return None
+    }
+    let mut cur = Cursor::new(&repr[0..8]);
+    let val = ReadExt::read_u64(&mut cur).ok()?;
+    Some(val)
+}
+
+#[test]
+fn test_fp_to_u64() {
+    let fp = pallas::Base::from(u64::MAX);
+    assert_eq!(fp_to_u64(fp), Some(u64::MAX));
+    assert_eq!(fp_to_u64(fp + pallas::Base::ONE), None);
+}