darkfi_dao_contract/client/
vote.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use darkfi_money_contract::model::CoinAttributes;
20use darkfi_sdk::{
21    bridgetree,
22    bridgetree::Hashable,
23    crypto::{
24        note::ElGamalEncryptedNote,
25        pasta_prelude::*,
26        pedersen_commitment_u64, poseidon_hash,
27        smt::{PoseidonFp, SparseMerkleTree, StorageAdapter, SMT_FP_DEPTH},
28        util::fv_mod_fp_unsafe,
29        Blind, MerkleNode, PublicKey, SecretKey,
30    },
31    pasta::pallas,
32};
33use rand::rngs::OsRng;
34use tracing::debug;
35
36use darkfi::{
37    zk::{halo2::Value, Proof, ProvingKey, Witness, ZkCircuit},
38    zkas::ZkBinary,
39    ClientFailed, Result,
40};
41
42use crate::{
43    error::DaoError,
44    model::{Dao, DaoProposal, DaoVoteParams, DaoVoteParamsInput, VecAuthCallCommit},
45};
46
47pub struct DaoVoteInput {
48    pub secret: SecretKey,
49    pub note: darkfi_money_contract::client::MoneyNote,
50    pub leaf_position: bridgetree::Position,
51    pub merkle_path: Vec<MerkleNode>,
52}
53
54// Inside ZK proof, check proposal is correct.
55pub struct DaoVoteCall<'a, T: StorageAdapter<Value = pallas::Base>> {
56    pub money_null_smt:
57        &'a SparseMerkleTree<'a, SMT_FP_DEPTH, { SMT_FP_DEPTH + 1 }, pallas::Base, PoseidonFp, T>,
58    pub inputs: Vec<DaoVoteInput>,
59    pub vote_option: bool,
60    pub proposal: DaoProposal,
61    pub dao: Dao,
62    pub current_blockwindow: u64,
63}
64
65impl<T: StorageAdapter<Value = pallas::Base>> DaoVoteCall<'_, T> {
66    pub fn make(
67        self,
68        burn_zkbin: &ZkBinary,
69        burn_pk: &ProvingKey,
70        main_zkbin: &ZkBinary,
71        main_pk: &ProvingKey,
72    ) -> Result<(DaoVoteParams, Vec<Proof>, Vec<SecretKey>)> {
73        debug!(target: "contract::dao::client::vote", "make()");
74
75        if self.dao.to_bulla() != self.proposal.dao_bulla {
76            return Err(ClientFailed::VerifyError(DaoError::InvalidCalls.to_string()).into())
77        }
78        let proposal_bulla = self.proposal.to_bulla();
79
80        let mut proofs = vec![];
81        let mut signature_secrets = vec![];
82
83        let gov_token_blind = pallas::Base::random(&mut OsRng);
84
85        let mut inputs = vec![];
86        let mut all_vote_value = 0;
87        let mut all_vote_blind = pallas::Scalar::from(0);
88
89        let last_input_idx = self.inputs.len() - 1;
90        for (i, input) in self.inputs.into_iter().enumerate() {
91            // Last input
92            // Choose a blinding factor that can be converted to pallas::Base exactly.
93            // We need this so we can verifiably encrypt the sum of input blinds
94            // in the next section.
95            // TODO: make a generalized widget for this, and also picking blinds in money::transfer()
96            let mut value_blind = pallas::Scalar::random(&mut OsRng);
97
98            if i == last_input_idx {
99                // It's near zero chance it ever loops at all.
100                // P(random ๐”ฝแตฅ โˆ‰ ๐”ฝโ‚š) = (q - p)/q = 2.99 ร— 10โปโตยน
101                loop {
102                    let av_blind = fv_mod_fp_unsafe(all_vote_blind + value_blind);
103
104                    if av_blind.is_none().into() {
105                        value_blind = pallas::Scalar::random(&mut OsRng);
106                        continue
107                    }
108
109                    break
110                }
111            }
112
113            all_vote_value += input.note.value;
114            all_vote_blind += value_blind;
115
116            let signature_secret = SecretKey::random(&mut OsRng);
117            let signature_public = PublicKey::from_secret(signature_secret);
118
119            // Note from the previous output
120            let note = input.note;
121            let leaf_pos: u64 = input.leaf_position.into();
122
123            let public_key = PublicKey::from_secret(input.secret);
124            let coin = CoinAttributes {
125                public_key,
126                value: note.value,
127                token_id: note.token_id,
128                spend_hook: note.spend_hook,
129                user_data: note.user_data,
130                blind: note.coin_blind,
131            }
132            .to_coin();
133            let nullifier = poseidon_hash([input.secret.inner(), coin.inner()]);
134
135            let smt_null_root = self.money_null_smt.root();
136            let smt_null_path = self.money_null_smt.prove_membership(&nullifier);
137            if !smt_null_path.verify(&smt_null_root, &pallas::Base::ZERO, &nullifier) {
138                return Err(
139                    ClientFailed::VerifyError(DaoError::InvalidInputMerkleRoot.to_string()).into()
140                )
141            }
142
143            let prover_witnesses = vec![
144                Witness::Base(Value::known(input.secret.inner())),
145                Witness::Base(Value::known(pallas::Base::from(note.value))),
146                Witness::Base(Value::known(note.token_id.inner())),
147                Witness::Base(Value::known(pallas::Base::ZERO)),
148                Witness::Base(Value::known(pallas::Base::ZERO)),
149                Witness::Base(Value::known(note.coin_blind.inner())),
150                Witness::Base(Value::known(proposal_bulla.inner())),
151                Witness::Scalar(Value::known(value_blind)),
152                Witness::Base(Value::known(gov_token_blind)),
153                Witness::Uint32(Value::known(leaf_pos.try_into().unwrap())),
154                Witness::MerklePath(Value::known(input.merkle_path.clone().try_into().unwrap())),
155                Witness::SparseMerklePath(Value::known(smt_null_path.path)),
156                Witness::Base(Value::known(signature_secret.inner())),
157            ];
158
159            let merkle_root = {
160                let position: u64 = input.leaf_position.into();
161                let mut current = MerkleNode::from(coin.inner());
162                for (level, sibling) in input.merkle_path.iter().enumerate() {
163                    let level = level as u8;
164                    current = if position & (1 << level) == 0 {
165                        MerkleNode::combine(level.into(), &current, sibling)
166                    } else {
167                        MerkleNode::combine(level.into(), sibling, &current)
168                    };
169                }
170                current
171            };
172
173            let token_commit = poseidon_hash([note.token_id.inner(), gov_token_blind]);
174            if note.token_id != self.dao.gov_token_id {
175                return Err(ClientFailed::InvalidTokenId(note.token_id.to_string()).into())
176            }
177
178            let vote_commit = pedersen_commitment_u64(note.value, Blind(value_blind));
179            let vote_commit_coords = vote_commit.to_affine().coordinates().unwrap();
180
181            let (sig_x, sig_y) = signature_public.xy();
182
183            let vote_nullifier =
184                poseidon_hash([nullifier, input.secret.inner(), proposal_bulla.inner()]);
185
186            let public_inputs = vec![
187                smt_null_root,
188                proposal_bulla.inner(),
189                vote_nullifier,
190                *vote_commit_coords.x(),
191                *vote_commit_coords.y(),
192                token_commit,
193                merkle_root.inner(),
194                sig_x,
195                sig_y,
196            ];
197
198            //darkfi::zk::export_witness_json("proof/witness/vote-input.json", &prover_witnesses, &public_inputs);
199            let circuit = ZkCircuit::new(prover_witnesses, burn_zkbin);
200            debug!(target: "contract::dao::client::vote", "input_proof Proof::create()");
201            let input_proof = Proof::create(burn_pk, &[circuit], &public_inputs, &mut OsRng)?;
202            proofs.push(input_proof);
203            signature_secrets.push(signature_secret);
204
205            let input = DaoVoteParamsInput {
206                vote_commit,
207                vote_nullifier: vote_nullifier.into(),
208                signature_public,
209            };
210            inputs.push(input);
211        }
212
213        let token_commit = poseidon_hash([self.dao.gov_token_id.inner(), gov_token_blind]);
214
215        let dao_proposer_limit = pallas::Base::from(self.dao.proposer_limit);
216        let dao_quorum = pallas::Base::from(self.dao.quorum);
217        let dao_early_exec_quorum = pallas::Base::from(self.dao.early_exec_quorum);
218        let dao_approval_ratio_quot = pallas::Base::from(self.dao.approval_ratio_quot);
219        let dao_approval_ratio_base = pallas::Base::from(self.dao.approval_ratio_base);
220        let (dao_notes_pub_x, dao_notes_pub_y) = self.dao.notes_public_key.xy();
221        let (dao_proposer_pub_x, dao_proposer_pub_y) = self.dao.proposer_public_key.xy();
222        let (dao_proposals_pub_x, dao_proposals_pub_y) = self.dao.proposals_public_key.xy();
223        let dao_votes_public_key = self.dao.votes_public_key.inner();
224        let (dao_exec_pub_x, dao_exec_pub_y) = self.dao.exec_public_key.xy();
225        let (dao_early_exec_pub_x, dao_early_exec_pub_y) = self.dao.early_exec_public_key.xy();
226
227        let vote_option = self.vote_option as u64;
228        if vote_option != 0 && vote_option != 1 {
229            return Err(ClientFailed::VerifyError(DaoError::VoteInputsEmpty.to_string()).into())
230        }
231
232        // Create a random blind b โˆˆ ๐”ฝแตฅ, such that b โˆˆ ๐”ฝโ‚š
233        let yes_vote_blind = loop {
234            let blind = pallas::Scalar::random(&mut OsRng);
235            if fv_mod_fp_unsafe(blind).is_some().into() {
236                break blind
237            }
238        };
239        let yes_vote_commit =
240            pedersen_commitment_u64(vote_option * all_vote_value, Blind(yes_vote_blind));
241        let yes_vote_commit_coords = yes_vote_commit.to_affine().coordinates().unwrap();
242
243        let all_vote_commit = pedersen_commitment_u64(all_vote_value, Blind(all_vote_blind));
244        if all_vote_commit != inputs.iter().map(|i| i.vote_commit).sum() {
245            return Err(ClientFailed::VerifyError(DaoError::VoteCommitMismatch.to_string()).into())
246        }
247        let all_vote_commit_coords = all_vote_commit.to_affine().coordinates().unwrap();
248
249        // Convert blinds to ๐”ฝโ‚š, which should work fine since we selected them
250        // to be convertable.
251        let yes_vote_blind = Blind(fv_mod_fp_unsafe(yes_vote_blind).unwrap());
252        let all_vote_blind = Blind(fv_mod_fp_unsafe(all_vote_blind).unwrap());
253
254        let vote_option = pallas::Base::from(vote_option);
255        let all_vote_value_fp = pallas::Base::from(all_vote_value);
256        let ephem_secret = SecretKey::random(&mut OsRng);
257        let ephem_pubkey = PublicKey::from_secret(ephem_secret);
258        let (ephem_x, ephem_y) = ephem_pubkey.xy();
259
260        let current_blockwindow = pallas::Base::from(self.current_blockwindow);
261
262        let prover_witnesses = vec![
263            // Proposal params
264            Witness::Base(Value::known(self.proposal.auth_calls.commit())),
265            Witness::Base(Value::known(pallas::Base::from(self.proposal.creation_blockwindow))),
266            Witness::Base(Value::known(pallas::Base::from(self.proposal.duration_blockwindows))),
267            Witness::Base(Value::known(self.proposal.user_data)),
268            Witness::Base(Value::known(self.proposal.blind.inner())),
269            // DAO params
270            Witness::Base(Value::known(dao_proposer_limit)),
271            Witness::Base(Value::known(dao_quorum)),
272            Witness::Base(Value::known(dao_early_exec_quorum)),
273            Witness::Base(Value::known(dao_approval_ratio_quot)),
274            Witness::Base(Value::known(dao_approval_ratio_base)),
275            Witness::Base(Value::known(self.dao.gov_token_id.inner())),
276            Witness::Base(Value::known(dao_notes_pub_x)),
277            Witness::Base(Value::known(dao_notes_pub_y)),
278            Witness::Base(Value::known(dao_proposer_pub_x)),
279            Witness::Base(Value::known(dao_proposer_pub_y)),
280            Witness::Base(Value::known(dao_proposals_pub_x)),
281            Witness::Base(Value::known(dao_proposals_pub_y)),
282            Witness::EcNiPoint(Value::known(dao_votes_public_key)),
283            Witness::Base(Value::known(dao_exec_pub_x)),
284            Witness::Base(Value::known(dao_exec_pub_y)),
285            Witness::Base(Value::known(dao_early_exec_pub_x)),
286            Witness::Base(Value::known(dao_early_exec_pub_y)),
287            Witness::Base(Value::known(self.dao.bulla_blind.inner())),
288            // Vote
289            Witness::Base(Value::known(vote_option)),
290            Witness::Base(Value::known(yes_vote_blind.inner())),
291            // Total number of gov tokens allocated
292            Witness::Base(Value::known(all_vote_value_fp)),
293            Witness::Base(Value::known(all_vote_blind.inner())),
294            // Gov token
295            Witness::Base(Value::known(gov_token_blind)),
296            // Time checks
297            Witness::Base(Value::known(current_blockwindow)),
298            // verifiable encryption
299            Witness::Base(Value::known(ephem_secret.inner())),
300        ];
301
302        let note = [vote_option, yes_vote_blind.inner(), all_vote_value_fp, all_vote_blind.inner()];
303        let enc_note =
304            ElGamalEncryptedNote::encrypt_unsafe(note, &ephem_secret, &self.dao.votes_public_key)?;
305
306        let public_inputs = vec![
307            token_commit,
308            proposal_bulla.inner(),
309            *yes_vote_commit_coords.x(),
310            *yes_vote_commit_coords.y(),
311            *all_vote_commit_coords.x(),
312            *all_vote_commit_coords.y(),
313            current_blockwindow,
314            ephem_x,
315            ephem_y,
316            enc_note.encrypted_values[0],
317            enc_note.encrypted_values[1],
318            enc_note.encrypted_values[2],
319            enc_note.encrypted_values[3],
320        ];
321
322        //darkfi::zk::export_witness_json("proof/witness/vote-main.json", &prover_witnesses, &public_inputs);
323        let circuit = ZkCircuit::new(prover_witnesses, main_zkbin);
324
325        debug!(target: "contract::dao::client::vote", "main_proof = Proof::create()");
326        let main_proof = Proof::create(main_pk, &[circuit], &public_inputs, &mut OsRng)?;
327        proofs.push(main_proof);
328
329        let params =
330            DaoVoteParams { token_commit, proposal_bulla, yes_vote_commit, note: enc_note, inputs };
331
332        Ok((params, proofs, signature_secrets))
333    }
334}