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(())
+}