diff --git a/src/contract/money/proof/token_mint_v1.zk b/src/contract/money/proof/token_mint_v1.zk index 0add4f537..ca3e17ce8 100644 --- a/src/contract/money/proof/token_mint_v1.zk +++ b/src/contract/money/proof/token_mint_v1.zk @@ -1,3 +1,4 @@ +# Circuit used to mint arbitrary coins given a mint authority secret. constant "TokenMint_V1" { EcFixedPointShort VALUE_COMMIT_VALUE, EcFixedPoint VALUE_COMMIT_RANDOM, @@ -9,15 +10,11 @@ contract "TokenMint_V1" { Base mint_authority, # Token supply Base supply, - # Fixed supply - Base fixed_supply, # Recipient's public key x coordinate Base rcpt_x, # Recipient's public key y coordinate Base rcpt_y, - # Unique serial number corresponding to this coin - Base serial, - # Random blinding factor for coin + # Random blinding factor for the minted coin Base coin_blind, # Allows composing this ZK proof to invoke other contracts Base spend_hook, @@ -41,12 +38,7 @@ circuit "TokenMint_V1" { token_id = poseidon_hash(mint_x, mint_y); constrain_instance(token_id); - # Constrain whether this token has a fixed supply or not. - # In case it is, subsequent mints will not be allowed. - bool_check(fixed_supply); - constrain_instance(fixed_supply); - - # Poseidon hash of the coin + # Poseidon hash of the minted coin C = poseidon_hash( rcpt_x, rcpt_y, diff --git a/src/contract/money/src/client.rs b/src/contract/money/src/client.rs index 956f66a8d..78dd7baf8 100644 --- a/src/contract/money/src/client.rs +++ b/src/contract/money/src/client.rs @@ -50,6 +50,9 @@ use crate::model::{ StakedInput, StakedOutput, }; +/// Client API for token minting and freezing +pub mod token_mint; + // Wallet SQL table constant names. These have to represent the SQL schema. // TODO: They should also ideally be prefixed with the contract ID to avoid // collisions. @@ -81,6 +84,11 @@ pub const MONEY_COINS_COL_NULLIFIER: &str = "nullifier"; pub const MONEY_COINS_COL_LEAF_POSITION: &str = "leaf_position"; pub const MONEY_COINS_COL_MEMO: &str = "memo"; +pub const MONEY_TOKENS_TABLE: &str = "money_tokens"; +pub const MONEY_TOKENS_COL_SECRET: &str = "secret"; +pub const MONEY_TOKENS_COL_TOKEN_ID: &str = "token_id"; +pub const MONEY_TOKENS_COL_FROZEN: &str = "frozen"; + pub const MONEY_ALIASES_TABLE: &str = "money_aliases"; pub const MONEY_ALIASES_COL_ALIAS: &str = "alias"; pub const MONEY_ALIASES_COL_TOKEN_ID: &str = "token_id"; diff --git a/src/contract/money/src/client/token_mint.rs b/src/contract/money/src/client/token_mint.rs new file mode 100644 index 000000000..2fa71ee6f --- /dev/null +++ b/src/contract/money/src/client/token_mint.rs @@ -0,0 +1,97 @@ +/* This file is part of DarkFi (https://dark.fi) + * + * Copyright (C) 2020-2023 Dyne.org foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +//! This API is experimental. Have to evaluate the best approach still. + +pub struct MoneyMintClient { + pub mint_authority: SecretKey, + pub amount: u64, + pub recipient: PublicKey, + pub spend_hook: pallas::Base, + pub user_data: pallas::Base, +} + +impl MoneyMintClient { + pub fn build(&self) -> Result { + debug!(target: "money::client::token_mint", "Building params"); + assert!(self.amount != 0); + + let token_id = TokenId::derive(self.mint_authority); + let value_blind = pallas::Scalar::random(&mut OsRng); + let token_blind = pallas::Scalar::random(&mut OsRng); + + let serial = pallas::Base::random(&mut OsRng); + let coin_blind = pallas::Base::random(&mut OsRng); + + let (pub_x, pub_y) = self.recipient.xy(); + + // Create the clear input + let input = ClearInput { + value: self.amount, + token_id, + value_blind, + token_blind, + signature_public: PublicKey::from_secret(mint_authority), + }; + + // Create the anonymous output + let coin = poseidon_hash([ + pub_x, + pub_y, + pallas::Base::from(self.amount), + token_id.inner(), + serial, + self.spend_hook, + self.user_data, + coin_blind, + ]); + + let note = MoneyNote { + serial, + value: self.amount, + token_id, + spend_hook, + user_data, + coin_blind, + value_blind, + token_blind, + memo: vec![], + }; + + let encrypted_note = note.encrypt(&self.recipient)?; + + let output = Output { + value_commit: pedersen_commitment_u64(self.amount, value_blind), + token_commit: pedersen_commitment_base(token_id, token_blind), + coin, + ciphertext: encrypted_note.ciphertext, + ephem_public: encrypted_note.ephem_public, + }; + + Ok(MoneyMintParams { input, output }) + } + + pub fn prove(&self, params: &MoneyMintParams, zkbin: &ZkBinary, proving_key: &ProvingKey) -> Result { + let prover_witnesses = vec![]; + + let circuit = ZkCircuit::new(prover_witnesses, zkbin.clone()); + let proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng)?; + + Ok(proof) + } +} diff --git a/src/contract/money/src/lib.rs b/src/contract/money/src/lib.rs index 6ed9bafae..9621157e5 100644 --- a/src/contract/money/src/lib.rs +++ b/src/contract/money/src/lib.rs @@ -19,12 +19,11 @@ #[cfg(not(feature = "no-entrypoint"))] use darkfi_sdk::{ crypto::{ - pallas, pasta_prelude::*, pedersen_commitment_base, pedersen_commitment_u64, Coin, - ContractId, MerkleNode, MerkleTree, PublicKey, DARK_TOKEN_ID, + pallas, pasta_prelude::*, pedersen_commitment_base, Coin, ContractId, MerkleNode, + MerkleTree, PublicKey, DARK_TOKEN_ID, }, db::{ - db_contains_key, db_get, db_init, db_lookup, db_set, set_return_data, - SMART_CONTRACT_ZKAS_DB_NAME, + db_contains_key, db_init, db_lookup, db_set, set_return_data, SMART_CONTRACT_ZKAS_DB_NAME, }, error::ContractResult, merkle::merkle_add, @@ -66,10 +65,22 @@ impl TryFrom for MoneyFunction { /// Structures and object definitions pub mod model; +// Contract functionalities +mod mint; +use mint::{money_mint_get_metadata, money_mint_process_instruction, money_mint_process_update}; +mod swap; +use swap::{ + money_otcswap_get_metadata, money_otcswap_process_instruction, money_otcswap_process_update, +}; +mod transfer; +use transfer::{ + money_transfer_get_metadata, money_transfer_process_instruction, money_transfer_process_update, +}; + #[cfg(not(feature = "no-entrypoint"))] use model::{ - MoneyStakeParams, MoneyStakeUpdate, MoneyTransferParams, MoneyTransferUpdate, - MoneyUnstakeParams, + MoneyMintParams, MoneyMintUpdate, MoneyStakeParams, MoneyStakeUpdate, MoneyTransferParams, + MoneyTransferUpdate, MoneyUnstakeParams, }; #[cfg(feature = "client")] @@ -87,7 +98,6 @@ darkfi_sdk::define_contract!( // These are the different sled trees that will be created pub const MONEY_CONTRACT_COIN_ROOTS_TREE: &str = "coin_roots"; pub const MONEY_CONTRACT_NULLIFIERS_TREE: &str = "nullifiers"; -pub const MONEY_CONTRACT_TOKEN_ROOTS_TREE: &str = "token_roots"; pub const MONEY_CONTRACT_TOKEN_FREEZE_TREE: &str = "token_freezes"; pub const MONEY_CONTRACT_INFO_TREE: &str = "info"; // lead coin, nullifier sled trees. @@ -137,8 +147,8 @@ fn init_contract(cid: ContractId, ix: &[u8]) -> ContractResult { let lead_mint_v1_bincode = include_bytes!("../proof/lead_mint_v1.zk.bin"); let lead_burn_v1_bincode = include_bytes!("../proof/lead_burn_v1.zk.bin"); - /* TODO: Do I really want to make zkas a dependency? Yeah, in the future. - For now we take anything. + /* For now we take anything, but the zkas db needs protection against + arbitrary data. let zkbin = ZkBinary::decode(mint_bincode)?; let mint_namespace = zkbin.namespace.clone(); assert_eq!(&mint_namespace, ZKAS_MINT_NS); @@ -166,11 +176,6 @@ fn init_contract(cid: ContractId, ix: &[u8]) -> ContractResult { db_init(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?; } - // Set up a database tree to hold Merkle roots of all tokens - if db_lookup(cid, MONEY_CONTRACT_TOKEN_ROOTS_TREE).is_err() { - db_init(cid, MONEY_CONTRACT_TOKEN_ROOTS_TREE)?; - } - // Set up a database tree to hold a set of frozen token mints if db_lookup(cid, MONEY_CONTRACT_TOKEN_FREEZE_TREE).is_err() { db_init(cid, MONEY_CONTRACT_TOKEN_FREEZE_TREE)?; @@ -214,71 +219,26 @@ fn init_contract(cid: ContractId, ix: &[u8]) -> ContractResult { /// This function is used by the VM's host to fetch the necessary metadata for /// verifying signatures and zk proofs. #[cfg(not(feature = "no-entrypoint"))] -fn get_metadata(_cid: ContractId, ix: &[u8]) -> ContractResult { - let (call_idx, call): (u32, Vec) = deserialize(ix)?; - assert!(call_idx < call.len() as u32); +fn get_metadata(cid: ContractId, ix: &[u8]) -> ContractResult { + let (call_idx, calls): (u32, Vec) = deserialize(ix)?; + assert!(call_idx < calls.len() as u32); - let self_ = &call[call_idx as usize]; + let self_ = &calls[call_idx as usize]; match MoneyFunction::try_from(self_.data[0])? { - MoneyFunction::Transfer | MoneyFunction::OtcSwap => { - let params: MoneyTransferParams = deserialize(&self_.data[1..])?; - - let mut zk_public_values: Vec<(String, Vec)> = vec![]; - let mut signature_pubkeys: Vec = vec![]; - - for input in ¶ms.clear_inputs { - signature_pubkeys.push(input.signature_public); - } - - for input in ¶ms.inputs { - let value_coords = input.value_commit.to_affine().coordinates().unwrap(); - let token_coords = input.token_commit.to_affine().coordinates().unwrap(); - let (sig_x, sig_y) = input.signature_public.xy(); - - zk_public_values.push(( - MONEY_CONTRACT_ZKAS_BURN_NS_V1.to_string(), - vec![ - input.nullifier.inner(), - *value_coords.x(), - *value_coords.y(), - *token_coords.x(), - *token_coords.y(), - input.merkle_root.inner(), - input.user_data_enc, - sig_x, - sig_y, - ], - )); - - signature_pubkeys.push(input.signature_public); - } - - for output in ¶ms.outputs { - let value_coords = output.value_commit.to_affine().coordinates().unwrap(); - let token_coords = output.token_commit.to_affine().coordinates().unwrap(); - - zk_public_values.push(( - MONEY_CONTRACT_ZKAS_MINT_NS_V1.to_string(), - vec![ - output.coin, - *value_coords.x(), - *value_coords.y(), - *token_coords.x(), - *token_coords.y(), - ], - )); - } - - let mut metadata = vec![]; - zk_public_values.encode(&mut metadata)?; - signature_pubkeys.encode(&mut metadata)?; - + MoneyFunction::Transfer => { + let metadata = money_transfer_get_metadata(cid, call_idx, calls)?; // Using this, we pass the above data to the host. set_return_data(&metadata)?; Ok(()) } + MoneyFunction::OtcSwap => { + let metadata = money_otcswap_get_metadata(cid, call_idx, calls)?; + set_return_data(&metadata)?; + Ok(()) + } + MoneyFunction::Stake => { let params: MoneyStakeParams = deserialize(&self_.data[1..])?; @@ -376,8 +336,10 @@ fn get_metadata(_cid: ContractId, ix: &[u8]) -> ContractResult { } MoneyFunction::Mint => { - msg!("[Mint] Entered match arm"); - unimplemented!(); + let metadata = money_mint_get_metadata(cid, call_idx, calls)?; + // Using this, we pass the above data to the host. + set_return_data(&metadata)?; + Ok(()) } MoneyFunction::Freeze => { @@ -391,222 +353,29 @@ fn get_metadata(_cid: ContractId, ix: &[u8]) -> ContractResult { /// update if everything is successful. #[cfg(not(feature = "no-entrypoint"))] fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult { - let (call_idx, call): (u32, Vec) = deserialize(ix)?; - assert!(call_idx < call.len() as u32); + let (call_idx, calls): (u32, Vec) = deserialize(ix)?; - let self_ = &call[call_idx as usize]; + if call_idx >= calls.len() as u32 { + msg!("Error: call_idx >= calls.len()"); + return Err(ContractError::Internal) + } + + let self_ = &calls[call_idx as usize]; match MoneyFunction::try_from(self_.data[0])? { MoneyFunction::Transfer => { msg!("[Transfer] Entered match arm"); - let params: MoneyTransferParams = deserialize(&self_.data[1..])?; - - assert!(params.clear_inputs.len() + params.inputs.len() > 0); - assert!(!params.outputs.is_empty()); - - let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?; - let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?; - let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; - - let Some(faucet_pubkeys) = db_get(info_db, &serialize(&MONEY_CONTRACT_FAUCET_PUBKEYS))? else { - msg!("[Transfer] Error: Missing faucet pubkeys from info db"); - return Err(ContractError::Internal); - }; - let faucet_pubkeys: Vec = deserialize(&faucet_pubkeys)?; - - // Accumulator for the value commitments - let mut valcom_total = pallas::Point::identity(); - - // State transition for payments - msg!("[Transfer] Iterating over clear inputs"); - for (i, input) in params.clear_inputs.iter().enumerate() { - let pk = input.signature_public; - - if !faucet_pubkeys.contains(&pk) { - msg!("[Transfer] Error: Clear input {} has invalid faucet pubkey", i); - return Err(ContractError::Custom(20)) - } - - valcom_total += pedersen_commitment_u64(input.value, input.value_blind); - } - - let mut new_nullifiers = Vec::with_capacity(params.inputs.len()); - - msg!("[Transfer] Iterating over anonymous inputs"); - for (i, input) in params.inputs.iter().enumerate() { - // The Merkle root is used to know whether this is a coin that existed - // in a previous state. - if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? { - msg!("[Transfer] Error: Merkle root not found in previous state (input {})", i); - return Err(ContractError::Custom(21)) - } - - // The nullifiers should not already exist. It is the double-spend protection. - if new_nullifiers.contains(&input.nullifier) || - db_contains_key(nullifiers_db, &serialize(&input.nullifier))? - { - msg!("[Transfer] Error: Duplicate nullifier found in input {}", i); - return Err(ContractError::Custom(22)) - } - - // Check the invoked contract if spend hook is set - if !bool::from(input.spend_hook.is_zero()) { - let next_call_idx = call_idx + 1; - if next_call_idx >= call.len() as u32 { - msg!( - "[Transfer] Error: next_call_idx = {} but len(calls) = {} in input {}", - next_call_idx, - call.len(), - i - ); - return Err(ContractError::Custom(23)) - } - - let next = &call[next_call_idx as usize]; - if next.contract_id.inner() != input.spend_hook { - msg!( - "[Transfer] Error: invoking contract call does not match spend hook\ - in input {}", - i - ); - return Err(ContractError::Custom(24)) - } - } - - new_nullifiers.push(input.nullifier); - valcom_total += input.value_commit; - } - - // Newly created coins for this transaction are in the outputs. - let mut new_coins = Vec::with_capacity(params.outputs.len()); - for (i, output) in params.outputs.iter().enumerate() { - // TODO: Should we have coins in a sled tree too to check dupes? - if new_coins.contains(&Coin::from(output.coin)) { - msg!("[Transfer] Error: Duplicate coin found in output {}", i); - return Err(ContractError::Custom(25)) - } - - // FIXME: Needs some work on types and their place within all these libraries - new_coins.push(Coin::from(output.coin)); - valcom_total -= output.value_commit; - } - - // If the accumulator is not back in its initial state, there's a value mismatch. - if valcom_total != pallas::Point::identity() { - msg!("[Transfer] Error: Value commitments do not result in identity"); - return Err(ContractError::Custom(26)) - } - - // Verify that the token commitments are all for the same token - let tokcom = params.outputs[0].token_commit; - let mut failed_tokcom = params.inputs.iter().any(|input| input.token_commit != tokcom); - - failed_tokcom = - failed_tokcom || params.outputs.iter().any(|output| output.token_commit != tokcom); - - failed_tokcom = failed_tokcom || - params.clear_inputs.iter().any(|input| { - pedersen_commitment_base(input.token_id.inner(), input.token_blind) != tokcom - }); - - if failed_tokcom { - msg!("[Transfer] Error: Token commitments do not match"); - return Err(ContractError::Custom(25)) - } - - // Create a state update - let update = MoneyTransferUpdate { nullifiers: new_nullifiers, coins: new_coins }; - let mut update_data = vec![]; - update_data.write_u8(MoneyFunction::Transfer as u8)?; - update.encode(&mut update_data)?; + let update_data = money_transfer_process_instruction(cid, call_idx, calls)?; set_return_data(&update_data)?; msg!("[Transfer] State update set!"); - Ok(()) } MoneyFunction::OtcSwap => { msg!("[OtcSwap] Entered match arm"); - let params: MoneyTransferParams = deserialize(&self_.data[1..])?; - - let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?; - let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; - - // State transition for OTC swaps - // For now we enforce 2 inputs and 2 outputs, which means the coins - // must be available beforehand. We might want to change this and - // allow transactions including leftover change. - assert!(params.clear_inputs.is_empty()); - assert!(params.inputs.len() == 2); - assert!(params.outputs.len() == 2); - - let mut new_nullifiers = Vec::with_capacity(params.inputs.len()); - - // inputs[0] is being swapped to outputs[1] - // inputs[1] is being swapped to outputs[0] - // So that's how we check the value and token commitments - if params.inputs[0].value_commit != params.outputs[1].value_commit { - msg!("[OtcSwap] Error: Value commitments for input 0 and output 1 do not match"); - return Err(ContractError::Custom(24)) - } - - if params.inputs[1].value_commit != params.outputs[0].value_commit { - msg!("[OtcSwap] Error: Value commitments for input 1 and output 0 do not match"); - return Err(ContractError::Custom(24)) - } - - if params.inputs[0].token_commit != params.outputs[1].token_commit { - msg!("[OtcSwap] Error: Token commitments for input 0 and output 1 do not match"); - return Err(ContractError::Custom(25)) - } - - if params.inputs[1].token_commit != params.outputs[0].token_commit { - msg!("[OtcSwap] Error: Token commitments for input 1 and output 0 do not match"); - return Err(ContractError::Custom(25)) - } - - msg!("[OtcSwap] Iterating over anonymous inputs"); - for (i, input) in params.inputs.iter().enumerate() { - // The Merkle root is used to know whether this is a coin that - // existed in a previous state. - if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? { - msg!("[OtcSwap] Error: Merkle root not found in previous state (input {})", i); - return Err(ContractError::Custom(21)) - } - - // The nullifiers should not already exist. It is the double-spend protection. - if new_nullifiers.contains(&input.nullifier) || - db_contains_key(nullifiers_db, &serialize(&input.nullifier))? - { - msg!("[OtcSwap] Error: Duplicate nullifier found in input {}", i); - return Err(ContractError::Custom(22)) - } - - new_nullifiers.push(input.nullifier); - } - - // Newly created coins for this transaction are in the outputs. - let mut new_coins = Vec::with_capacity(params.outputs.len()); - for (i, output) in params.outputs.iter().enumerate() { - // TODO: Should we have coins in a sled tree too to check dupes? - if new_coins.contains(&Coin::from(output.coin)) { - msg!("[OtcSwap] Error: Duplicate coin found in output {}", i); - return Err(ContractError::Custom(23)) - } - - // FIXME: Needs some work on types and their place within all these libraries - new_coins.push(Coin::from(output.coin)); - } - - // Create a state update. We also use the MoneyTransferUpdate because they're - // essentially the same thing just with a different transition ruleset. - let update = MoneyTransferUpdate { nullifiers: new_nullifiers, coins: new_coins }; - let mut update_data = vec![]; - update_data.write_u8(MoneyFunction::OtcSwap as u8)?; - update.encode(&mut update_data)?; + let update_data = money_otcswap_process_instruction(cid, call_idx, calls)?; set_return_data(&update_data)?; msg!("[OtcSwap] State update set!"); - Ok(()) } @@ -766,7 +535,10 @@ fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult { MoneyFunction::Mint => { msg!("[Mint] Entered match arm"); - unimplemented!(); + let update_data = money_mint_process_instruction(cid, call_idx, calls)?; + set_return_data(&update_data)?; + msg!("[Mint] State update set!"); + Ok(()) } MoneyFunction::Freeze => { @@ -779,26 +551,15 @@ fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult { #[cfg(not(feature = "no-entrypoint"))] fn process_update(cid: ContractId, update_data: &[u8]) -> ContractResult { match MoneyFunction::try_from(update_data[0])? { - MoneyFunction::Transfer | MoneyFunction::OtcSwap => { + MoneyFunction::Transfer => { let update: MoneyTransferUpdate = deserialize(&update_data[1..])?; + money_transfer_process_update(cid, update)?; + Ok(()) + } - let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?; - let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?; - let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; - - for nullifier in update.nullifiers { - db_set(nullifiers_db, &serialize(&nullifier), &[])?; - } - - msg!("Adding coins {:?} to Merkle tree", update.coins); - let coins: Vec<_> = update.coins.iter().map(|x| MerkleNode::from(x.inner())).collect(); - merkle_add( - info_db, - coin_roots_db, - &serialize(&MONEY_CONTRACT_COIN_MERKLE_TREE), - &coins, - )?; - + MoneyFunction::OtcSwap => { + let update: MoneyTransferUpdate = deserialize(&update_data[1..])?; + money_otcswap_process_update(cid, update)?; Ok(()) } @@ -826,8 +587,9 @@ fn process_update(cid: ContractId, update_data: &[u8]) -> ContractResult { } MoneyFunction::Mint => { - msg!("[Mint] Entered match arm"); - unimplemented!(); + let update: MoneyMintUpdate = deserialize(&update_data[1..])?; + money_mint_process_update(cid, update)?; + Ok(()) } MoneyFunction::Freeze => { diff --git a/src/contract/money/src/mint.rs b/src/contract/money/src/mint.rs new file mode 100644 index 000000000..2703bfcdf --- /dev/null +++ b/src/contract/money/src/mint.rs @@ -0,0 +1,145 @@ +/* This file is part of DarkFi (https://dark.fi) + * + * Copyright (C) 2020-2023 Dyne.org foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use darkfi_sdk::{ + crypto::{ + pasta_prelude::*, pedersen_commitment_base, pedersen_commitment_u64, poseidon_hash, Coin, + ContractId, MerkleNode, PublicKey, TokenId, + }, + db::{db_contains_key, db_lookup}, + error::ContractError, + merkle_add, msg, + pasta::pallas, + ContractCall, +}; +use darkfi_serial::{deserialize, serialize, Encodable, WriteExt}; + +use crate::{ + MoneyFunction, MoneyMintParams, MoneyMintUpdate, MONEY_CONTRACT_COIN_MERKLE_TREE, + MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_INFO_TREE, MONEY_CONTRACT_TOKEN_FREEZE_TREE, + MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1, +}; + +pub fn money_mint_get_metadata( + _cid: ContractId, + call_idx: u32, + calls: Vec, +) -> Result, ContractError> { + let self_ = &calls[call_idx as usize]; + let params: MoneyMintParams = deserialize(&self_.data[1..])?; + + let mut zk_public_values: Vec<(String, Vec)> = vec![]; + let mut signature_pubkeys: Vec = vec![]; + + signature_pubkeys.push(params.input.signature_public); + + let value_coords = params.output.value_commit.to_affine().coordinates().unwrap(); + let token_coords = params.output.token_commit.to_affine().coordinates().unwrap(); + + let (sig_x, sig_y) = params.input.signature_public.xy(); + let token_id = poseidon_hash([sig_x, sig_y]); + + zk_public_values.push(( + MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1.to_string(), + vec![ + sig_x, + sig_y, + token_id, + params.output.coin, + *value_coords.x(), + *value_coords.y(), + *token_coords.x(), + *token_coords.y(), + ], + )); + + let mut metadata = vec![]; + zk_public_values.encode(&mut metadata)?; + signature_pubkeys.encode(&mut metadata)?; + + Ok(metadata) +} + +pub fn money_mint_process_instruction( + cid: ContractId, + call_idx: u32, + calls: Vec, +) -> Result, ContractError> { + let self_ = &calls[call_idx as usize]; + let params: MoneyMintParams = deserialize(&self_.data[1..])?; + + //let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?; + //let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; + let token_freeze_db = db_lookup(cid, MONEY_CONTRACT_TOKEN_FREEZE_TREE)?; + + // Check that the signature public key is actually the token ID + let (mint_x, mint_y) = params.input.signature_public.xy(); + let token_id = TokenId::from(poseidon_hash([mint_x, mint_y])); + if token_id != params.input.token_id { + msg!("[Mint] Token ID does not derive from mint authority"); + return Err(ContractError::Custom(18)) + } + + // Check that the mint is not frozen + if db_contains_key(token_freeze_db, &serialize(&token_id))? { + msg!("[Mint] Error: The mint for token {} is frozen", token_id); + return Err(ContractError::Custom(19)) + } + + // TODO: Check that the new coin did not exist before. We should + // probably have a sled tree of all coins ever in order to + // assert against duplicates. + + // Verify that the value and token commitments match + if pedersen_commitment_u64(params.input.value, params.input.value_blind) != + params.output.value_commit + { + msg!("[Mint] Error: Value commitments do not match"); + return Err(ContractError::Custom(10)) + } + + if pedersen_commitment_base(params.input.token_id.inner(), params.input.token_blind) != + params.output.token_commit + { + msg!("[Mint] Error: Token commitments do not match"); + return Err(ContractError::Custom(11)) + } + + // Create a state update. We only need the new coin. + let update = MoneyMintUpdate { coin: Coin::from(params.output.coin) }; + let mut update_data = vec![]; + update_data.write_u8(MoneyFunction::Mint as u8)?; + update.encode(&mut update_data)?; + + Ok(update_data) +} + +pub fn money_mint_process_update( + cid: ContractId, + update: MoneyMintUpdate, +) -> Result<(), ContractError> { + let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?; + let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; + + let coins = vec![MerkleNode::from(update.coin.inner())]; + + msg!("[Mint] Adding new coin to Merkle tree"); + merkle_add(info_db, coin_roots_db, &serialize(&MONEY_CONTRACT_COIN_MERKLE_TREE), &coins)?; + + Ok(()) +} diff --git a/src/contract/money/src/model.rs b/src/contract/money/src/model.rs index 30c5bd42f..6e178eb23 100644 --- a/src/contract/money/src/model.rs +++ b/src/contract/money/src/model.rs @@ -21,6 +21,17 @@ use darkfi_sdk::crypto::{ }; use darkfi_serial::{SerialDecodable, SerialEncodable}; +#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] +pub struct MoneyMintParams { + pub input: ClearInput, + pub output: Output, +} + +#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] +pub struct MoneyMintUpdate { + pub coin: Coin, +} + /// Inputs and outputs for staking coins #[derive(Clone, Debug, SerialEncodable, SerialDecodable)] pub struct MoneyStakeParams { diff --git a/src/contract/money/src/swap.rs b/src/contract/money/src/swap.rs new file mode 100644 index 000000000..4998bf398 --- /dev/null +++ b/src/contract/money/src/swap.rs @@ -0,0 +1,150 @@ +/* This file is part of DarkFi (https://dark.fi) + * + * Copyright (C) 2020-2023 Dyne.org foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use darkfi_sdk::{ + crypto::{Coin, ContractId}, + db::{db_contains_key, db_lookup}, + error::ContractError, + msg, + pasta::pallas, + ContractCall, +}; +use darkfi_serial::{deserialize, serialize, Encodable, WriteExt}; + +use crate::{ + money_transfer_get_metadata, money_transfer_process_update, MoneyFunction, MoneyTransferParams, + MoneyTransferUpdate, MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_NULLIFIERS_TREE, +}; + +pub fn money_otcswap_get_metadata( + cid: ContractId, + call_idx: u32, + calls: Vec, +) -> Result, ContractError> { + Ok(money_transfer_get_metadata(cid, call_idx, calls)?) +} + +pub fn money_otcswap_process_instruction( + cid: ContractId, + call_idx: u32, + calls: Vec, +) -> Result, ContractError> { + let self_ = &calls[call_idx as usize]; + let params: MoneyTransferParams = deserialize(&self_.data[1..])?; + + // State transition for OTC atomic swaps. + // We enforce 2 inputs and 2 outputs so every atomic swap looks the same. + if !params.clear_inputs.is_empty() { + msg!("[OtcSwap] Error: Clear inputs are not empty"); + return Err(ContractError::Custom(12)) + } + + if params.inputs.len() != 2 { + msg!("[OtcSwap] Error: Expected 2 inputs"); + return Err(ContractError::Custom(13)) + } + + if params.outputs.len() != 2 { + msg!("[OtcSwap] Error: Expected 2 outputs"); + return Err(ContractError::Custom(14)) + } + + let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?; + let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; + + let mut new_nullifiers = Vec::with_capacity(2); + + // inputs[0] is being swapped to outputs[1] + // inputs[1] is being swapped to outputs[0] + // So that's how we check the value and token commitments + if params.inputs[0].value_commit != params.outputs[1].value_commit { + msg!("[OtcSwap] Error: Value commitments for input 0 and output 1 do not match"); + return Err(ContractError::Custom(10)) + } + + if params.inputs[1].value_commit != params.outputs[0].value_commit { + msg!("[OtcSwap] Error: Value commitments for input 1 and output 0 do not match"); + return Err(ContractError::Custom(10)) + } + + if params.inputs[0].token_commit != params.outputs[1].token_commit { + msg!("[OtcSwap] Error: Token commitments for input 0 and output 1 do not match"); + return Err(ContractError::Custom(11)) + } + + if params.inputs[1].token_commit != params.outputs[0].token_commit { + msg!("[OtcSwap] Error: Token commitments for input 1 and output 0 do not match"); + return Err(ContractError::Custom(11)) + } + + msg!("[OtcSwap] Iternating over anonymous inputs"); + for (i, input) in params.inputs.iter().enumerate() { + // For now, make sure that the inputs' spend hooks are zero. + // This should however be allowed to some extent. + if input.spend_hook != pallas::Base::zero() { + msg!("[OtcSwap] Error: Unable to swap coins with spend_hook != 0 (input {})", i); + return Err(ContractError::Custom(17)) + } + + // The Merkle root is used to know whether this coin existed + // in a previous state. + if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? { + msg!("[OtcSwap] Error: Merkle root not found in previous state (input {})", i); + return Err(ContractError::Custom(5)) + } + + // The nullifiers should not already exist. It is the double-spend protection. + if new_nullifiers.contains(&input.nullifier) || + db_contains_key(nullifiers_db, &serialize(&input.nullifier))? + { + msg!("[OtcSwap] Error: Duplicate nullifier found in input {}", i); + return Err(ContractError::Custom(6)) + } + + new_nullifiers.push(input.nullifier); + } + + // Newly created coins for this transaction are in the outputs. + let mut new_coins = Vec::with_capacity(2); + for (i, output) in params.outputs.iter().enumerate() { + // TODO: Coins should exist in a sled tree in order to check dupes. + if new_coins.contains(&Coin::from(output.coin)) { + msg!("[OtcSwap] Error: Duplicate coin found in output {}", i); + return Err(ContractError::Custom(9)) + } + + new_coins.push(Coin::from(output.coin)); + } + + // Create a state update. We also use the `MoneyTransferUpdate` because + // they're essentially the same thing, just with a different transition + // rule set. + let update = MoneyTransferUpdate { nullifiers: new_nullifiers, coins: new_coins }; + let mut update_data = vec![]; + update_data.write_u8(MoneyFunction::OtcSwap as u8)?; + update.encode(&mut update_data)?; + + Ok(update_data) +} + +pub fn money_otcswap_process_update( + cid: ContractId, + update: MoneyTransferUpdate, +) -> Result<(), ContractError> { + Ok(money_transfer_process_update(cid, update)?) +} diff --git a/src/contract/money/src/transfer.rs b/src/contract/money/src/transfer.rs new file mode 100644 index 000000000..523889778 --- /dev/null +++ b/src/contract/money/src/transfer.rs @@ -0,0 +1,251 @@ +/* This file is part of DarkFi (https://dark.fi) + * + * Copyright (C) 2020-2023 Dyne.org foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use darkfi_sdk::{ + crypto::{ + pasta_prelude::*, pedersen_commitment_base, pedersen_commitment_u64, Coin, ContractId, + MerkleNode, PublicKey, DARK_TOKEN_ID, + }, + db::{db_contains_key, db_get, db_lookup, db_set}, + error::ContractError, + merkle_add, msg, + pasta::pallas, + ContractCall, +}; +use darkfi_serial::{deserialize, serialize, Encodable, WriteExt}; + +use crate::{ + MoneyFunction, MoneyTransferParams, MoneyTransferUpdate, MONEY_CONTRACT_COIN_MERKLE_TREE, + MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_FAUCET_PUBKEYS, MONEY_CONTRACT_INFO_TREE, + MONEY_CONTRACT_NULLIFIERS_TREE, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1, +}; + +pub fn money_transfer_get_metadata( + _cid: ContractId, + call_idx: u32, + calls: Vec, +) -> Result, ContractError> { + let self_ = &calls[call_idx as usize]; + let params: MoneyTransferParams = deserialize(&self_.data[1..])?; + + let mut zk_public_values: Vec<(String, Vec)> = vec![]; + let mut signature_pubkeys: Vec = vec![]; + + for input in ¶ms.clear_inputs { + signature_pubkeys.push(input.signature_public); + } + + for input in ¶ms.inputs { + let value_coords = input.value_commit.to_affine().coordinates().unwrap(); + let token_coords = input.token_commit.to_affine().coordinates().unwrap(); + let (sig_x, sig_y) = input.signature_public.xy(); + + zk_public_values.push(( + MONEY_CONTRACT_ZKAS_BURN_NS_V1.to_string(), + vec![ + input.nullifier.inner(), + *value_coords.x(), + *value_coords.y(), + *token_coords.x(), + *token_coords.y(), + input.merkle_root.inner(), + input.user_data_enc, + sig_x, + sig_y, + ], + )); + + signature_pubkeys.push(input.signature_public); + } + + for output in ¶ms.outputs { + let value_coords = output.value_commit.to_affine().coordinates().unwrap(); + let token_coords = output.token_commit.to_affine().coordinates().unwrap(); + + zk_public_values.push(( + MONEY_CONTRACT_ZKAS_MINT_NS_V1.to_string(), + vec![ + output.coin, + *value_coords.x(), + *value_coords.y(), + *token_coords.x(), + *token_coords.y(), + ], + )); + } + + let mut metadata = vec![]; + zk_public_values.encode(&mut metadata)?; + signature_pubkeys.encode(&mut metadata)?; + + Ok(metadata) +} + +pub fn money_transfer_process_instruction( + cid: ContractId, + call_idx: u32, + calls: Vec, +) -> Result, ContractError> { + let self_ = &calls[call_idx as usize]; + let params: MoneyTransferParams = deserialize(&self_.data[1..])?; + + if params.clear_inputs.len() + params.inputs.len() < 1 { + msg!("[Transfer] Error: No inputs in the call"); + return Err(ContractError::Custom(1)) + } + + if params.outputs.is_empty() { + msg!("[Transfer] Error: No outputs in the call"); + return Err(ContractError::Custom(2)) + } + + let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?; + let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?; + let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; + + let Some(faucet_pubkeys) = db_get(info_db, &serialize(&MONEY_CONTRACT_FAUCET_PUBKEYS))? else { + msg!("[Transfer] Error: Missing faucet pubkeys from info db"); + return Err(ContractError::Internal) + }; + let faucet_pubkeys: Vec = deserialize(&faucet_pubkeys)?; + + // Accumulator for the value commitments + let mut valcom_total = pallas::Point::identity(); + + // State transition for payments + msg!("[Transfer] Iterating over clear inputs"); + for (i, input) in params.clear_inputs.iter().enumerate() { + if input.token_id != *DARK_TOKEN_ID { + msg!("[Transfer] Error: Clear input {} used non-native token", i); + return Err(ContractError::Custom(3)) + } + + if !faucet_pubkeys.contains(&input.signature_public) { + msg!("[Transfer] Error: Clear input {} used unauthorised pubkey", i); + return Err(ContractError::Custom(4)) + } + + valcom_total += pedersen_commitment_u64(input.value, input.value_blind); + } + + let mut new_nullifiers = Vec::with_capacity(params.inputs.len()); + msg!("[Transfer] Iterating over anonymous inputs"); + for (i, input) in params.inputs.iter().enumerate() { + // The Merkle root is used to know whether this is a coin that + // existed in a previous state. + if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? { + msg!("[Transfer] Error: Merkle root not found in previous state (input {})", i); + return Err(ContractError::Custom(5)) + } + + // The nullifiers should not already exist. It is the double-spend protection. + if new_nullifiers.contains(&input.nullifier) || + db_contains_key(nullifiers_db, &serialize(&input.nullifier))? + { + msg!("[Transfer] Error: Duplicate nullifier found in input {}", i); + return Err(ContractError::Custom(6)) + } + + // Check the invoked contract if spend hook is set + if input.spend_hook != pallas::Base::zero() { + let next_call_idx = call_idx + 1; + if next_call_idx >= calls.len() as u32 { + msg!( + "[Transfer] Error: next_call_idx={} but len(calls)={} (input {})", + next_call_idx, + calls.len(), + i + ); + return Err(ContractError::Custom(7)) + } + + let next = &calls[next_call_idx as usize]; + if next.contract_id.inner() != input.spend_hook { + msg!("[Transfer] Error: Invoking contract call does not match spend hook in input {}", i); + return Err(ContractError::Custom(8)) + } + } + + new_nullifiers.push(input.nullifier); + valcom_total += input.value_commit; + } + + // Newly created coins for this transaction are in the outputs. + let mut new_coins = Vec::with_capacity(params.outputs.len()); + for (i, output) in params.outputs.iter().enumerate() { + // TODO: Coins should exist in a sled tree in order to check dupes. + if new_coins.contains(&Coin::from(output.coin)) { + msg!("[Transfer] Error: Duplicate coin found in output {}", i); + return Err(ContractError::Custom(9)) + } + + // FIXME: Needs some work on types and their place within all these libraries + new_coins.push(Coin::from(output.coin)); + valcom_total -= output.value_commit; + } + + // If the accumulator is not back in its initial state, there's a value mismatch. + if valcom_total != pallas::Point::identity() { + msg!("[Transfer] Error: Value commitments do not result in identity"); + return Err(ContractError::Custom(10)) + } + + // Verify that the token commitments are all for the same token + let tokcom = params.outputs[0].token_commit; + let mut failed_tokcom = params.inputs.iter().any(|input| input.token_commit != tokcom); + failed_tokcom = + failed_tokcom || params.outputs.iter().any(|output| output.token_commit != tokcom); + failed_tokcom = failed_tokcom || + params.clear_inputs.iter().any(|input| { + pedersen_commitment_base(input.token_id.inner(), input.token_blind) != tokcom + }); + + if failed_tokcom { + msg!("[Transfer] Error: Token commitments do not match"); + return Err(ContractError::Custom(11)) + } + + // Create a state update + let update = MoneyTransferUpdate { nullifiers: new_nullifiers, coins: new_coins }; + let mut update_data = vec![]; + update_data.write_u8(MoneyFunction::Transfer as u8)?; + update.encode(&mut update_data)?; + + Ok(update_data) +} + +pub fn money_transfer_process_update( + cid: ContractId, + update: MoneyTransferUpdate, +) -> Result<(), ContractError> { + let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?; + let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?; + let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; + + msg!("[Transfer] Adding new nullifiers to the set"); + for nullifier in update.nullifiers { + db_set(nullifiers_db, &serialize(&nullifier), &[])?; + } + + let coins: Vec<_> = update.coins.iter().map(|x| MerkleNode::from(x.inner())).collect(); + + msg!("[Transfer] Adding new coins to Merkle tree"); + merkle_add(info_db, coin_roots_db, &serialize(&MONEY_CONTRACT_COIN_MERKLE_TREE), &coins)?; + + Ok(()) +}