Преглед изворни кода

contract/money/genesis_mint: impl multiple outputs

skoupidi пре 1 година
родитељ
комит
8f59a91e38

+ 4 - 2
bin/drk/src/money.rs

@@ -767,8 +767,10 @@ impl Drk {
             MoneyFunction::GenesisMintV1 => {
                 println!("[parse_money_call] Found Money::GenesisMintV1 call");
                 let params: MoneyGenesisMintParamsV1 = deserialize_async(&data[1..]).await?;
-                coins.push(params.output.coin);
-                notes.push(params.output.note);
+                for output in params.outputs {
+                    coins.push(output.coin);
+                    notes.push(output.note);
+                }
             }
             MoneyFunction::PoWRewardV1 => {
                 println!("[parse_money_call] Found Money::PoWRewardV1 call");

+ 12 - 9
script/research/gg/src/main.rs

@@ -75,8 +75,8 @@ enum Subcmd {
     /// Generate a Darkfi genesis transaction using the secret
     /// key from  stdin
     GenerateTx {
-        /// Amount to mint for this genesis transaction
-        amount: String,
+        /// Amounts to mint for this genesis transaction
+        amounts: Vec<String>,
 
         #[arg(short, long)]
         /// Optional recipient's public key, in case we want to mint to a different address
@@ -92,7 +92,7 @@ enum Subcmd {
     },
 }
 
-/// Auxiliary function to read a bs58 genesis block from stdin
+/// Auxiliary function to read a base64 genesis block from stdin
 async fn read_block() -> Result<BlockInfo> {
     println!("Reading genesis block from stdin...");
     let mut buf = String::new();
@@ -169,16 +169,19 @@ async fn main() -> Result<()> {
             println!("Genesis block {hash} verified successfully!");
         }
 
-        Subcmd::GenerateTx { amount, recipient, spend_hook, user_data } => {
+        Subcmd::GenerateTx { amounts, recipient, spend_hook, user_data } => {
             let mut buf = String::new();
             stdin().read_to_string(&mut buf)?;
             let signature_secret = SecretKey::from_str(buf.trim())?;
 
-            if let Err(e) = f64::from_str(&amount) {
-                eprintln!("Invalid amount: {e:?}");
-                exit(2);
+            let mut coin_amounts = vec![];
+            for amount in amounts {
+                if let Err(e) = f64::from_str(&amount) {
+                    eprintln!("Invalid amount: {e:?}");
+                    exit(2);
+                }
+                coin_amounts.push(decode_base10(&amount, 8, true)?);
             }
-            let amount = decode_base10(&amount, 8, true)?;
 
             let recipient = match recipient {
                 Some(r) => match PublicKey::from_str(&r) {
@@ -244,7 +247,7 @@ async fn main() -> Result<()> {
             // Build the contract call
             let builder = GenesisMintCallBuilder {
                 signature_public: PublicKey::from_secret(signature_secret),
-                amount,
+                amounts: coin_amounts,
                 recipient,
                 spend_hook,
                 user_data,

+ 4 - 4
src/contract/dao/tests/integration.rs

@@ -52,7 +52,7 @@ const ALICE_GOV_SUPPLY: u64 = 100_000_000;
 const BOB_GOV_SUPPLY: u64 = 100_000_000;
 const CHARLIE_GOV_SUPPLY: u64 = 100_000_000;
 // DRK token, the treasury token, supply
-const DRK_TOKEN_SUPPLY: u64 = 1_000_000_000;
+const DRK_TOKEN_SUPPLY: [u64; 1] = [1_000_000_000];
 // DAO parameters configuration
 const PROPOSER_LIMIT: u64 = 100_000_000;
 const QUORUM: u64 = 200_000_000;
@@ -128,7 +128,7 @@ fn integration_test() -> Result<()> {
         let (genesis_mint_tx, genesis_mint_params) = th
             .genesis_mint(
                 &Holder::Dao,
-                DRK_TOKEN_SUPPLY,
+                &DRK_TOKEN_SUPPLY,
                 Some(spend_hook),
                 Some(dao.to_bulla().inner()),
             )
@@ -150,7 +150,7 @@ fn integration_test() -> Result<()> {
         let _dao_tokens = &th.holders.get(&Holder::Dao).unwrap().unspent_money_coins;
         assert!(_dao_tokens.len() == 1);
         assert!(_dao_tokens[0].note.token_id == *DARK_TOKEN_ID);
-        assert!(_dao_tokens[0].note.value == DRK_TOKEN_SUPPLY);
+        assert!(_dao_tokens[0].note.value == DRK_TOKEN_SUPPLY[0]);
 
         current_block_height += 1;
 
@@ -581,7 +581,7 @@ async fn execute_transfer_proposal(
 
     let dao_wallet = th.holders.get(&Holder::Dao).unwrap();
     assert!(
-        dao_wallet.unspent_money_coins[0].note.value == DRK_TOKEN_SUPPLY - dao_treasury_decrease
+        dao_wallet.unspent_money_coins[0].note.value == DRK_TOKEN_SUPPLY[0] - dao_treasury_decrease
     );
     assert!(dao_wallet.unspent_money_coins[0].note.token_id == *DARK_TOKEN_ID);
 

+ 69 - 51
src/contract/money/src/client/genesis_mint_v1.rs

@@ -30,6 +30,7 @@ use rand::rngs::OsRng;
 
 use crate::{
     client::{
+        compute_remainder_blind,
         transfer_v1::{proof::create_transfer_mint_proof, TransferCallOutput},
         MoneyNote,
     },
@@ -61,8 +62,8 @@ impl GenesisMintRevealed {
 pub struct GenesisMintCallBuilder {
     /// Caller's public key, corresponding to the one used in the signature
     pub signature_public: PublicKey,
-    /// Amount of tokens we want to mint
-    pub amount: u64,
+    /// Vector containing each output value we want to mint
+    pub amounts: Vec<u64>,
     /// Optional recipient's public key, in case we want to mint to a different address
     pub recipient: Option<PublicKey>,
     /// Optional contract spend hook to use in the output
@@ -78,8 +79,9 @@ pub struct GenesisMintCallBuilder {
 impl GenesisMintCallBuilder {
     pub fn build(&self) -> Result<GenesisMintCallDebris> {
         debug!(target: "contract::money::client::genesis_mint", "Building Money::MintV1 contract call");
-        if self.amount == 0 {
-            return Err(ClientFailed::InvalidAmount(self.amount).into())
+        let value = self.amounts.iter().sum();
+        if value == 0 {
+            return Err(ClientFailed::InvalidAmount(value).into())
         }
 
         // In this call, we will build one clear input and one anonymous output.
@@ -89,63 +91,79 @@ impl GenesisMintCallBuilder {
         // Building the clear input using random blinds
         let value_blind = Blind::random(&mut OsRng);
         let token_blind = Blind::random(&mut OsRng);
-        let coin_blind = Blind::random(&mut OsRng);
-        let c_input = ClearInput {
-            value: self.amount,
+        let input = ClearInput {
+            value,
             token_id,
             value_blind,
             token_blind,
             signature_public: self.signature_public,
         };
 
-        // Grab the spend hook and user data to use in the output
+        // Grab the public key, spend hook and user data to use in the outputs
+        let public_key = self.recipient.unwrap_or(self.signature_public);
         let spend_hook = self.spend_hook.unwrap_or(FuncId::none());
         let user_data = self.user_data.unwrap_or(pallas::Base::ZERO);
 
-        // Building the anonymous output
-        let output = TransferCallOutput {
-            public_key: self.recipient.unwrap_or(self.signature_public),
-            value: self.amount,
-            token_id,
-            spend_hook,
-            user_data,
-            blind: Blind::random(&mut OsRng),
-        };
-
-        debug!(target: "contract::money::client::genesis_mint", "Creating token mint proof for output");
-        let (proof, public_inputs) = create_transfer_mint_proof(
-            &self.mint_zkbin,
-            &self.mint_pk,
-            &output,
-            value_blind,
-            token_blind,
-            spend_hook,
-            user_data,
-            coin_blind,
-        )?;
-
-        let note = MoneyNote {
-            value: output.value,
-            token_id: output.token_id,
-            spend_hook,
-            user_data,
-            coin_blind,
-            value_blind,
-            token_blind,
-            memo: vec![],
-        };
-
-        let encrypted_note = AeadEncryptedNote::encrypt(&note, &output.public_key, &mut OsRng)?;
-
-        let c_output = Output {
-            value_commit: public_inputs.value_commit,
-            token_commit: public_inputs.token_commit,
-            coin: public_inputs.coin,
-            note: encrypted_note,
-        };
+        // Building the anonymous outputs
+        let input_blinds = vec![value_blind];
+        let mut output_blinds = Vec::with_capacity(self.amounts.len());
+        let mut outputs = Vec::with_capacity(self.amounts.len());
+        let mut proofs = Vec::with_capacity(self.amounts.len());
+        for (i, amount) in self.amounts.iter().enumerate() {
+            let value_blind = if i == self.amounts.len() - 1 {
+                compute_remainder_blind(&input_blinds, &output_blinds)
+            } else {
+                Blind::random(&mut OsRng)
+            };
+            output_blinds.push(value_blind);
+
+            let output = TransferCallOutput {
+                public_key,
+                value: *amount,
+                token_id,
+                spend_hook,
+                user_data,
+                blind: Blind::random(&mut OsRng),
+            };
+
+            debug!(target: "contract::money::client::genesis_mint", "Creating token mint proof for output {}", i);
+            let (proof, public_inputs) = create_transfer_mint_proof(
+                &self.mint_zkbin,
+                &self.mint_pk,
+                &output,
+                value_blind,
+                token_blind,
+                spend_hook,
+                user_data,
+                output.blind,
+            )?;
+            proofs.push(proof);
+
+            let note = MoneyNote {
+                value: output.value,
+                token_id: output.token_id,
+                spend_hook,
+                user_data,
+                coin_blind: output.blind,
+                value_blind,
+                token_blind,
+                memo: vec![],
+            };
+
+            let encrypted_note = AeadEncryptedNote::encrypt(&note, &public_key, &mut OsRng)?;
+
+            let output = Output {
+                value_commit: public_inputs.value_commit,
+                token_commit: public_inputs.token_commit,
+                coin: public_inputs.coin,
+                note: encrypted_note,
+            };
+
+            outputs.push(output);
+        }
 
-        let params = MoneyGenesisMintParamsV1 { input: c_input, output: c_output };
-        let debris = GenesisMintCallDebris { params, proofs: vec![proof] };
+        let params = MoneyGenesisMintParamsV1 { input, outputs };
+        let debris = GenesisMintCallDebris { params, proofs };
         Ok(debris)
     }
 }

+ 58 - 37
src/contract/money/src/entrypoint/genesis_mint_v1.rs

@@ -49,18 +49,15 @@ pub(crate) fn money_genesis_mint_get_metadata_v1(
     // Public keys for the transaction signatures we have to verify
     let signature_pubkeys = vec![params.input.signature_public];
 
-    // Grab the pedersen commitment from the anonymous output
-    let value_coords = params.output.value_commit.to_affine().coordinates().unwrap();
-
-    zk_public_inputs.push((
-        MONEY_CONTRACT_ZKAS_MINT_NS_V1.to_string(),
-        vec![
-            params.output.coin.inner(),
-            *value_coords.x(),
-            *value_coords.y(),
-            params.output.token_commit,
-        ],
-    ));
+    // Grab the pedersen commitments from the anonymous outputs
+    for output in &params.outputs {
+        let value_coords = output.value_commit.to_affine().coordinates().unwrap();
+
+        zk_public_inputs.push((
+            MONEY_CONTRACT_ZKAS_MINT_NS_V1.to_string(),
+            vec![output.coin.inner(), *value_coords.x(), *value_coords.y(), output.token_commit],
+        ));
+    }
 
     // Serialize everything gathered and return it
     let mut metadata = vec![];
@@ -95,35 +92,56 @@ pub(crate) fn money_genesis_mint_process_instruction_v1(
         return Err(MoneyError::TransferClearInputNonNativeToken.into())
     }
 
+    // Check outputs exist
+    if params.outputs.is_empty() {
+        msg!("[GenesisMintV1] Error: No outputs in the call");
+        return Err(MoneyError::TransferMissingOutputs.into())
+    }
+
     // Access the necessary databases where there is information to
     // validate this state transition.
     let coins_db = wasm::db::db_lookup(cid, MONEY_CONTRACT_COINS_TREE)?;
 
-    // Check that the coin from the output hasn't existed before.
-    if wasm::db::db_contains_key(coins_db, &serialize(&params.output.coin))? {
-        msg!("[GenesisMintV1] Error: Duplicate coin in output");
-        return Err(MoneyError::DuplicateCoin.into())
+    // Compute the expected token commitment of the outputs
+    let tokcom = poseidon_hash([params.input.token_id.inner(), params.input.token_blind.inner()]);
+
+    // Accumulator for the outputs value commitments. For the commitments to
+    // be valid, the accumulator must reach the input value commitment.
+    let mut valcom_total = pallas::Point::identity();
+
+    // Newly created coins for this call are in the outputs. Here we gather them,
+    // check that they haven't existed before and their token commitment is valid.
+    let mut new_coins = Vec::with_capacity(params.outputs.len());
+    msg!("[GenesisMintV1] Iterating over anonymous outputs");
+    for (i, output) in params.outputs.iter().enumerate() {
+        // Check that the coin has not existed before
+        if new_coins.contains(&output.coin) ||
+            wasm::db::db_contains_key(coins_db, &serialize(&output.coin))?
+        {
+            msg!("[GenesisMintV1] Error: Duplicate coin found in output {}", i);
+            return Err(MoneyError::DuplicateCoin.into())
+        }
+
+        // Verify the token commitment is the expected one
+        if tokcom != output.token_commit {
+            msg!("[GenesisMintV1] Error: Token commitment mismatch in output {}", i);
+            return Err(MoneyError::TokenMismatch.into())
+        }
+
+        // Append this new coin to seen coins, and accumulate the value commitment
+        new_coins.push(output.coin);
+        valcom_total += output.value_commit;
     }
 
-    // Verify that the value and token commitments match. In here we just
-    // confirm that the clear input and the anon output have the same
-    // commitments.
-    if pedersen_commitment_u64(params.input.value, params.input.value_blind) !=
-        params.output.value_commit
-    {
-        msg!("[GenesisMintV1] Error: Value commitment mismatch");
+    // If the accumulator doesn't result in the input value commitment, there
+    // is a value mismatch between input and outputs.
+    if valcom_total != pedersen_commitment_u64(params.input.value, params.input.value_blind) {
+        msg!("[GenesisMintV1] Error: Output value commitments do not result in input value commitment");
         return Err(MoneyError::ValueMismatch.into())
     }
 
-    if poseidon_hash([params.input.token_id.inner(), params.input.token_blind.inner()]) !=
-        params.output.token_commit
-    {
-        msg!("[GenesisMintV1] Error: Token commitment mismatch");
-        return Err(MoneyError::TokenMismatch.into())
-    }
-
-    // Create a state update. We only need the new coin.
-    let update = MoneyGenesisMintUpdateV1 { coin: params.output.coin };
+    // Create a state update. We only need the new coins.
+    let update = MoneyGenesisMintUpdateV1 { coins: new_coins };
     let mut update_data = vec![];
     update_data.write_u8(MoneyFunction::GenesisMintV1 as u8)?;
     update.encode(&mut update_data)?;
@@ -153,17 +171,20 @@ pub(crate) fn money_genesis_mint_process_update_v1(
         &[],
     )?;
 
-    msg!("[GenesisMintV1] Adding new coin to the set");
-    wasm::db::db_set(coins_db, &serialize(&update.coin), &[])?;
+    msg!("[GenesisMintV1] Adding new coins to the set");
+    let mut new_coins = Vec::with_capacity(update.coins.len());
+    for coin in &update.coins {
+        wasm::db::db_set(coins_db, &serialize(coin), &[])?;
+        new_coins.push(MerkleNode::from(coin.inner()));
+    }
 
-    msg!("[GenesisMintV1] Adding new coin to the Merkle tree");
-    let coins = vec![MerkleNode::from(update.coin.inner())];
+    msg!("[GenesisMintV1] Adding new coins to the Merkle tree");
     wasm::merkle::merkle_add(
         info_db,
         coin_roots_db,
         MONEY_CONTRACT_LATEST_COIN_ROOT,
         MONEY_CONTRACT_COIN_MERKLE_TREE,
-        &coins,
+        &new_coins,
     )?;
 
     Ok(())

+ 4 - 4
src/contract/money/src/model/mod.rs

@@ -217,15 +217,15 @@ pub struct MoneyTransferUpdateV1 {
 pub struct MoneyGenesisMintParamsV1 {
     /// Clear input
     pub input: ClearInput,
-    /// Anonymous output
-    pub output: Output,
+    /// Anonymous outputs
+    pub outputs: Vec<Output>,
 }
 
 /// State update for `Money::GenesisMint`
 #[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
 pub struct MoneyGenesisMintUpdateV1 {
-    /// The newly minted coin
-    pub coin: Coin,
+    /// The newly minted coins
+    pub coins: Vec<Coin>,
 }
 
 /// Parameters for `Money::TokenMint`

+ 8 - 5
src/contract/money/tests/genesis_mint.rs

@@ -38,8 +38,8 @@ fn genesis_mint() -> Result<()> {
         const HOLDERS: [Holder; 2] = [Holder::Alice, Holder::Bob];
 
         // Some numbers we want to assert
-        const ALICE_INITIAL: u64 = 100;
-        const BOB_INITIAL: u64 = 200;
+        const ALICE_INITIAL: [u64; 1] = [100];
+        const BOB_INITIAL: [u64; 2] = [100, 100];
 
         // Block height to verify against
         let current_block_height = 0;
@@ -51,7 +51,7 @@ fn genesis_mint() -> Result<()> {
         info!(target: "money", "[Alice] Building genesis mint tx");
         info!(target: "money", "[Alice] ========================");
         let (genesis_mint_tx, genesis_mint_params) =
-            th.genesis_mint(&Holder::Alice, ALICE_INITIAL, None, None).await?;
+            th.genesis_mint(&Holder::Alice, &ALICE_INITIAL, None, None).await?;
 
         info!(target: "money", "[Malicious] =============================================");
         info!(target: "money", "[Malicious] Checking genesis mint tx not on genesis block");
@@ -87,7 +87,7 @@ fn genesis_mint() -> Result<()> {
         info!(target: "money", "[Bob] Building genesis mint tx");
         info!(target: "money", "[Bob] ========================");
         let (genesis_mint_tx, genesis_mint_params) =
-            th.genesis_mint(&Holder::Bob, BOB_INITIAL, None, None).await?;
+            th.genesis_mint(&Holder::Bob, &BOB_INITIAL, None, None).await?;
 
         for holder in &HOLDERS {
             info!(target: "money", "[{holder:?}] =============================");
@@ -108,7 +108,10 @@ fn genesis_mint() -> Result<()> {
         let alice_owncoins = &th.holders.get(&Holder::Alice).unwrap().unspent_money_coins;
         let bob_owncoins = &th.holders.get(&Holder::Bob).unwrap().unspent_money_coins;
         assert!(alice_owncoins.len() == 1);
-        assert!(bob_owncoins.len() == 1);
+        assert!(alice_owncoins[0].note.value == ALICE_INITIAL[0]);
+        assert!(bob_owncoins.len() == 2);
+        assert!(bob_owncoins[0].note.value == BOB_INITIAL[0]);
+        assert!(bob_owncoins[1].note.value == BOB_INITIAL[1]);
 
         // Thanks for reading
         Ok(())

+ 24 - 18
src/contract/test-harness/src/money_genesis_mint.rs

@@ -42,7 +42,7 @@ impl TestHarness {
     pub async fn genesis_mint(
         &mut self,
         holder: &Holder,
-        amount: u64,
+        amounts: &[u64],
         spend_hook: Option<FuncId>,
         user_data: Option<pallas::Base>,
     ) -> Result<(Transaction, MoneyGenesisMintParamsV1)> {
@@ -53,7 +53,7 @@ impl TestHarness {
         // Build the contract call
         let builder = GenesisMintCallBuilder {
             signature_public: wallet.keypair.public,
-            amount,
+            amounts: amounts.to_vec(),
             recipient: None,
             spend_hook,
             user_data,
@@ -96,22 +96,28 @@ impl TestHarness {
             return Ok(vec![])
         }
 
-        wallet.money_merkle_tree.append(MerkleNode::from(params.output.coin.inner()));
-
-        let Ok(note) = params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
-            return Ok(vec![])
-        };
-
-        let owncoin = OwnCoin {
-            coin: params.output.coin,
-            note: note.clone(),
-            secret: wallet.keypair.secret,
-            leaf_position: wallet.money_merkle_tree.mark().unwrap(),
-        };
-
-        debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
-        wallet.unspent_money_coins.push(owncoin.clone());
+        // Iterate over call outputs to find any new OwnCoins
+        let mut found_owncoins = vec![];
+        for output in &params.outputs {
+            wallet.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
+
+            // Attempt to decrypt the output note to see if this is a coin for the holder.
+            let Ok(note) = output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
+                continue
+            };
+
+            let owncoin = OwnCoin {
+                coin: output.coin,
+                note: note.clone(),
+                secret: wallet.keypair.secret,
+                leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+            };
+
+            debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+            wallet.unspent_money_coins.push(owncoin.clone());
+            found_owncoins.push(owncoin);
+        }
 
-        Ok(vec![owncoin])
+        Ok(found_owncoins)
     }
 }