darkfi_dao_contract/client/
propose.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::AeadEncryptedNote,
25        pasta_prelude::*,
26        pedersen::pedersen_commitment_u64,
27        poseidon_hash,
28        smt::{PoseidonFp, SparseMerkleTree, StorageAdapter, SMT_FP_DEPTH},
29        Blind, FuncId, MerkleNode, PublicKey, ScalarBlind, SecretKey,
30    },
31    pasta::pallas,
32};
33use rand::rngs::OsRng;
34
35use darkfi::{
36    zk::{halo2::Value, Proof, ProvingKey, Witness, ZkCircuit},
37    zkas::ZkBinary,
38    ClientFailed, Result,
39};
40
41use crate::{
42    error::DaoError,
43    model::{Dao, DaoProposal, DaoProposeParams, DaoProposeParamsInput, VecAuthCallCommit},
44};
45
46pub struct DaoProposeStakeInput {
47    pub secret: SecretKey,
48    pub note: darkfi_money_contract::client::MoneyNote,
49    pub leaf_position: bridgetree::Position,
50    pub merkle_path: Vec<MerkleNode>,
51}
52
53pub struct DaoProposeCall<'a, T: StorageAdapter<Value = pallas::Base>> {
54    pub money_null_smt:
55        &'a SparseMerkleTree<'a, SMT_FP_DEPTH, { SMT_FP_DEPTH + 1 }, pallas::Base, PoseidonFp, T>,
56    pub inputs: Vec<DaoProposeStakeInput>,
57    pub proposal: DaoProposal,
58    pub dao: Dao,
59    pub dao_leaf_position: bridgetree::Position,
60    pub dao_merkle_path: Vec<MerkleNode>,
61    pub dao_merkle_root: MerkleNode,
62}
63
64impl<T: StorageAdapter<Value = pallas::Base>> DaoProposeCall<'_, T> {
65    pub fn make(
66        self,
67        dao_proposer_secret_key: &SecretKey,
68        burn_zkbin: &ZkBinary,
69        burn_pk: &ProvingKey,
70        main_zkbin: &ZkBinary,
71        main_pk: &ProvingKey,
72    ) -> Result<(DaoProposeParams, Vec<Proof>, Vec<SecretKey>)> {
73        let mut proofs = vec![];
74        let mut signature_secrets = vec![];
75
76        let gov_token_blind = Blind::random(&mut OsRng);
77        let smt_null_root = self.money_null_smt.root();
78
79        let mut inputs = vec![];
80        let mut total_funds = 0;
81        let mut total_funds_blinds = ScalarBlind::ZERO;
82
83        for input in self.inputs {
84            let funds_blind = Blind::random(&mut OsRng);
85            total_funds += input.note.value;
86            total_funds_blinds += funds_blind;
87
88            // Note from the previous output
89            let note = input.note;
90            let leaf_pos: u64 = input.leaf_position.into();
91
92            let public_key = PublicKey::from_secret(input.secret);
93            let coin = CoinAttributes {
94                public_key,
95                value: note.value,
96                token_id: note.token_id,
97                spend_hook: FuncId::none(),
98                user_data: pallas::Base::ZERO,
99                blind: note.coin_blind,
100            }
101            .to_coin();
102            let nullifier = poseidon_hash([input.secret.inner(), coin.inner()]);
103
104            let smt_null_path = self.money_null_smt.prove_membership(&nullifier);
105            if !smt_null_path.verify(&smt_null_root, &pallas::Base::ZERO, &nullifier) {
106                return Err(
107                    ClientFailed::VerifyError(DaoError::InvalidInputMerkleRoot.to_string()).into()
108                )
109            }
110
111            let signature_secret = SecretKey::random(&mut OsRng);
112            let signature_public = PublicKey::from_secret(signature_secret);
113            let (sig_x, sig_y) = signature_public.xy();
114
115            let prover_witnesses = vec![
116                Witness::Base(Value::known(input.secret.inner())),
117                Witness::Base(Value::known(pallas::Base::from(note.value))),
118                Witness::Base(Value::known(note.token_id.inner())),
119                Witness::Base(Value::known(pallas::Base::ZERO)),
120                Witness::Base(Value::known(pallas::Base::ZERO)),
121                Witness::Base(Value::known(note.coin_blind.inner())),
122                Witness::Scalar(Value::known(funds_blind.inner())),
123                Witness::Base(Value::known(gov_token_blind.inner())),
124                Witness::Uint32(Value::known(leaf_pos.try_into().unwrap())),
125                Witness::MerklePath(Value::known(input.merkle_path.clone().try_into().unwrap())),
126                Witness::SparseMerklePath(Value::known(smt_null_path.path)),
127                Witness::Base(Value::known(signature_secret.inner())),
128            ];
129
130            // TODO: We need a generic ZkSet widget to avoid doing this all the time
131
132            let merkle_coin_root = {
133                let position: u64 = input.leaf_position.into();
134                let mut current = MerkleNode::from(coin.inner());
135                for (level, sibling) in input.merkle_path.iter().enumerate() {
136                    let level = level as u8;
137                    current = if position & (1 << level) == 0 {
138                        MerkleNode::combine(level.into(), &current, sibling)
139                    } else {
140                        MerkleNode::combine(level.into(), sibling, &current)
141                    };
142                }
143                current
144            };
145
146            let token_commit = poseidon_hash([note.token_id.inner(), gov_token_blind.inner()]);
147            if note.token_id != self.dao.gov_token_id {
148                return Err(ClientFailed::InvalidTokenId(note.token_id.to_string()).into())
149            }
150
151            let value_commit = pedersen_commitment_u64(note.value, funds_blind);
152            let value_coords = value_commit.to_affine().coordinates().unwrap();
153
154            let public_inputs = vec![
155                smt_null_root,
156                *value_coords.x(),
157                *value_coords.y(),
158                token_commit,
159                merkle_coin_root.inner(),
160                sig_x,
161                sig_y,
162            ];
163            //darkfi::zk::export_witness_json("proof/witness/propose-input.json", &prover_witnesses, &public_inputs);
164            let circuit = ZkCircuit::new(prover_witnesses, burn_zkbin);
165
166            let proving_key = &burn_pk;
167            let input_proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng)?;
168            proofs.push(input_proof);
169            signature_secrets.push(signature_secret);
170
171            let input = DaoProposeParamsInput {
172                value_commit,
173                merkle_coin_root,
174                smt_null_root,
175                signature_public,
176            };
177            inputs.push(input);
178        }
179
180        let total_funds_commit = pedersen_commitment_u64(total_funds, total_funds_blinds);
181        let total_funds_coords = total_funds_commit.to_affine().coordinates().unwrap();
182        let total_funds = pallas::Base::from(total_funds);
183
184        let token_commit = poseidon_hash([self.dao.gov_token_id.inner(), gov_token_blind.inner()]);
185
186        let dao_proposer_limit = pallas::Base::from(self.dao.proposer_limit);
187        let dao_quorum = pallas::Base::from(self.dao.quorum);
188        let dao_early_exec_quorum = pallas::Base::from(self.dao.early_exec_quorum);
189        let dao_approval_ratio_quot = pallas::Base::from(self.dao.approval_ratio_quot);
190        let dao_approval_ratio_base = pallas::Base::from(self.dao.approval_ratio_base);
191        let (dao_notes_pub_x, dao_notes_pub_y) = self.dao.notes_public_key.xy();
192        let (dao_proposals_pub_x, dao_proposals_pub_y) = self.dao.proposals_public_key.xy();
193        let (dao_votes_pub_x, dao_votes_pub_y) = self.dao.votes_public_key.xy();
194        let (dao_exec_pub_x, dao_exec_pub_y) = self.dao.exec_public_key.xy();
195        let (dao_early_exec_pub_x, dao_early_exec_pub_y) = self.dao.early_exec_public_key.xy();
196
197        let dao_leaf_position: u64 = self.dao_leaf_position.into();
198
199        if self.dao.to_bulla() != self.proposal.dao_bulla {
200            return Err(ClientFailed::VerifyError(DaoError::InvalidCalls.to_string()).into())
201        }
202        let proposal_bulla = self.proposal.to_bulla();
203
204        let prover_witnesses = vec![
205            // Proposers total number of gov tokens
206            Witness::Base(Value::known(total_funds)),
207            Witness::Scalar(Value::known(total_funds_blinds.inner())),
208            // Used for blinding exported gov token ID
209            Witness::Base(Value::known(gov_token_blind.inner())),
210            // Proposal params
211            Witness::Base(Value::known(self.proposal.auth_calls.commit())),
212            Witness::Base(Value::known(pallas::Base::from(self.proposal.creation_blockwindow))),
213            Witness::Base(Value::known(pallas::Base::from(self.proposal.duration_blockwindows))),
214            Witness::Base(Value::known(self.proposal.user_data)),
215            Witness::Base(Value::known(self.proposal.blind.inner())),
216            // DAO params
217            Witness::Base(Value::known(dao_proposer_limit)),
218            Witness::Base(Value::known(dao_quorum)),
219            Witness::Base(Value::known(dao_early_exec_quorum)),
220            Witness::Base(Value::known(dao_approval_ratio_quot)),
221            Witness::Base(Value::known(dao_approval_ratio_base)),
222            Witness::Base(Value::known(self.dao.gov_token_id.inner())),
223            Witness::Base(Value::known(dao_notes_pub_x)),
224            Witness::Base(Value::known(dao_notes_pub_y)),
225            Witness::Base(Value::known(dao_proposer_secret_key.inner())),
226            Witness::Base(Value::known(dao_proposals_pub_x)),
227            Witness::Base(Value::known(dao_proposals_pub_y)),
228            Witness::Base(Value::known(dao_votes_pub_x)),
229            Witness::Base(Value::known(dao_votes_pub_y)),
230            Witness::Base(Value::known(dao_exec_pub_x)),
231            Witness::Base(Value::known(dao_exec_pub_y)),
232            Witness::Base(Value::known(dao_early_exec_pub_x)),
233            Witness::Base(Value::known(dao_early_exec_pub_y)),
234            Witness::Base(Value::known(self.dao.bulla_blind.inner())),
235            Witness::Uint32(Value::known(dao_leaf_position.try_into().unwrap())),
236            Witness::MerklePath(Value::known(self.dao_merkle_path.try_into().unwrap())),
237        ];
238        let public_inputs = vec![
239            token_commit,
240            self.dao_merkle_root.inner(),
241            proposal_bulla.inner(),
242            pallas::Base::from(self.proposal.creation_blockwindow),
243            *total_funds_coords.x(),
244            *total_funds_coords.y(),
245        ];
246        //darkfi::zk::export_witness_json("proof/witness/propose-main.json", &prover_witnesses, &public_inputs);
247        let circuit = ZkCircuit::new(prover_witnesses, main_zkbin);
248
249        let main_proof = Proof::create(main_pk, &[circuit], &public_inputs, &mut OsRng)?;
250        proofs.push(main_proof);
251
252        let enc_note =
253            AeadEncryptedNote::encrypt(&self.proposal, &self.dao.proposals_public_key, &mut OsRng)
254                .unwrap();
255        let params = DaoProposeParams {
256            dao_merkle_root: self.dao_merkle_root,
257            proposal_bulla,
258            token_commit,
259            note: enc_note,
260            inputs,
261        };
262
263        Ok((params, proofs, signature_secrets))
264    }
265}