diff --git a/src/contract/money/Cargo.toml b/src/contract/money/Cargo.toml index 0a5582344..564563a80 100644 --- a/src/contract/money/Cargo.toml +++ b/src/contract/money/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] darkfi-sdk = { path = "../../sdk" } darkfi-serial = { path = "../../serial", features = ["derive", "crypto"] } +thiserror = "1.0.38" # The following dependencies are used for the client API and # probably shouldn't be in WASM diff --git a/src/contract/money/proof/token_freeze_v1.zk b/src/contract/money/proof/token_freeze_v1.zk index 679e284fa..95aed526f 100644 --- a/src/contract/money/proof/token_freeze_v1.zk +++ b/src/contract/money/proof/token_freeze_v1.zk @@ -1,16 +1,10 @@ constant "TokenFreeze_V1" { - EcFixedPointShort VALUE_COMMIT_VALUE, - EcFixedPoint VALUE_COMMIT_RANDOM, EcFixedPointBase NULLIFIER_K, } contract "TokenFreeze_V1" { # Token mint authority secret Base mint_authority, - # Leaf position in the Merkle tree of tokens - Uint32 leaf_pos, - # Merkle authentication path - MerklePath path, } circuit "TokenFreeze_V1" { @@ -24,8 +18,4 @@ circuit "TokenFreeze_V1" { # Derive the token ID token_id = poseidon_hash(mint_x, mint_y); constrain_instance(token_id); - - # Prove that this token was minted first - root = merkle_root(leaf_pos, path, token_id); - constrain_instance(root); } diff --git a/src/contract/money/src/client.rs b/src/contract/money/src/client.rs deleted file mode 100644 index 78dd7baf8..000000000 --- a/src/contract/money/src/client.rs +++ /dev/null @@ -1,1335 +0,0 @@ -/* 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 module implements the client-side of this contract's interaction. -//! What we basically do here is implement an API that creates the necessary -//! structures and is able to export them to create a DarkFi Transaction -//! object that can be broadcasted to the network when we want to make a -//! payment with some coins in our wallet. -//! Note that this API doesn't involve any wallet interaction, but only -//! takes the necessary objects provided by the caller. This is so we can -//! abstract away the wallet interface to client implementations. - -use chacha20poly1305::{AeadInPlace, ChaCha20Poly1305, KeyInit}; -use darkfi::{ - consensus::LeadCoin, - zk::{Proof, ProvingKey, Witness, ZkCircuit}, - zkas::ZkBinary, - ClientFailed, Error, Result, -}; -use darkfi_sdk::crypto::{ - diffie_hellman::{kdf_sapling, sapling_ka_agree}, - merkle_prelude::*, - pallas, - pasta_prelude::*, - pedersen_commitment_base, pedersen_commitment_u64, poseidon_hash, Keypair, MerkleNode, - MerklePosition, MerkleTree, Nullifier, PublicKey, SecretKey, TokenId, ValueBlind, ValueCommit, -}; -use darkfi_serial::{serialize, Decodable, Encodable, SerialDecodable, SerialEncodable}; -use halo2_proofs::circuit::Value; -use log::{debug, error, info}; -use rand::rngs::OsRng; - -use crate::model::{ - ClearInput, Input, MoneyStakeParams, MoneyTransferParams, MoneyUnstakeParams, Output, - 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. -pub const MONEY_INFO_TABLE: &str = "money_info"; -pub const MONEY_INFO_COL_LAST_SCANNED_SLOT: &str = "last_scanned_slot"; - -pub const MONEY_TREE_TABLE: &str = "money_tree"; -pub const MONEY_TREE_COL_TREE: &str = "tree"; - -pub const MONEY_KEYS_TABLE: &str = "money_keys"; -pub const MONEY_KEYS_COL_KEY_ID: &str = "key_id"; -pub const MONEY_KEYS_COL_IS_DEFAULT: &str = "is_default"; -pub const MONEY_KEYS_COL_PUBLIC: &str = "public"; -pub const MONEY_KEYS_COL_SECRET: &str = "secret"; - -pub const MONEY_COINS_TABLE: &str = "money_coins"; -pub const MONEY_COINS_COL_COIN: &str = "coin"; -pub const MONEY_COINS_COL_IS_SPENT: &str = "is_spent"; -pub const MONEY_COINS_COL_SERIAL: &str = "serial"; -pub const MONEY_COINS_COL_VALUE: &str = "value"; -pub const MONEY_COINS_COL_TOKEN_ID: &str = "token_id"; -pub const MONEY_COINS_COL_SPEND_HOOK: &str = "spend_hook"; -pub const MONEY_COINS_COL_USER_DATA: &str = "user_data"; -pub const MONEY_COINS_COL_COIN_BLIND: &str = "coin_blind"; -pub const MONEY_COINS_COL_VALUE_BLIND: &str = "value_blind"; -pub const MONEY_COINS_COL_TOKEN_BLIND: &str = "token_blind"; -pub const MONEY_COINS_COL_SECRET: &str = "secret"; -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"; - -/// Byte length of the AEAD tag of the chacha20 cipher used for note encryption -pub const AEAD_TAG_SIZE: usize = 16; - -/// The `Coin` is represented as a base field element. -#[derive(Debug, Clone, Copy, Eq, PartialEq, SerialEncodable, SerialDecodable)] -pub struct Coin(pub pallas::Base); - -impl Coin { - /// Reference the raw inner base field element - pub fn inner(&self) -> pallas::Base { - self.0 - } - - /// Try to create a `Coin` type from the given 32 bytes. - /// Returns an error if the bytes don't fit in the base field. - pub fn from_bytes(bytes: [u8; 32]) -> Result { - match pallas::Base::from_repr(bytes).into() { - Some(v) => Ok(Self(v)), - None => Err(Error::CoinFromBytes), - } - } -} - -impl From for Coin { - fn from(x: pallas::Base) -> Self { - Self(x) - } -} - -/// The `OwnCoin` is a representation of `Coin` with its respective metadata. -#[derive(Debug, Clone, Eq, PartialEq, SerialEncodable, SerialDecodable)] -pub struct OwnCoin { - /// The coin hash - pub coin: Coin, - /// The attached Note - pub note: Note, - /// Coin's secret key - pub secret: SecretKey, - /// Coin's nullifier, - pub nullifier: Nullifier, - /// Coin's leaf position in the Merkle tree of coins - pub leaf_position: MerklePosition, -} - -/// The `Note` holds the inner attributes of a `Coin` -#[derive(Debug, Clone, Eq, PartialEq, SerialEncodable, SerialDecodable)] -pub struct Note { - /// Serial number of the coin, used for the nullifier - pub serial: pallas::Base, - /// Value of the coin - pub value: u64, - /// Token ID of the coin - pub token_id: TokenId, - /// Spend hook used for protocol owned liquidity. - /// Specifies which contract owns this coin. - pub spend_hook: pallas::Base, - /// User data used by protocol when spend hook is enabled. - pub user_data: pallas::Base, - /// Blinding factor for the coin bulla - pub coin_blind: pallas::Base, - /// Blinding factor for the value pedersen commitment - pub value_blind: ValueBlind, - /// Blinding factor for the token ID pedersen commitment - pub token_blind: ValueBlind, - /// Attached memo (arbitrary data) - pub memo: Vec, -} - -impl Note { - /// Encrypt the note to some given `PublicKey` using an AEAD cipher. - pub fn encrypt(&self, public_key: &PublicKey) -> Result { - let ephem_keypair = Keypair::random(&mut OsRng); - let shared_secret = sapling_ka_agree(&ephem_keypair.secret, public_key); - let key = kdf_sapling(&shared_secret, &ephem_keypair.public); - - let mut input = vec![]; - self.encode(&mut input)?; - let input_len = input.len(); - - let mut ciphertext = vec![0_u8; input_len + AEAD_TAG_SIZE]; - ciphertext[..input_len].copy_from_slice(&input); - - ChaCha20Poly1305::new(key.as_ref().into()) - .encrypt_in_place([0u8; 12][..].into(), &[], &mut ciphertext) - .unwrap(); - - Ok(EncryptedNote { ciphertext, ephem_public: ephem_keypair.public }) - } -} - -/// The `EncryptedNote` represents a structure holding the ciphertext (which is -/// an encryption of the `Note` object, and the ephemeral `PublicKey` created at -/// the time when the encryption was done -#[derive(Debug, Clone, Eq, PartialEq, SerialEncodable, SerialDecodable)] -pub struct EncryptedNote { - /// Ciphertext of the encrypted `Note` - pub ciphertext: Vec, - /// Ephemeral public key created at the time of encrypting the note - pub ephem_public: PublicKey, -} - -impl EncryptedNote { - /// Attempt to decrypt an `EncryptedNote` given a secret key. - pub fn decrypt(&self, secret: &SecretKey) -> Result { - let shared_secret = sapling_ka_agree(secret, &self.ephem_public); - let key = kdf_sapling(&shared_secret, &self.ephem_public); - - let ciphertext_len = self.ciphertext.len(); - let mut plaintext = vec![0_u8; ciphertext_len]; - plaintext.copy_from_slice(&self.ciphertext); - - match ChaCha20Poly1305::new(key.as_ref().into()).decrypt_in_place( - [0u8; 12][..].into(), - &[], - &mut plaintext, - ) { - Ok(()) => Ok(Note::decode(&plaintext[..ciphertext_len - AEAD_TAG_SIZE])?), - Err(e) => Err(Error::NoteDecryptionFailed(e.to_string())), - } - } -} - -// TODO: we can put all these in an internal module like: -// money_transfer::builder::ClearInputInfo - -struct TransactionBuilderClearInputInfo { - pub value: u64, - pub token_id: TokenId, - pub signature_secret: SecretKey, -} - -struct TransactionBuilderInputInfo { - pub leaf_position: MerklePosition, - pub merkle_path: Vec, - pub secret: SecretKey, - pub note: Note, -} - -struct TransactionBuilderOutputInfo { - pub value: u64, - pub token_id: TokenId, - pub public_key: PublicKey, -} - -pub struct TransferBurnRevealed { - pub value_commit: ValueCommit, - pub token_commit: ValueCommit, - pub nullifier: Nullifier, - pub merkle_root: MerkleNode, - pub spend_hook: pallas::Base, - pub user_data_enc: pallas::Base, - pub signature_public: PublicKey, -} - -impl TransferBurnRevealed { - #[allow(clippy::too_many_arguments)] - pub fn compute( - value: u64, - token_id: TokenId, - value_blind: ValueBlind, - token_blind: ValueBlind, - serial: pallas::Base, - spend_hook: pallas::Base, - user_data: pallas::Base, - user_data_blind: pallas::Base, - coin_blind: pallas::Base, - secret_key: SecretKey, - leaf_position: MerklePosition, - merkle_path: Vec, - signature_secret: SecretKey, - ) -> Self { - let nullifier = Nullifier::from(poseidon_hash([secret_key.inner(), serial])); - - let public_key = PublicKey::from_secret(secret_key); - let (pub_x, pub_y) = public_key.xy(); - - let coin = poseidon_hash([ - pub_x, - pub_y, - pallas::Base::from(value), - token_id.inner(), - serial, - spend_hook, - user_data, - coin_blind, - ]); - - let merkle_root = { - let position: u64 = leaf_position.into(); - let mut current = MerkleNode::from(coin); - for (level, sibling) in merkle_path.iter().enumerate() { - let level = level as u8; - current = if position & (1 << level) == 0 { - MerkleNode::combine(level.into(), ¤t, sibling) - } else { - MerkleNode::combine(level.into(), sibling, ¤t) - }; - } - current - }; - - let user_data_enc = poseidon_hash([user_data, user_data_blind]); - - let value_commit = pedersen_commitment_u64(value, value_blind); - let token_commit = pedersen_commitment_base(token_id.inner(), token_blind); - - Self { - value_commit, - token_commit, - nullifier, - merkle_root, - spend_hook, - user_data_enc, - signature_public: PublicKey::from_secret(signature_secret), - } - } - - pub fn to_vec(&self) -> Vec { - let valcom_coords = self.value_commit.to_affine().coordinates().unwrap(); - let tokcom_coords = self.token_commit.to_affine().coordinates().unwrap(); - let sigpub_coords = self.signature_public.inner().to_affine().coordinates().unwrap(); - - // NOTE: It's important to keep this order the same as the `constrain_instance` - // calls in the zkas code. - vec![ - self.nullifier.inner(), - *valcom_coords.x(), - *valcom_coords.y(), - *tokcom_coords.x(), - *tokcom_coords.y(), - self.merkle_root.inner(), - self.user_data_enc, - *sigpub_coords.x(), - *sigpub_coords.y(), - // TODO: Why is spend_hook in the struct but not here? - ] - } -} - -pub struct TransferMintRevealed { - pub coin: Coin, - pub value_commit: ValueCommit, - pub token_commit: ValueCommit, -} - -impl TransferMintRevealed { - #[allow(clippy::too_many_arguments)] - pub fn compute( - value: u64, - token_id: TokenId, - value_blind: ValueBlind, - token_blind: ValueBlind, - serial: pallas::Base, - spend_hook: pallas::Base, - user_data: pallas::Base, - coin_blind: pallas::Base, - public_key: PublicKey, - ) -> Self { - let value_commit = pedersen_commitment_u64(value, value_blind); - let token_commit = pedersen_commitment_base(token_id.inner(), token_blind); - - let (pub_x, pub_y) = public_key.xy(); - - let coin = Coin::from(poseidon_hash([ - pub_x, - pub_y, - pallas::Base::from(value), - token_id.inner(), - serial, - spend_hook, - user_data, - coin_blind, - ])); - - Self { coin, value_commit, token_commit } - } - - pub fn to_vec(&self) -> Vec { - let valcom_coords = self.value_commit.to_affine().coordinates().unwrap(); - let tokcom_coords = self.token_commit.to_affine().coordinates().unwrap(); - - // NOTE: It's important to keep this order the same as the `constrain_instance` - // calls in the zkas code. - vec![ - self.coin.inner(), - *valcom_coords.x(), - *valcom_coords.y(), - *tokcom_coords.x(), - *tokcom_coords.y(), - ] - } -} - -#[allow(clippy::too_many_arguments)] -pub fn create_transfer_mint_proof( - zkbin: &ZkBinary, - pk: &ProvingKey, - value: u64, - token_id: TokenId, - value_blind: ValueBlind, - token_blind: ValueBlind, - serial: pallas::Base, - spend_hook: pallas::Base, - user_data: pallas::Base, - coin_blind: pallas::Base, - public_key: PublicKey, -) -> Result<(Proof, TransferMintRevealed)> { - let revealed = TransferMintRevealed::compute( - value, - token_id, - value_blind, - token_blind, - serial, - spend_hook, - user_data, - coin_blind, - public_key, - ); - - let (pub_x, pub_y) = public_key.xy(); - - // NOTE: It's important to keep these in the same order as the zkas code. - let prover_witnesses = vec![ - Witness::Base(Value::known(pub_x)), - Witness::Base(Value::known(pub_y)), - Witness::Base(Value::known(pallas::Base::from(value))), - Witness::Base(Value::known(token_id.inner())), - Witness::Base(Value::known(serial)), - Witness::Base(Value::known(coin_blind)), - Witness::Base(Value::known(spend_hook)), - Witness::Base(Value::known(user_data)), - Witness::Scalar(Value::known(value_blind)), - Witness::Scalar(Value::known(token_blind)), - ]; - - let circuit = ZkCircuit::new(prover_witnesses, zkbin.clone()); - let proof = Proof::create(pk, &[circuit], &revealed.to_vec(), &mut OsRng)?; - - Ok((proof, revealed)) -} - -#[allow(clippy::too_many_arguments)] -pub fn create_transfer_burn_proof( - zkbin: &ZkBinary, - pk: &ProvingKey, - value: u64, - token_id: TokenId, - value_blind: ValueBlind, - token_blind: ValueBlind, - serial: pallas::Base, - spend_hook: pallas::Base, - user_data: pallas::Base, - user_data_blind: pallas::Base, - coin_blind: pallas::Base, - secret_key: SecretKey, - leaf_position: MerklePosition, - merkle_path: Vec, - signature_secret: SecretKey, -) -> Result<(Proof, TransferBurnRevealed)> { - let revealed = TransferBurnRevealed::compute( - value, - token_id, - value_blind, - token_blind, - serial, - spend_hook, - user_data, - user_data_blind, - coin_blind, - secret_key, - leaf_position, - merkle_path.clone(), - signature_secret, - ); - - // NOTE: It's important to keep these in the same order as the zkas code. - let prover_witnesses = vec![ - Witness::Base(Value::known(pallas::Base::from(value))), - Witness::Base(Value::known(token_id.inner())), - Witness::Scalar(Value::known(value_blind)), - Witness::Scalar(Value::known(token_blind)), - Witness::Base(Value::known(serial)), - Witness::Base(Value::known(spend_hook)), - Witness::Base(Value::known(user_data)), - Witness::Base(Value::known(user_data_blind)), - Witness::Base(Value::known(coin_blind)), - Witness::Base(Value::known(secret_key.inner())), - Witness::Uint32(Value::known(u64::from(leaf_position).try_into().unwrap())), - Witness::MerklePath(Value::known(merkle_path.try_into().unwrap())), - Witness::Base(Value::known(signature_secret.inner())), - ]; - - let circuit = ZkCircuit::new(prover_witnesses, zkbin.clone()); - let proof = Proof::create(pk, &[circuit], &revealed.to_vec(), &mut OsRng)?; - - Ok((proof, revealed)) -} - -struct StakeLeadMintRevealed { - pub value_commit: ValueCommit, - pub pk: pallas::Base, - pub commitment_x: pallas::Base, - pub commitment_y: pallas::Base, -} - -impl StakeLeadMintRevealed { - pub fn compute( - value: pallas::Base, - pk: pallas::Base, - value_blind: pallas::Scalar, - commitment: pallas::Point, - ) -> Self { - let value_commit = pedersen_commitment_base(value, value_blind); - let coord = commitment.to_affine().coordinates().unwrap(); - Self { value_commit, pk, commitment_x: *coord.x(), commitment_y: *coord.y() } - } - pub fn to_vec(&self) -> Vec { - let value_coord = self.value_commit.to_affine().coordinates().unwrap(); - let value_cm_x = *value_coord.x(); - let value_cm_y = *value_coord.y(); - vec![value_cm_x, value_cm_y, self.pk, self.commitment_x, self.commitment_y] - } -} - -fn create_stake_mint_proof( - zkbin: &ZkBinary, // LeadMint contract binary - pk: &ProvingKey, - public_key: pallas::Base, - coin_commitment: pallas::Point, - value: pallas::Base, - value_blind: ValueBlind, - coin_blind: ValueBlind, - sk: pallas::Base, - sk_root: pallas::Base, - tau: pallas::Base, - nonce: pallas::Base, // rho -) -> Result<(Proof, StakeLeadMintRevealed)> { - let revealed = StakeLeadMintRevealed::compute(value, public_key, value_blind, coin_commitment); - - let prover_witnesses = vec![ - Witness::Base(Value::known(sk)), - Witness::Base(Value::known(sk_root)), - Witness::Base(Value::known(tau)), - Witness::Base(Value::known(nonce)), - Witness::Scalar(Value::known(coin_blind)), - Witness::Base(Value::known(value)), - Witness::Scalar(Value::known(value_blind)), - ]; - let circuit = ZkCircuit::new(prover_witnesses, zkbin.clone()); - let proof = Proof::create(pk, &[circuit], &revealed.to_vec(), &mut OsRng)?; - - Ok((proof, revealed)) -} - -struct UnstakeLeadBurnRevealed { - pub value_commit: ValueCommit, - pub pk: pallas::Base, - pub commitment_x: pallas::Base, - pub commitment_y: pallas::Base, - pub commitment_root: pallas::Base, - pub sk_root: pallas::Base, - pub nullifier: pallas::Base, -} - -impl UnstakeLeadBurnRevealed { - pub fn compute( - value: pallas::Base, - value_blind: ValueBlind, - pk: pallas::Base, - commitment: pallas::Point, - commitment_root: pallas::Base, - sk_root: pallas::Base, - nullifier: pallas::Base, - ) -> Self { - let value_commit = pedersen_commitment_base(value, value_blind); - let coord = commitment.to_affine().coordinates().unwrap(); - let commitment_x = *coord.x(); - let commitment_y = *coord.y(); - Self { value_commit, pk, commitment_x, commitment_y, commitment_root, sk_root, nullifier } - } - - pub fn to_vec(&self) -> Vec { - let coord = self.value_commit.to_affine().coordinates().unwrap(); - let value_cm_x = *coord.x(); - let value_cm_y = *coord.y(); - vec![ - value_cm_x, - value_cm_y, - self.pk, - self.commitment_x, - self.commitment_y, - self.commitment_root, - self.sk_root, - self.nullifier, - ] - } -} - -fn create_unstake_burn_proof( - zkbin: &ZkBinary, - pk: &ProvingKey, - value: pallas::Base, - value_blind: ValueBlind, - coin_blind: ValueBlind, - public_key: pallas::Base, - sk: pallas::Base, - sk_root: pallas::Base, - sk_pos: MerklePosition, - sk_path: Vec, - commitment_merkle_path: Vec, - commitment: pallas::Point, - commitment_root: pallas::Base, - commitment_pos: MerklePosition, - slot: u64, - nonce: pallas::Base, - nullifier: pallas::Base, -) -> Result<(Proof, UnstakeLeadBurnRevealed)> { - let revealed = UnstakeLeadBurnRevealed::compute( - value, - value_blind, - public_key, - commitment, - commitment_root, - sk_root, - nullifier, - ); - - let prover_witnesses = vec![ - Witness::MerklePath(Value::known(commitment_merkle_path.try_into().unwrap())), - Witness::Uint32(Value::known(u64::from(commitment_pos).try_into().unwrap())), // u32 - Witness::Uint32(Value::known(u64::from(sk_pos).try_into().unwrap())), // u32 - Witness::Base(Value::known(sk)), - Witness::Base(Value::known(sk_root)), - Witness::MerklePath(Value::known(sk_path.try_into().unwrap())), - Witness::Base(Value::known(pallas::Base::from(slot))), - Witness::Base(Value::known(nonce)), - Witness::Scalar(Value::known(coin_blind)), - Witness::Base(Value::known(value)), - Witness::Scalar(Value::known(value_blind)), - ]; - let circuit = ZkCircuit::new(prover_witnesses, zkbin.clone()); - let proof = Proof::create(pk, &[circuit], &revealed.to_vec(), &mut OsRng)?; - - Ok((proof, revealed)) -} - -/// Build half of the money contract OTC swap transaction parameters with the given data: -/// * `value_send` - Amount to send -/// * `token_id_send` - Token ID to send -/// * `value_recv` - Amount to receive -/// * `token_id_recv` - Token ID to receive -/// * `value_blinds` - Value blinds to use if we're the second half -/// * `token_blinds` - Token blinds to use if we're the second half -/// * `coins` - Set of coins we're able to spend -/// * `tree` - Current Merkle tree of coins -/// * `mint_zkbin` - ZkBinary of the mint circuit -/// * `mint_pk` - Proving key for the ZK mint proof -/// * `burn_zkbin` - ZkBinary of the burn circuit -/// * `burn_pk` - Proving key for the ZK burn proof -#[allow(clippy::too_many_arguments)] -#[allow(clippy::type_complexity)] -pub fn build_half_swap_tx( - pubkey: &PublicKey, - value_send: u64, - token_id_send: TokenId, - value_recv: u64, - token_id_recv: TokenId, - value_blinds: &[ValueBlind], - token_blinds: &[ValueBlind], - coins: &[OwnCoin], - tree: &MerkleTree, - mint_zkbin: &ZkBinary, - mint_pk: &ProvingKey, - burn_zkbin: &ZkBinary, - burn_pk: &ProvingKey, -) -> Result<( - MoneyTransferParams, - Vec, - Vec, - Vec, - Vec, - Vec, -)> { - debug!(target: "money", "Building OTC swap transaction half"); - assert!(value_send != 0); - assert!(value_recv != 0); - assert!(!coins.is_empty()); - - debug!(target: "money", "Money::build_half_swap_tx(): Building anonymous inputs"); - // We'll take any coin that has correct value - let Some(coin) = coins.iter().find(|x| x.note.value == value_send && x.note.token_id == token_id_send) else { - error!(target: "money", "Money::build_half_swap_tx(): Did not find a coin with enough value to swap"); - return Err(ClientFailed::NotEnoughValue(value_send).into()) - }; - - let leaf_position = coin.leaf_position; - let root = tree.root(0).unwrap(); - let merkle_path = tree.authentication_path(leaf_position, &root).unwrap(); - - let input = TransactionBuilderInputInfo { - leaf_position, - merkle_path, - secret: coin.secret, - note: coin.note.clone(), - }; - - let spent_coins = vec![coin.clone()]; - - let output = TransactionBuilderOutputInfo { - value: value_recv, - token_id: token_id_recv, - public_key: *pubkey, - }; - - // We now fill this with necessary stuff - let mut params = MoneyTransferParams { clear_inputs: vec![], inputs: vec![], outputs: vec![] }; - - let val_blinds: Vec; - let tok_blinds: Vec; - - // If we got non-empty `value_blinds` passed into this function, we use them here. - // They should be sent to the second party by the swap initiator. - let (value_send_blind, value_recv_blind) = { - if value_blinds.is_empty() { - let value_send_blind = ValueBlind::random(&mut OsRng); - let value_recv_blind = ValueBlind::random(&mut OsRng); - val_blinds = vec![value_send_blind, value_recv_blind]; - (value_send_blind, value_recv_blind) - } else { - val_blinds = vec![value_blinds[1], value_blinds[0]]; - (value_blinds[1], value_blinds[0]) - } - }; - - // The same goes for token blinds - let (token_send_blind, token_recv_blind) = { - if token_blinds.is_empty() { - let token_send_blind = ValueBlind::random(&mut OsRng); - let token_recv_blind = ValueBlind::random(&mut OsRng); - tok_blinds = vec![token_send_blind, token_recv_blind]; - (token_send_blind, token_recv_blind) - } else { - tok_blinds = vec![token_blinds[1], token_blinds[0]]; - (token_blinds[1], token_blinds[0]) - } - }; - - // The ephemeral secret key we're using here. - let signature_secret = SecretKey::random(&mut OsRng); - - // Disable composability for this old obsolete API - let spend_hook = pallas::Base::zero(); - let user_data = pallas::Base::zero(); - let user_data_blind = pallas::Base::random(&mut OsRng); - - let mut zk_proofs = vec![]; - - info!(target: "money", "Creating swap burn proof for input 0"); - let (proof, revealed) = create_transfer_burn_proof( - burn_zkbin, - burn_pk, - input.note.value, - input.note.token_id, - value_send_blind, - token_send_blind, - input.note.serial, - spend_hook, - user_data, - user_data_blind, - input.note.coin_blind, - input.secret, - input.leaf_position, - input.merkle_path, - signature_secret, - )?; - - params.inputs.push(Input { - value_commit: revealed.value_commit, - token_commit: revealed.token_commit, - nullifier: revealed.nullifier, - merkle_root: revealed.merkle_root, - spend_hook: revealed.spend_hook, - user_data_enc: revealed.user_data_enc, - signature_public: revealed.signature_public, - }); - - zk_proofs.push(proof); - - let serial = pallas::Base::random(&mut OsRng); - let coin_blind = pallas::Base::random(&mut OsRng); - - // Disable composability for this old obsolete API - let spend_hook = pallas::Base::zero(); - let user_data = pallas::Base::zero(); - - info!(target: "money", "Creating swap mint proof for output 0"); - let (proof, revealed) = create_transfer_mint_proof( - mint_zkbin, - mint_pk, - output.value, - output.token_id, - value_recv_blind, - token_recv_blind, - serial, - spend_hook, - user_data, - coin_blind, - output.public_key, - )?; - - zk_proofs.push(proof); - - // Encrypted note - let note = Note { - serial, - value: output.value, - token_id: output.token_id, - spend_hook: pallas::Base::zero(), - user_data: pallas::Base::zero(), - coin_blind, - value_blind: value_recv_blind, - token_blind: token_recv_blind, - // Here we store our secret key we use for signing - memo: serialize(&signature_secret), - }; - - let encrypted_note = note.encrypt(&output.public_key)?; - - params.outputs.push(Output { - value_commit: revealed.value_commit, - token_commit: revealed.token_commit, - coin: revealed.coin.inner(), - ciphertext: encrypted_note.ciphertext, - ephem_public: encrypted_note.ephem_public, - }); - - // Now we should have all the params, zk proofs, and signature secrets. - // We return it all and let the caller deal with it. - Ok((params, zk_proofs, vec![signature_secret], spent_coins, val_blinds, tok_blinds)) -} - -/// Build money contract transfer transaction parameters with the given data: -/// * `keypair` - Caller's keypair -/// * `pubkey` - Public key of the recipient -/// * `value` - Value of the transfer -/// * `token_id` - Token ID to transfer -/// * `spend_hook` - Spend hook -/// * `user_data` - User data -/// * `user_data_blind` - Blinding for user data -/// * `coins` - Set of coins we're able to spend -/// * `tree` - Current Merkle tree of coins -/// * `mint_zkbin` - ZkBinary of the mint circuit -/// * `mint_pk` - Proving key for the ZK mint proof -/// * `burn_zkbin` - ZkBinary of the burn circuit -/// * `burn_pk` - Proving key for the ZK burn proof -/// * `clear_input` - Marks if we're creating clear or anonymous inputs -#[allow(clippy::too_many_arguments)] -#[allow(clippy::type_complexity)] -pub fn build_transfer_tx( - keypair: &Keypair, - pubkey: &PublicKey, - value: u64, - token_id: TokenId, - spend_hook: pallas::Base, - user_data: pallas::Base, - user_data_blind: pallas::Base, - coins: &[OwnCoin], - tree: &MerkleTree, - mint_zkbin: &ZkBinary, - mint_pk: &ProvingKey, - burn_zkbin: &ZkBinary, - burn_pk: &ProvingKey, - clear_input: bool, -) -> Result<(MoneyTransferParams, Vec, Vec, Vec)> { - debug!(target: "money", "Building money contract transfer transaction"); - assert!(value != 0); - if !clear_input { - assert!(!coins.is_empty()); - } - // Ensure the coins given to us are all of the same token_id. - // The money contract base transfer doesn't allow conversions. - for coin in coins.iter() { - assert_eq!(token_id, coin.note.token_id); - } - - let mut clear_inputs = vec![]; - let mut inputs = vec![]; - let mut outputs = vec![]; - let mut change_outputs = vec![]; - let mut spent_coins = vec![]; - - if clear_input { - debug!(target: "money", "Money::build_transfer_tx(): Building clear input"); - let input = - TransactionBuilderClearInputInfo { value, token_id, signature_secret: keypair.secret }; - clear_inputs.push(input); - } else { - debug!(target: "money", "Money::build_transfer_tx(): Building anonymous inputs"); - let mut inputs_value = 0; - for coin in coins.iter() { - if inputs_value >= value { - debug!(target: "money", "inputs_value >= value"); - break - } - - let leaf_position = coin.leaf_position; - let root = tree.root(0).unwrap(); - let merkle_path = tree.authentication_path(leaf_position, &root).unwrap(); - inputs_value += coin.note.value; - - let input = TransactionBuilderInputInfo { - leaf_position, - merkle_path, - secret: coin.secret, - note: coin.note.clone(), - }; - - inputs.push(input); - spent_coins.push(coin.clone()); - } - - if inputs_value < value { - error!(target: "money", "Money::build_transfer_tx(): Not enough value to build tx inputs"); - return Err(ClientFailed::NotEnoughValue(inputs_value).into()) - } - - if inputs_value > value { - let return_value = inputs_value - value; - change_outputs.push(TransactionBuilderOutputInfo { - value: return_value, - token_id, - public_key: keypair.public, - }); - } - - debug!(target: "money", "Money::build_transfer_tx(): Finished building inputs"); - } - - outputs.push(TransactionBuilderOutputInfo { value, token_id, public_key: *pubkey }); - assert!(clear_inputs.len() + inputs.len() > 0); - - // We now fill this with necessary stuff - let mut params = MoneyTransferParams { clear_inputs: vec![], inputs: vec![], outputs: vec![] }; - - // I assumed this vec will contain a secret key for each clear input and anonymous input. - let mut signature_secrets = vec![]; - - let token_blind = ValueBlind::random(&mut OsRng); - for input in clear_inputs { - // TODO: FIXME: What to do with this signature secret? - let signature_public = PublicKey::from_secret(input.signature_secret); - signature_secrets.push(input.signature_secret); - let value_blind = ValueBlind::random(&mut OsRng); - - params.clear_inputs.push(ClearInput { - value: input.value, - token_id: input.token_id, - value_blind, - token_blind, - signature_public, - }); - } - - let mut input_blinds = vec![]; - let mut output_blinds = vec![]; - let mut zk_proofs = vec![]; - - for (i, input) in inputs.iter().enumerate() { - let value_blind = ValueBlind::random(&mut OsRng); - input_blinds.push(value_blind); - - let signature_secret = SecretKey::random(&mut OsRng); - signature_secrets.push(signature_secret); - - info!(target: "money", "Creating transfer burn proof for input {}", i); - let (proof, revealed) = create_transfer_burn_proof( - burn_zkbin, - burn_pk, - input.note.value, - input.note.token_id, - value_blind, - token_blind, - input.note.serial, - pallas::Base::zero(), - pallas::Base::zero(), - user_data_blind, // <-- FIXME: This api needs rework to support normal and DAO transfers - input.note.coin_blind, - input.secret, - input.leaf_position, - input.merkle_path.clone(), - signature_secret, - )?; - - params.inputs.push(Input { - value_commit: revealed.value_commit, - token_commit: revealed.token_commit, - nullifier: revealed.nullifier, - merkle_root: revealed.merkle_root, - spend_hook: revealed.spend_hook, - user_data_enc: revealed.user_data_enc, - signature_public: revealed.signature_public, - }); - - zk_proofs.push(proof); - } - - // This value_blind calc assumes there will always be at least a single output - assert!(!outputs.is_empty()); - - for (i, output) in change_outputs.iter().chain(outputs.iter()).enumerate() { - let value_blind = if i == outputs.len() + change_outputs.len() - 1 { - compute_remainder_blind(¶ms.clear_inputs, &input_blinds, &output_blinds) - } else { - ValueBlind::random(&mut OsRng) - }; - - output_blinds.push(value_blind); - - let serial = pallas::Base::random(&mut OsRng); - let coin_blind = pallas::Base::random(&mut OsRng); - - // A hacky way to zeroize spend hooks for the change outputs - let (scoped_spend_hook, scoped_user_data) = { - if i >= change_outputs.len() { - (spend_hook, user_data) - } else { - (pallas::Base::zero(), pallas::Base::zero()) - } - }; - - info!(target: "money", "Creating transfer mint proof for output {}", i); - let (proof, revealed) = create_transfer_mint_proof( - mint_zkbin, - mint_pk, - output.value, - output.token_id, - value_blind, - token_blind, - serial, - scoped_spend_hook, - scoped_user_data, - coin_blind, - output.public_key, - )?; - - zk_proofs.push(proof); - - // Encrypted note - let note = Note { - serial, - value: output.value, - token_id: output.token_id, - spend_hook: scoped_spend_hook, - user_data: scoped_user_data, - coin_blind, - value_blind, - token_blind, - // NOTE: Perhaps pass in memos to this entire function with - // VecDeque and then pop front to add here. - memo: vec![], - }; - - let encrypted_note = note.encrypt(&output.public_key)?; - - params.outputs.push(Output { - value_commit: revealed.value_commit, - token_commit: revealed.token_commit, - coin: revealed.coin.inner(), - ciphertext: encrypted_note.ciphertext, - ephem_public: encrypted_note.ephem_public, - }) - } - - // Now we should have all the params, zk proofs, and signature secrets. - // We return it all and let the caller deal with it. - Ok((params, zk_proofs, signature_secrets, spent_coins)) -} - -pub fn build_stake_tx( - //pubkey: &PublicKey, - coins: &[OwnCoin], - tx_tree: &mut MerkleTree, - cm_tree: &mut MerkleTree, - sk_tree: &mut MerkleTree, - mint_zkbin: &ZkBinary, - mint_pk: &ProvingKey, - burn_zkbin: &ZkBinary, - burn_pk: &ProvingKey, - slot_index: u64, -) -> Result<(MoneyStakeParams, Vec, Vec, Vec, Vec)> { - // convert owncoins to leadcoins. - // TODO: verify this token blind usage - let token_blind = ValueBlind::random(&mut OsRng); - let mut leadcoins: Vec = vec![]; - let mut params = MoneyStakeParams { inputs: vec![], outputs: vec![], token_blind }; - let mut proofs = vec![]; - let mut own_blinds = vec![]; - let mut lead_blinds = vec![]; - for coin in coins.iter() { - // burn the coin - let value_blind = ValueBlind::random(&mut OsRng); - own_blinds.push(value_blind); - let spend_hook = pallas::Base::zero(); - let user_data = pallas::Base::zero(); - let user_data_blind = pallas::Base::random(&mut OsRng); - let tx_leaf_position = coin.leaf_position; - let tx_root = tx_tree.root(0).unwrap(); - let tx_merkle_path = tx_tree.authentication_path(tx_leaf_position, &tx_root).unwrap(); - let signature_secret = SecretKey::random(&mut OsRng); - //signature_secrets.push(signature_secret); - let (own_proof, own_revealed) = create_transfer_burn_proof( - burn_zkbin, - burn_pk, - coin.note.value, - coin.note.token_id, - coin.note.value_blind, - coin.note.token_blind, - coin.note.serial, - spend_hook, - user_data, - user_data_blind, - coin.note.coin_blind, - coin.secret, - coin.leaf_position, - tx_merkle_path.clone(), - signature_secret, - )?; - params.inputs.push(Input { - value_commit: own_revealed.value_commit, - token_commit: own_revealed.token_commit, - nullifier: own_revealed.nullifier, - merkle_root: own_revealed.merkle_root, - spend_hook: own_revealed.spend_hook, - user_data_enc: own_revealed.user_data_enc, - signature_public: own_revealed.signature_public, - }); - proofs.push(own_proof); - let lead_value_blind = ValueBlind::random(&mut OsRng); - lead_blinds.push(lead_value_blind); - sk_tree.append(&MerkleNode::from(coin.secret.inner())); - let sk_pos = sk_tree.witness().unwrap(); - let sk_root = sk_tree.root(0).unwrap(); - let sk_merkle_path = sk_tree.authentication_path(sk_pos, &sk_root).unwrap(); - let leadcoin = LeadCoin::new( - coin.note.value, - slot_index, // tau - coin.secret.inner(), // coin secret key - sk_root, - sk_pos.try_into().unwrap(), - sk_merkle_path, - coin.note.serial, - cm_tree, - ); - leadcoins.push(leadcoin.clone()); - let lead_coin_blind = ValueBlind::random(&mut OsRng); - let public_key = leadcoin.pk(); - let (lead_proof, lead_revealed) = create_stake_mint_proof( - mint_zkbin, - mint_pk, - public_key, - leadcoin.coin1_commitment, - pallas::Base::from(coin.note.value), - lead_value_blind, - lead_coin_blind, - coin.secret.inner(), - sk_root.inner(), - pallas::Base::from(slot_index), // tau - coin.note.serial, // nonce - )?; - let coin_commit_coords = [lead_revealed.commitment_x, lead_revealed.commitment_y]; - let coin_commit_hash = poseidon_hash(coin_commit_coords); - params.outputs.push(StakedOutput { - value_commit: lead_revealed.value_commit, - coin_commit_hash, - coin_pk_hash: public_key, - }); - proofs.push(lead_proof); - } - Ok((params, proofs, leadcoins, own_blinds, lead_blinds)) -} - -pub fn build_unstake_tx( - pubkey: &PublicKey, //recepient of owncoin public key - token_id_recv: TokenId, - coins: &[LeadCoin], - mint_zkbin: &ZkBinary, // stake own mint binary - mint_pk: &ProvingKey, - burn_zkbin: &ZkBinary, // unstake lead burn binary - burn_pk: &ProvingKey, -) -> Result<(MoneyUnstakeParams, Vec, Vec, Vec, Vec)> { - // convert leadcoin to owncoin - // TODO: verify this token blind usage - let token_blind = ValueBlind::random(&mut OsRng); - //let owncoins : Vec= vec![]; - let mut params = MoneyUnstakeParams { inputs: vec![], outputs: vec![], token_blind }; - let mut proofs = vec![]; - let mut own_blinds = vec![]; - let mut lead_blinds = vec![]; - for coin in coins.iter() { - // burn lead coin - let value_blind = ValueBlind::random(&mut OsRng); - lead_blinds.push(value_blind); - let pk = coin.pk(); - let nullifier = coin.sn(); - let (unstake_proof, unstake_revealed) = create_unstake_burn_proof( - burn_zkbin, - burn_pk, - pallas::Base::from(coin.value), - value_blind, - coin.coin1_blind, - pk, - coin.coin1_sk, - coin.coin1_sk_root.inner(), - MerklePosition::from(coin.coin1_sk_pos as usize), - coin.coin1_sk_merkle_path.to_vec(), - coin.coin1_commitment_merkle_path.to_vec(), - coin.coin1_commitment, - coin.coin1_commitment_root.inner(), - MerklePosition::from(coin.coin1_commitment_pos as usize), - coin.slot, - coin.nonce, - nullifier, - )?; - let commitment_coord = [unstake_revealed.commitment_x, unstake_revealed.commitment_y]; - let coin_commitment_hash = poseidon_hash(commitment_coord); - params.inputs.push(StakedInput { - nullifier: nullifier.into(), - value_commit: unstake_revealed.value_commit, - coin_commit_hash: coin_commitment_hash, - coin_pk_hash: unstake_revealed.pk, - coin_commit_root: unstake_revealed.commitment_root.into(), - sk_root: unstake_revealed.sk_root.into(), - }); - proofs.push(unstake_proof); - let own_value_blind = ValueBlind::random(&mut OsRng); - own_blinds.push(own_value_blind); - // mint own coin - let serial = pallas::Base::random(&mut OsRng); - let coin_blind = pallas::Base::random(&mut OsRng); - let token_recv_blind = ValueBlind::random(&mut OsRng); - // Disable composability for this old obsolete API - let spend_hook = pallas::Base::zero(); - let user_data = pallas::Base::zero(); - let (proof, revealed) = create_transfer_mint_proof( - mint_zkbin, - mint_pk, - coin.value, - token_id_recv, - own_value_blind, - token_recv_blind, - serial, - spend_hook, - user_data, - coin_blind, - *pubkey, //receipient public_key - )?; - proofs.push(proof); - // Encrypted note - let note = Note { - serial, - value: coin.value, - token_id: token_id_recv, - spend_hook: pallas::Base::zero(), - user_data: pallas::Base::zero(), - coin_blind, - value_blind, - token_blind: token_recv_blind, - // Here we store our secret key we use for signing - memo: vec![], - }; - - let encrypted_note = note.encrypt(&pubkey)?; - - params.outputs.push(Output { - value_commit: revealed.value_commit, - token_commit: revealed.token_commit, - coin: revealed.coin.inner(), - ciphertext: encrypted_note.ciphertext, - ephem_public: encrypted_note.ephem_public, - }); - } - Ok((params, proofs, vec![], lead_blinds, own_blinds)) -} - -fn compute_remainder_blind( - clear_inputs: &[ClearInput], - input_blinds: &[ValueBlind], - output_blinds: &[ValueBlind], -) -> ValueBlind { - let mut total = ValueBlind::zero(); - - for input in clear_inputs { - total += input.value_blind; - } - - for input_blind in input_blinds { - total += input_blind - } - - for output_blind in output_blinds { - total -= output_blind; - } - - total -} - -#[cfg(test)] -mod tests { - use darkfi_sdk::pasta::group::ff::Field; - - use super::*; - - #[test] - fn test_note_encdec() { - let note = Note { - serial: pallas::Base::random(&mut OsRng), - value: 100, - token_id: TokenId::from(pallas::Base::random(&mut OsRng)), - spend_hook: pallas::Base::zero(), - user_data: pallas::Base::zero(), - coin_blind: pallas::Base::random(&mut OsRng), - value_blind: pallas::Scalar::random(&mut OsRng), - token_blind: pallas::Scalar::random(&mut OsRng), - memo: vec![32, 223, 231, 3, 1, 1], - }; - - let keypair = Keypair::random(&mut OsRng); - - let encrypted_note = note.encrypt(&keypair.public).unwrap(); - let note2 = encrypted_note.decrypt(&keypair.secret).unwrap(); - assert_eq!(note.serial, note2.serial); - assert_eq!(note.value, note2.value); - assert_eq!(note.token_id, note2.token_id); - assert_eq!(note.coin_blind, note2.coin_blind); - assert_eq!(note.value_blind, note2.value_blind); - assert_eq!(note.token_blind, note2.token_blind); - assert_eq!(note.memo, note2.memo); - assert_eq!(note, note2); - } -} diff --git a/src/contract/money/src/client/token_mint.rs b/src/contract/money/src/client/token_mint.rs deleted file mode 100644 index 2fa71ee6f..000000000 --- a/src/contract/money/src/client/token_mint.rs +++ /dev/null @@ -1,97 +0,0 @@ -/* 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/entrypoint.rs b/src/contract/money/src/entrypoint.rs new file mode 100644 index 000000000..05a398a58 --- /dev/null +++ b/src/contract/money/src/entrypoint.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::{ContractId, MerkleTree, PublicKey}, + db::{db_init, db_lookup, db_set, set_return_data, SMART_CONTRACT_ZKAS_DB_NAME}, + error::{ContractError, ContractResult}, + msg, ContractCall, +}; +use darkfi_serial::{deserialize, serialize, Encodable, WriteExt}; + +use crate::{ + model::{MoneyFreezeUpdateV1, MoneyMintUpdateV1, MoneyTransferUpdateV1}, + MoneyFunction, MONEY_CONTRACT_COINS_TREE, MONEY_CONTRACT_COIN_MERKLE_TREE, + MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_FAUCET_PUBKEYS, MONEY_CONTRACT_INFO_TREE, + MONEY_CONTRACT_NULLIFIERS_TREE, MONEY_CONTRACT_TOKEN_FREEZE_TREE, + MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1, + MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1, MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1, +}; + +/// `Money::Transfer` functions +mod transfer_v1; +use transfer_v1::{ + money_transfer_get_metadata_v1, money_transfer_process_instruction_v1, + money_transfer_process_update_v1, +}; + +/// `Money::OtcSwap` functions +mod swap_v1; +use swap_v1::{ + money_otcswap_get_metadata_v1, money_otcswap_process_instruction_v1, + money_otcswap_process_update_v1, +}; + +/// `Money::Mint` functions +mod mint_v1; +use mint_v1::{ + money_mint_get_metadata_v1, money_mint_process_instruction_v1, money_mint_process_update_v1, +}; + +/// `Money::Freeze` functions +mod freeze_v1; +use freeze_v1::{ + money_freeze_get_metadata_v1, money_freeze_process_instruction_v1, + money_freeze_process_update_v1, +}; + +darkfi_sdk::define_contract!( + init: init_contract, + exec: process_instruction, + apply: process_update, + metadata: get_metadata +); + +/// This entrypoint function runs when the contract is (re)deployed and initialized. +/// We use this function to initialize all the necessary databases and prepare them +/// with initial data if necessary. This is also the place where we bundle the zkas +/// circuits that are to be used with functions provided by the contract. +fn init_contract(cid: ContractId, ix: &[u8]) -> ContractResult { + // The payload for now contains a vector of `PublicKey` used to + // whitelist faucets that can create clear inputs. + let faucet_pubkeys: Vec = deserialize(ix)?; + + // The zkas circuit can simply be embedded in the wasm and set up by + // the initialization. Note that the tree should then be called "zkas". + // The lookups can be done by `contract_id+_zkas+namespace`. + // TODO: For the zkas tree, external host checks should be done to ensure + // that the bincode is actually valid and not arbitrary. + let zkas_db = match db_lookup(cid, SMART_CONTRACT_ZKAS_DB_NAME) { + Ok(v) => v, + Err(_) => db_init(cid, SMART_CONTRACT_ZKAS_DB_NAME)?, + }; + + let mint_v1_bincode = include_bytes!("../proof/mint_v1.zk.bin"); + let burn_v1_bincode = include_bytes!("../proof/burn_v1.zk.bin"); + + let token_mint_v1_bincode = include_bytes!("../proof/token_mint_v1.zk.bin"); + let token_frz_v1_bincode = include_bytes!("../proof/token_freeze_v1.zk.bin"); + + db_set(zkas_db, &serialize(&MONEY_CONTRACT_ZKAS_MINT_NS_V1), &mint_v1_bincode[..])?; + db_set(zkas_db, &serialize(&MONEY_CONTRACT_ZKAS_BURN_NS_V1), &burn_v1_bincode[..])?; + db_set(zkas_db, &serialize(&MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1), &token_mint_v1_bincode[..])?; + db_set(zkas_db, &serialize(&MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1), &token_frz_v1_bincode[..])?; + + // Set up a database tree to hold Merkle roots of all coins + // k=MerkleNode, v=[] + if db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE).is_err() { + db_init(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; + } + + // Set up a database tree to hold all coins ever seen + // k=Coin, v=[] + if db_lookup(cid, MONEY_CONTRACT_COINS_TREE).is_err() { + db_init(cid, MONEY_CONTRACT_COINS_TREE)?; + } + + // Set up a database tree to hold nullifiers of all spent coins + // k=Nullifier, v=[] + if db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE).is_err() { + db_init(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?; + } + + // Set up a database tree to hold the set of frozen token mints + // k=TokenId, v=[] + if db_lookup(cid, MONEY_CONTRACT_TOKEN_FREEZE_TREE).is_err() { + db_init(cid, MONEY_CONTRACT_TOKEN_FREEZE_TREE)?; + } + + // Set up a database tree for arbitrary data + let info_db = match db_lookup(cid, MONEY_CONTRACT_INFO_TREE) { + Ok(v) => v, + Err(_) => { + let info_db = db_init(cid, MONEY_CONTRACT_INFO_TREE)?; + + // Create the incrementalmerkletree for seen coins + let coin_tree = MerkleTree::new(100); + let mut coin_tree_data = vec![]; + + coin_tree_data.write_u32(0)?; + coin_tree.encode(&mut coin_tree_data)?; + + db_set(info_db, &serialize(&MONEY_CONTRACT_COIN_MERKLE_TREE), &coin_tree_data)?; + info_db + } + }; + + // Whitelisted faucets + db_set(info_db, &serialize(&MONEY_CONTRACT_FAUCET_PUBKEYS), &serialize(&faucet_pubkeys))?; + + Ok(()) +} + +/// This function is used by the wasm VM's host to fetch the necessary metadata +/// for verifying signatures and zk proofs. The payload given here are all the +/// contract calls in the transaction. +fn get_metadata(cid: ContractId, ix: &[u8]) -> ContractResult { + let (call_idx, calls): (u32, Vec) = deserialize(ix)?; + if call_idx < calls.len() as u32 { + msg!("Error: call_idx >= calls.len()"); + return Err(ContractError::Internal) + } + + match MoneyFunction::try_from(calls[call_idx as usize].data[0])? { + MoneyFunction::TransferV1 => { + // We pass everything into the correct function, and it will return + // the metadata for us, which we can then copy into the host with + // the `set_return_data` function. On the host, this metadata will + // be used to do external verification (zk proofs, and signatures). + let metadata = money_transfer_get_metadata_v1(cid, call_idx, calls)?; + Ok(set_return_data(&metadata)?) + } + + MoneyFunction::OtcSwapV1 => { + let metadata = money_otcswap_get_metadata_v1(cid, call_idx, calls)?; + Ok(set_return_data(&metadata)?) + } + + MoneyFunction::MintV1 => { + let metadata = money_mint_get_metadata_v1(cid, call_idx, calls)?; + Ok(set_return_data(&metadata)?) + } + + MoneyFunction::FreezeV1 => { + let metadata = money_freeze_get_metadata_v1(cid, call_idx, calls)?; + Ok(set_return_data(&metadata)?) + } + } +} + +/// This function verifies a state transition and produces a state update +/// if everything is successful. This step should happen **after** the host +/// has successfully verified the metadata from `get_metadata()`. +fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult { + let (call_idx, calls): (u32, Vec) = deserialize(ix)?; + if call_idx < calls.len() as u32 { + msg!("Error: call_idx => calls.len()"); + return Err(ContractError::Internal) + } + + match MoneyFunction::try_from(calls[call_idx as usize].data[0])? { + MoneyFunction::TransferV1 => { + // Again, we pass everything into the correct function. + // If it executes successfully, we'll get a state update + // which we can copy into the host using `set_return_data`. + // This update can then be written with `process_update()` + // if everything is in order. + let update_data = money_transfer_process_instruction_v1(cid, call_idx, calls)?; + Ok(set_return_data(&update_data)?) + } + + MoneyFunction::OtcSwapV1 => { + let update_data = money_otcswap_process_instruction_v1(cid, call_idx, calls)?; + Ok(set_return_data(&update_data)?) + } + + MoneyFunction::MintV1 => { + let update_data = money_mint_process_instruction_v1(cid, call_idx, calls)?; + Ok(set_return_data(&update_data)?) + } + + MoneyFunction::FreezeV1 => { + let update_data = money_freeze_process_instruction_v1(cid, call_idx, calls)?; + Ok(set_return_data(&update_data)?) + } + } +} + +/// This function attempts to write a given state update provided the previous steps +/// of the contract call execution all were successful. It's the last in line, and +/// assumes that the transaction/call was successful. The payload given to the function +/// is the update data retrieved from `process_instruction()`. +fn process_update(cid: ContractId, update_data: &[u8]) -> ContractResult { + match MoneyFunction::try_from(update_data[0])? { + MoneyFunction::TransferV1 => { + let update: MoneyTransferUpdateV1 = deserialize(&update_data[1..])?; + Ok(money_transfer_process_update_v1(cid, update)?) + } + + MoneyFunction::OtcSwapV1 => { + // For the atomic swaps, we use the same state update like we would + // use for `Money::Transfer`. + let update: MoneyTransferUpdateV1 = deserialize(&update_data[1..])?; + Ok(money_otcswap_process_update_v1(cid, update)?) + } + + MoneyFunction::MintV1 => { + let update: MoneyMintUpdateV1 = deserialize(&update_data[1..])?; + Ok(money_mint_process_update_v1(cid, update)?) + } + + MoneyFunction::FreezeV1 => { + let update: MoneyFreezeUpdateV1 = deserialize(&update_data[1..])?; + Ok(money_freeze_process_update_v1(cid, update)?) + } + } +} diff --git a/src/contract/money/src/entrypoint/freeze_v1.rs b/src/contract/money/src/entrypoint/freeze_v1.rs new file mode 100644 index 000000000..7b43fd33e --- /dev/null +++ b/src/contract/money/src/entrypoint/freeze_v1.rs @@ -0,0 +1,107 @@ +/* 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::{poseidon_hash, ContractId, PublicKey, TokenId}, + db::{db_contains_key, db_lookup, db_set}, + error::{ContractError, ContractResult}, + msg, + pasta::pallas, + ContractCall, +}; +use darkfi_serial::{deserialize, serialize, Encodable, WriteExt}; + +use crate::{ + error::MoneyError, + model::{MoneyFreezeParamsV1, MoneyFreezeUpdateV1}, + MoneyFunction, MONEY_CONTRACT_TOKEN_FREEZE_TREE, MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1, +}; + +/// `get_metadata` function for `Money::FreezeV1` +pub(crate) fn money_freeze_get_metadata_v1( + _cid: ContractId, + call_idx: u32, + calls: Vec, +) -> Result, ContractError> { + let self_ = &calls[call_idx as usize]; + let params: MoneyFreezeParamsV1 = deserialize(&self_.data[1..])?; + + // Public inputs for the ZK proofs we have to verify + let mut zk_public_inputs: Vec<(String, Vec)> = vec![]; + // Public keys for the transaction signatures we have to verify + let signature_pubkeys: Vec = vec![params.signature_public]; + + let (mint_x, mint_y) = params.signature_public.xy(); + let token_id = poseidon_hash([mint_x, mint_y]); + + zk_public_inputs + .push((MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1.to_string(), vec![mint_x, mint_y, token_id])); + + // Serialize everything gathered and return it + let mut metadata = vec![]; + zk_public_inputs.encode(&mut metadata)?; + signature_pubkeys.encode(&mut metadata)?; + + Ok(metadata) +} + +/// `process_instruction` function for `Money::FreezeV1` +pub(crate) fn money_freeze_process_instruction_v1( + cid: ContractId, + call_idx: u32, + calls: Vec, +) -> Result, ContractError> { + let self_ = &calls[call_idx as usize]; + let params: MoneyFreezeParamsV1 = deserialize(&self_.data[1..])?; + + // We just check if the mint was already frozen beforehand + let token_freeze_db = db_lookup(cid, MONEY_CONTRACT_TOKEN_FREEZE_TREE)?; + + let (mint_x, mint_y) = params.signature_public.xy(); + let token_id = TokenId::from(poseidon_hash([mint_x, mint_y])); + + // Check that the mint is not frozen + if db_contains_key(token_freeze_db, &serialize(&token_id))? { + msg!("[MintV1] Error: Token mint for {} is frozen", token_id); + return Err(MoneyError::MintFrozen.into()) + } + + // Create a state update. We only need the new coin. + let update = MoneyFreezeUpdateV1 { signature_public: params.signature_public }; + let mut update_data = vec![]; + update_data.write_u8(MoneyFunction::FreezeV1 as u8)?; + update.encode(&mut update_data)?; + + Ok(update_data) +} + +/// `process_update` function for `Money::FreezeV1` +pub(crate) fn money_freeze_process_update_v1( + cid: ContractId, + update: MoneyFreezeUpdateV1, +) -> ContractResult { + let token_freeze_db = db_lookup(cid, MONEY_CONTRACT_TOKEN_FREEZE_TREE)?; + + let (mint_x, mint_y) = update.signature_public.xy(); + let token_id = TokenId::from(poseidon_hash([mint_x, mint_y])); + + msg!("[MintV1] Freezing mint for token {}", token_id); + db_set(token_freeze_db, &serialize(&token_id), &[])?; + + Ok(()) +} diff --git a/src/contract/money/src/mint.rs b/src/contract/money/src/entrypoint/mint_v1.rs similarity index 54% rename from src/contract/money/src/mint.rs rename to src/contract/money/src/entrypoint/mint_v1.rs index 2703bfcdf..bac69158b 100644 --- a/src/contract/money/src/mint.rs +++ b/src/contract/money/src/entrypoint/mint_v1.rs @@ -21,8 +21,8 @@ use darkfi_sdk::{ pasta_prelude::*, pedersen_commitment_base, pedersen_commitment_u64, poseidon_hash, Coin, ContractId, MerkleNode, PublicKey, TokenId, }, - db::{db_contains_key, db_lookup}, - error::ContractError, + db::{db_contains_key, db_lookup, db_set}, + error::{ContractError, ContractResult}, merkle_add, msg, pasta::pallas, ContractCall, @@ -30,31 +30,41 @@ use darkfi_sdk::{ use darkfi_serial::{deserialize, serialize, Encodable, WriteExt}; use crate::{ - MoneyFunction, MoneyMintParams, MoneyMintUpdate, MONEY_CONTRACT_COIN_MERKLE_TREE, + error::MoneyError, + model::{MoneyMintParamsV1, MoneyMintUpdateV1}, + MoneyFunction, MONEY_CONTRACT_COINS_TREE, 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( +/// `get_metadata` function for `Money::MintV1` +pub(crate) fn money_mint_get_metadata_v1( _cid: ContractId, call_idx: u32, calls: Vec, ) -> Result, ContractError> { let self_ = &calls[call_idx as usize]; - let params: MoneyMintParams = deserialize(&self_.data[1..])?; + let params: MoneyMintParamsV1 = deserialize(&self_.data[1..])?; - let mut zk_public_values: Vec<(String, Vec)> = vec![]; + // Public inputs for the ZK proofs we have to verify + let mut zk_public_inputs: Vec<(String, Vec)> = vec![]; + // Public keys for the transaction signatures we have to verify let mut signature_pubkeys: Vec = vec![]; + // The minting transaction creates 1 clear input and 1 anonymous output. + // We check the signature from the clear input, which is supposed to be + // signed by the mint authority. 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 token_coords = params.output.value_commit.to_affine().coordinates().unwrap(); + // Since we expect a signature from the mint authority, we use those coordinates + // as public inputs for the ZK proof: let (sig_x, sig_y) = params.input.signature_public.xy(); let token_id = poseidon_hash([sig_x, sig_y]); - zk_public_values.push(( + zk_public_inputs.push(( MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1.to_string(), vec![ sig_x, @@ -68,77 +78,89 @@ pub fn money_mint_get_metadata( ], )); + // Serialize everything gathered and return it let mut metadata = vec![]; - zk_public_values.encode(&mut metadata)?; + zk_public_inputs.encode(&mut metadata)?; signature_pubkeys.encode(&mut metadata)?; Ok(metadata) } -pub fn money_mint_process_instruction( +/// `process_instruction` function for `Money::MintV1` +pub(crate) fn money_mint_process_instruction_v1( cid: ContractId, call_idx: u32, calls: Vec, ) -> Result, ContractError> { let self_ = &calls[call_idx as usize]; - let params: MoneyMintParams = deserialize(&self_.data[1..])?; + let params: MoneyMintParamsV1 = 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)?; + // We have to check if the token mint is frozen, and if by some chance + // the minted coin has existed already. + let coins_db = db_lookup(cid, MONEY_CONTRACT_COINS_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)) + msg!("[MintV1] Error: Token ID does not derive from mint authority"); + return Err(MoneyError::TokenIdDoesNotDeriveFromMint.into()) } // 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)) + msg!("[MintV1] Error: Token mint for {} is frozen", token_id); + return Err(MoneyError::MintFrozen.into()) } - // 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. + // Check that the coin from the output hasn't existed before + if db_contains_key(coins_db, &serialize(¶ms.output.coin))? { + msg!("[MintV1] Error: Duplicate coin in output"); + return Err(MoneyError::DuplicateCoin.into()) + } - // Verify that the value and token commitments match + // 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!("[Mint] Error: Value commitments do not match"); - return Err(ContractError::Custom(10)) + msg!("[MintV1] Error: Value commitment mismatch"); + return Err(MoneyError::ValueMismatch.into()) } 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)) + msg!("[MintV1] Error: Token commitment mismatch"); + return Err(MoneyError::TokenMismatch.into()) } // Create a state update. We only need the new coin. - let update = MoneyMintUpdate { coin: Coin::from(params.output.coin) }; + let update = MoneyMintUpdateV1 { coin: Coin::from(params.output.coin) }; let mut update_data = vec![]; - update_data.write_u8(MoneyFunction::Mint as u8)?; + update_data.write_u8(MoneyFunction::MintV1 as u8)?; update.encode(&mut update_data)?; Ok(update_data) } -pub fn money_mint_process_update( +/// `process_update` function for `Money::MintV1` +pub(crate) fn money_mint_process_update_v1( cid: ContractId, - update: MoneyMintUpdate, -) -> Result<(), ContractError> { + update: MoneyMintUpdateV1, +) -> ContractResult { + // Grab all db handles we want to work on let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?; + let coins_db = db_lookup(cid, MONEY_CONTRACT_COINS_TREE)?; let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; - let coins = vec![MerkleNode::from(update.coin.inner())]; + msg!("[MintV1] Adding new coin to the set"); + db_set(coins_db, &serialize(&update.coin), &[])?; - msg!("[Mint] Adding new coin to Merkle tree"); + msg!("[MintV1] Adding new coin to the Merkle tree"); + let coins = vec![MerkleNode::from(update.coin.inner())]; merkle_add(info_db, coin_roots_db, &serialize(&MONEY_CONTRACT_COIN_MERKLE_TREE), &coins)?; Ok(()) diff --git a/src/contract/money/src/entrypoint/swap_v1.rs b/src/contract/money/src/entrypoint/swap_v1.rs new file mode 100644 index 000000000..1311902f8 --- /dev/null +++ b/src/contract/money/src/entrypoint/swap_v1.rs @@ -0,0 +1,171 @@ +/* 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, ContractResult}, + msg, + pasta::pallas, + ContractCall, +}; +use darkfi_serial::{deserialize, serialize, Encodable, WriteExt}; + +use super::transfer_v1::{money_transfer_get_metadata_v1, money_transfer_process_update_v1}; +use crate::{ + error::MoneyError, + model::{MoneyTransferParamsV1, MoneyTransferUpdateV1}, + MoneyFunction, MONEY_CONTRACT_COINS_TREE, MONEY_CONTRACT_COIN_ROOTS_TREE, + MONEY_CONTRACT_NULLIFIERS_TREE, +}; + +/// `get_metadata` function for `Money::OtcSwapV1` +pub(crate) fn money_otcswap_get_metadata_v1( + cid: ContractId, + call_idx: u32, + calls: Vec, +) -> Result, ContractError> { + // In here we can use the same function as we use in `TransferV1`. + Ok(money_transfer_get_metadata_v1(cid, call_idx, calls)?) +} + +/// `process_instruction` function for `Money::OtcSwapV1` +pub(crate) fn money_otcswap_process_instruction_v1( + cid: ContractId, + call_idx: u32, + calls: Vec, +) -> Result, ContractError> { + let self_ = &calls[call_idx as usize]; + let params: MoneyTransferParamsV1 = deserialize(&self_.data[1..])?; + + // The atomic swap is able to use the same parameters as `TransferV1`. + // In here we just have a different state transition where we enforce + // 2 anonymous inputs and 2 anonymous outputs. This is enforced so that + // every atomic swap looks the same on the network, therefore there is + // no special anonymity leak for different swaps that are being done, + // at least in the scope of this contract call. + + if !params.clear_inputs.is_empty() { + msg!("[OtcSwapV1] Error: Clear inputs are not empty"); + return Err(MoneyError::InvalidNumberOfInputs.into()) + } + + if params.inputs.len() != 2 { + msg!("[OtcSwapV1] Error: Expected 2 inputs"); + return Err(MoneyError::InvalidNumberOfInputs.into()) + } + + if params.outputs.len() != 2 { + msg!("[OtcSwapV1] Error: Expected 2 outputs"); + return Err(MoneyError::InvalidNumberOfOutputs.into()) + } + + // Grab the db handles we'll be using here + let coins_db = db_lookup(cid, MONEY_CONTRACT_COINS_TREE)?; + let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?; + let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; + + // We expect two new nullifiers and two new coins + let mut new_nullifiers = Vec::with_capacity(2); + let mut new_coins = 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!("[OtcSwapV1] Error: Value commitments for input 0 and output 1 mismatch"); + return Err(MoneyError::ValueMismatch.into()) + } + + if params.inputs[1].value_commit != params.outputs[0].value_commit { + msg!("[OtcSwapV1] Error: Value commitments for input 1 and ouptut 0 mismatch"); + return Err(MoneyError::ValueMismatch.into()) + } + + if params.inputs[0].token_commit != params.outputs[1].token_commit { + msg!("[OtcSwapV1] Error: Token commitments for input 0 and output 1 mismatch"); + return Err(MoneyError::TokenMismatch.into()) + } + + if params.inputs[1].token_commit != params.outputs[0].token_commit { + msg!("[OtcSwapV1] Error: Token commitments for input 1 and output 0 mismatch"); + return Err(MoneyError::TokenMismatch.into()) + } + + msg!("[OtcSwapV1] Iterating 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, e.g. if we + // want a DAO to be able to do an atomic swap. + if input.spend_hook != pallas::Base::zero() { + msg!("[OtcSwapV1] Error: Unable to swap coins with spend_hook != 0 (input {})", i); + return Err(MoneyError::SpendHookNonZero.into()) + } + + // The Merkle root is used to know whether this coin + // has existed in a previous state. + if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? { + msg!("[OtcSwapV1] Error: Merkle root not found in previous state (input {})", i); + return Err(MoneyError::SwapMerkleRootNotFound.into()) + } + + // 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!("[OtcSwapV1] Error: Duplicate nullifier found in input {}", i); + return Err(MoneyError::DuplicateNullifier.into()) + } + + new_nullifiers.push(input.nullifier); + } + + // Newly created coins for this call are in the outputs + for (i, output) in params.outputs.iter().enumerate() { + if new_coins.contains(&Coin::from(output.coin)) || + db_contains_key(coins_db, &serialize(&output.coin))? + { + msg!("[OtcSwapV1] Error: Duplicate coin found in output {}", i); + return Err(MoneyError::DuplicateCoin.into()) + } + + new_coins.push(Coin::from(output.coin)); + } + + // Create a state update. We also use `MoneyTransferUpdateV1` because + // they're essentially the same thing, just with a different transition + // ruleset. + // FIXME: The function should not actually be written here. It should + // be prepended by the host to enforce correctness. The host can + // simply copy it from the payload. + let update = MoneyTransferUpdateV1 { nullifiers: new_nullifiers, coins: new_coins }; + let mut update_data = vec![]; + update_data.write_u8(MoneyFunction::OtcSwapV1 as u8)?; + update.encode(&mut update_data)?; + + Ok(update_data) +} + +/// `process_update` function for `Money::OtcSwapV1` +pub(crate) fn money_otcswap_process_update_v1( + cid: ContractId, + update: MoneyTransferUpdateV1, +) -> ContractResult { + // In here we can use the same function as we use in `TransferV1`. + Ok(money_transfer_process_update_v1(cid, update)?) +} diff --git a/src/contract/money/src/entrypoint/transfer_v1.rs b/src/contract/money/src/entrypoint/transfer_v1.rs new file mode 100644 index 000000000..9f6a35b6f --- /dev/null +++ b/src/contract/money/src/entrypoint/transfer_v1.rs @@ -0,0 +1,291 @@ +/* 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, ContractResult}, + merkle_add, msg, + pasta::pallas, + ContractCall, +}; +use darkfi_serial::{deserialize, serialize, Encodable, WriteExt}; + +use crate::{ + error::MoneyError, + model::{MoneyTransferParamsV1, MoneyTransferUpdateV1}, + MoneyFunction, MONEY_CONTRACT_COINS_TREE, 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, +}; + +/// `get_metadata` function for `Money::TransferV1` +pub(crate) fn money_transfer_get_metadata_v1( + _cid: ContractId, + call_idx: u32, + calls: Vec, +) -> Result, ContractError> { + let self_ = &calls[call_idx as usize]; + let params: MoneyTransferParamsV1 = deserialize(&self_.data[1..])?; + + // Public inputs for the ZK proofs we have to verify + let mut zk_public_inputs: Vec<(String, Vec)> = vec![]; + // Public keys for the transaction signatures we have to verify + let mut signature_pubkeys: Vec = vec![]; + + // Take all the pubkeys from any clear inputs + for input in ¶ms.clear_inputs { + signature_pubkeys.push(input.signature_public); + } + + // Grab the pedersen commitments and signature pubkeys from the + // anonymous inputs + 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(); + + // It is very important that these are in the same order as the + // `constrain_instance` calls in the zkas code. + // Otherwise verification will fail. + zk_public_inputs.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); + } + + // Grab the pedersen commitments from the anonymous outputs + 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_inputs.push(( + MONEY_CONTRACT_ZKAS_MINT_NS_V1.to_string(), + vec![ + output.coin, + *value_coords.x(), + *value_coords.y(), + *token_coords.x(), + *token_coords.y(), + ], + )); + } + + // Serialize everything gathered and return it + let mut metadata = vec![]; + zk_public_inputs.encode(&mut metadata)?; + signature_pubkeys.encode(&mut metadata)?; + + Ok(metadata) +} + +/// `process_instruction` function for `Money::TransferV1` +pub(crate) fn money_transfer_process_instruction_v1( + cid: ContractId, + call_idx: u32, + calls: Vec, +) -> Result, ContractError> { + let self_ = &calls[call_idx as usize]; + let params: MoneyTransferParamsV1 = deserialize(&self_.data[1..])?; + + if params.clear_inputs.len() + params.inputs.len() < 1 { + msg!("[TransferV1] Error: No inputs in the call"); + return Err(MoneyError::TransferMissingInputs.into()) + } + + if params.outputs.is_empty() { + msg!("[TransferV1] 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 info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?; + let coins_db = db_lookup(cid, MONEY_CONTRACT_COINS_TREE)?; + let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?; + let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; + + // Grab faucet pubkeys. They're allowed to create clear inputs. + // Currently we use them for airdrops in the testnet. + let Some(faucet_pubkeys) = db_get(info_db, &serialize(&MONEY_CONTRACT_FAUCET_PUBKEYS))? else { + msg!("[TransferV1] Error: Missing faucet pubkeys from info db"); + return Err(MoneyError::TransferMissingFaucetKeys.into()) + }; + let faucet_pubkeys: Vec = deserialize(&faucet_pubkeys)?; + + // Accumulator for the value commitments. We add inputs to it, and subtract + // outputs from it. For the commitments to be valid, the accumulator must + // be in its initial state after performing the arithmetics. + let mut valcom_total = pallas::Point::identity(); + + // =================================== + // Perform the actual state transition + // =================================== + + // For clear inputs, we only allow the whitelisted faucet(s) to create them. + // Additionally, only DARK_TOKEN_ID is able to be here. For any arbitrary + // tokens, there is another functionality in this contract called `Mint` which + // allows users to mint their own tokens. + msg!("[TransferV1] Iterating over clear inputs"); + for (i, input) in params.clear_inputs.iter().enumerate() { + if input.token_id != *DARK_TOKEN_ID { + msg!("[TransferV1] Error: Clear input {} used non-native token", i); + return Err(MoneyError::TransferClearInputNonNativeToken.into()) + } + + if !faucet_pubkeys.contains(&input.signature_public) { + msg!("[TransferV1] Error: Clear input {} used unauthorised pubkey", i); + return Err(MoneyError::TransferClearInputUnauthorised.into()) + } + + // Add this input to the value commitment accumulator + valcom_total += pedersen_commitment_u64(input.value, input.value_blind); + } + + // For anonymous inputs, we must also gather all the new nullifiers + // that are introduced. + let mut new_nullifiers = Vec::with_capacity(params.inputs.len()); + msg!("[TransferV1] 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!("[TransferV1] Error: Merkle root not found in previous state (input {})", i); + return Err(MoneyError::TransferMerkleRootNotFound.into()) + } + + // 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!("[TransferV1] Error: Duplicate nullifier found (input {})", i); + return Err(MoneyError::DuplicateNullifier.into()) + } + + // If spend hook is set, check its correctness + if input.spend_hook != pallas::Base::zero() { + let next_call_idx = call_idx + 1; + if next_call_idx >= calls.len() as u32 { + msg!("[TransferV1] Error: next_call_idx out of bounds (input {})", i); + return Err(MoneyError::SpendHookOutOfBounds.into()) + } + + let next = &calls[next_call_idx as usize]; + if next.contract_id.inner() != input.spend_hook { + msg!("[TransferV1] Error: Invoking contract call does not match spend hook in input {}", i); + return Err(MoneyError::SpendHookMismatch.into()) + } + } + + // Append this new nullifier to seen nullifiers, and accumulate the value commitment + new_nullifiers.push(input.nullifier); + valcom_total += input.value_commit; + } + + // Newly created coins for this call are in the outputs. Here we gather them, + // and we also check that they haven't existed before. + let mut new_coins = Vec::with_capacity(params.outputs.len()); + for (i, output) in params.outputs.iter().enumerate() { + if new_coins.contains(&Coin::from(output.coin)) || + db_contains_key(coins_db, &serialize(&output.coin))? + { + msg!("[TransferV1] Error: Duplicate coin found in output {}", i); + return Err(MoneyError::DuplicateCoin.into()) + } + + // Append this new coin to seen coins, and subtract the value commitment + new_coins.push(Coin::from(output.coin)); + valcom_total -= output.value_commit; + } + + // If the accumulator is not back in its initial state, that means there + // is a value mismatch between inputs and outputs. + if valcom_total != pallas::Point::identity() { + msg!("[TransferV1] Error: Value commitments do not result in identity"); + return Err(MoneyError::ValueMismatch.into()) + } + + // We also need to verify that all token commitments are the same. + // In the basic transfer, we only allow the same token type to be + // transferred. For exchanging we use another functionality of this + // contract called `OtcSwap`. + let tokcom = params.outputs[0].token_commit; + let mut failed_tokcom = params.inputs.iter().any(|x| x.token_commit != tokcom); + failed_tokcom = failed_tokcom || params.outputs.iter().any(|x| x.token_commit != tokcom); + failed_tokcom = failed_tokcom || + params + .clear_inputs + .iter() + .any(|x| pedersen_commitment_base(x.token_id.inner(), x.token_blind) != tokcom); + + if failed_tokcom { + msg!("[TransferV1] Error: Token commitments do not match"); + return Err(MoneyError::TokenMismatch.into()) + } + + // At this point the state transition has passed, so we create a state update + let update = MoneyTransferUpdateV1 { nullifiers: new_nullifiers, coins: new_coins }; + let mut update_data = vec![]; + update_data.write_u8(MoneyFunction::TransferV1 as u8)?; + update.encode(&mut update_data)?; + // and return it + Ok(update_data) +} + +/// `process_update` function for `Money::TransferV1` +pub(crate) fn money_transfer_process_update_v1( + cid: ContractId, + update: MoneyTransferUpdateV1, +) -> ContractResult { + // Grab all necessary db handles for where we want to write + let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?; + let coins_db = db_lookup(cid, MONEY_CONTRACT_COINS_TREE)?; + let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?; + let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; + + msg!("[TransferV1] Adding new nullifiers to the set"); + for nullifier in update.nullifiers { + db_set(nullifiers_db, &serialize(&nullifier), &[])?; + } + + msg!("[TransferV1] Adding new coins to the set"); + for coin in &update.coins { + db_set(coins_db, &serialize(coin), &[])?; + } + + msg!("[TransferV1] Adding new coins to the Merkle tree"); + 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)?; + + Ok(()) +} diff --git a/src/contract/money/src/error.rs b/src/contract/money/src/error.rs new file mode 100644 index 000000000..a27c8d861 --- /dev/null +++ b/src/contract/money/src/error.rs @@ -0,0 +1,101 @@ +/* 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::error::ContractError; + +#[derive(Debug, Clone, thiserror::Error)] +pub enum MoneyError { + #[error("Missing inputs in transfer call")] + TransferMissingInputs, + + #[error("Missing outputs in transfer call")] + TransferMissingOutputs, + + #[error("Missing faucet pubkeys from info db")] + TransferMissingFaucetKeys, + + #[error("Clear input used non-native token")] + TransferClearInputNonNativeToken, + + #[error("Clear input used unauthorised pubkey")] + TransferClearInputUnauthorised, + + #[error("Merkle root not found in previous state")] + TransferMerkleRootNotFound, + + #[error("Duplicate nullifier found")] + DuplicateNullifier, + + #[error("Spend hook out of bounds")] + SpendHookOutOfBounds, + + #[error("Spend hook mismatch")] + SpendHookMismatch, + + #[error("Duplicate coin found")] + DuplicateCoin, + + #[error("Value commitment mismatch")] + ValueMismatch, + + #[error("Token commitment mismatch")] + TokenMismatch, + + #[error("Invalid number of inputs")] + InvalidNumberOfInputs, + + #[error("Invalid number of outputs")] + InvalidNumberOfOutputs, + + #[error("Spend hook is not zero")] + SpendHookNonZero, + + #[error("Merkle root not found in previous state")] + SwapMerkleRootNotFound, + + #[error("Token ID does not derive from mint authority")] + TokenIdDoesNotDeriveFromMint, + + #[error("Token mint is frozen")] + MintFrozen, +} + +impl From for ContractError { + fn from(e: MoneyError) -> Self { + match e { + MoneyError::TransferMissingInputs => Self::Custom(1), + MoneyError::TransferMissingOutputs => Self::Custom(2), + MoneyError::TransferMissingFaucetKeys => Self::Custom(3), + MoneyError::TransferClearInputNonNativeToken => Self::Custom(4), + MoneyError::TransferClearInputUnauthorised => Self::Custom(5), + MoneyError::TransferMerkleRootNotFound => Self::Custom(6), + MoneyError::DuplicateNullifier => Self::Custom(7), + MoneyError::SpendHookOutOfBounds => Self::Custom(8), + MoneyError::SpendHookMismatch => Self::Custom(9), + MoneyError::DuplicateCoin => Self::Custom(10), + MoneyError::ValueMismatch => Self::Custom(11), + MoneyError::TokenMismatch => Self::Custom(12), + MoneyError::InvalidNumberOfInputs => Self::Custom(13), + MoneyError::InvalidNumberOfOutputs => Self::Custom(14), + MoneyError::SpendHookNonZero => Self::Custom(15), + MoneyError::SwapMerkleRootNotFound => Self::Custom(16), + MoneyError::TokenIdDoesNotDeriveFromMint => Self::Custom(17), + MoneyError::MintFrozen => Self::Custom(18), + } + } +} diff --git a/src/contract/money/src/lib.rs b/src/contract/money/src/lib.rs index 9621157e5..1bc0bf804 100644 --- a/src/contract/money/src/lib.rs +++ b/src/contract/money/src/lib.rs @@ -16,98 +16,59 @@ * along with this program. If not, see . */ -#[cfg(not(feature = "no-entrypoint"))] -use darkfi_sdk::{ - crypto::{ - pallas, pasta_prelude::*, pedersen_commitment_base, Coin, ContractId, MerkleNode, - MerkleTree, PublicKey, DARK_TOKEN_ID, - }, - db::{ - db_contains_key, db_init, db_lookup, db_set, set_return_data, SMART_CONTRACT_ZKAS_DB_NAME, - }, - error::ContractResult, - merkle::merkle_add, - msg, ContractCall, -}; +//! Smart contract implementing money transfers, atomic swaps, token +//! minting and freezing, and staking/unstaking of consensus tokens. use darkfi_sdk::error::ContractError; -#[cfg(not(feature = "no-entrypoint"))] -use darkfi_serial::{deserialize, serialize, Encodable, WriteExt}; - -/// Functions we allow in this contract +/// Functions available in the contract #[repr(u8)] pub enum MoneyFunction { - Transfer = 0x00, - OtcSwap = 0x01, - Stake = 0x02, - Unstake = 0x03, - Mint = 0x04, - Freeze = 0x05, + TransferV1 = 0x00, + OtcSwapV1 = 0x01, + MintV1 = 0x02, + FreezeV1 = 0x03, + //Fee = 0x04, + //Stake = 0x05, + //Unstake = 0x06, } impl TryFrom for MoneyFunction { type Error = ContractError; - fn try_from(b: u8) -> core::result::Result { + fn try_from(b: u8) -> core::result::Result { match b { - 0x00 => Ok(Self::Transfer), - 0x01 => Ok(Self::OtcSwap), - 0x02 => Ok(Self::Stake), - 0x03 => Ok(Self::Unstake), - 0x04 => Ok(Self::Mint), - 0x05 => Ok(Self::Freeze), + 0x00 => Ok(Self::TransferV1), + 0x01 => Ok(Self::OtcSwapV1), + 0x02 => Ok(Self::MintV1), + 0x03 => Ok(Self::FreezeV1), + //0x04 => Ok(Self::Fee), + //0x05 => Ok(Self::Stake), + //0x06 => Ok(Self::Unstake), _ => Err(ContractError::InvalidFunction), } } } -/// Structures and object definitions +/// Internal contract errors +pub mod error; + +#[cfg(not(feature = "no-entrypoint"))] +/// WASM entrypoint functions +pub mod entrypoint; + +/// Call parameters 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::{ - MoneyMintParams, MoneyMintUpdate, MoneyStakeParams, MoneyStakeUpdate, MoneyTransferParams, - MoneyTransferUpdate, MoneyUnstakeParams, -}; - -#[cfg(feature = "client")] -/// Transaction building API for clients interacting with this contract. -pub mod client; - -#[cfg(not(feature = "no-entrypoint"))] -darkfi_sdk::define_contract!( - init: init_contract, - exec: process_instruction, - apply: process_update, - metadata: get_metadata -); - // These are the different sled trees that will be created +pub const MONEY_CONTRACT_INFO_TREE: &str = "info"; +pub const MONEY_CONTRACT_COINS_TREE: &str = "coins"; pub const MONEY_CONTRACT_COIN_ROOTS_TREE: &str = "coin_roots"; pub const MONEY_CONTRACT_NULLIFIERS_TREE: &str = "nullifiers"; pub const MONEY_CONTRACT_TOKEN_FREEZE_TREE: &str = "token_freezes"; -pub const MONEY_CONTRACT_INFO_TREE: &str = "info"; -// lead coin, nullifier sled trees. -pub const MONEY_CONTRACT_LEAD_COIN_ROOTS_TREE: &str = "lead_coin_roots"; -pub const MONEY_CONTRACT_LEAD_NULLIFIERS_TREE: &str = "lead_nullifiers"; -pub const MONEY_CONTRACT_LEAD_INFO_TREE: &str = "lead_info"; -// This is a key inside the info tree +// These are keys inside the info tree pub const MONEY_CONTRACT_COIN_MERKLE_TREE: &str = "coin_tree"; -pub const MONEY_CONTRACT_LEAD_COIN_MERKLE_TREE: &str = "lead_coin_tree"; pub const MONEY_CONTRACT_FAUCET_PUBKEYS: &str = "faucet_pubkeys"; /// zkas mint circuit namespace @@ -118,483 +79,3 @@ pub const MONEY_CONTRACT_ZKAS_BURN_NS_V1: &str = "Burn_V1"; pub const MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1: &str = "TokenMint_V1"; /// zkas token freeze circuit namespace pub const MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1: &str = "TokenFreeze_V1"; -/// zkas staking coin mint circuit namespace -pub const MONEY_CONTRACT_ZKAS_LEAD_MINT_NS_V1: &str = "Lead_Mint_V1"; -/// zkas staking coin burn circuit namespace -pub const MONEY_CONTRACT_ZKAS_LEAD_BURN_NS_V1: &str = "Lead_Burn_V1"; - -/// This function runs when the contract is (re)deployed and initialized. -#[cfg(not(feature = "no-entrypoint"))] -fn init_contract(cid: ContractId, ix: &[u8]) -> ContractResult { - // The payload for now contains a vector of `PublicKey` used to - // whitelist faucets that can create clear inputs. - let faucet_pubkeys: Vec = deserialize(ix)?; - - // The zkas circuits can simply be embedded in the wasm and set up by - // the initialization. Note that the tree should then be called "zkas". - // The lookups can then be done by `contract_id+_zkas+namespace`. - let zkas_db = match db_lookup(cid, SMART_CONTRACT_ZKAS_DB_NAME) { - Ok(v) => v, - Err(_) => db_init(cid, SMART_CONTRACT_ZKAS_DB_NAME)?, - }; - - let mint_v1_bincode = include_bytes!("../proof/mint_v1.zk.bin"); - let burn_v1_bincode = include_bytes!("../proof/burn_v1.zk.bin"); - - let token_mint_v1_bincode = include_bytes!("../proof/token_mint_v1.zk.bin"); - let token_frz_v1_bincode = include_bytes!("../proof/token_freeze_v1.zk.bin"); - - 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"); - - /* 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); - let zkbin = ZkBinary::decode(burn_bincode)?; - let burn_namespace = zkbin.namespace.clone(); - assert_eq!(&burn_namespace, ZKAS_BURN_NS); - db_set(zkas_db, &serialize(&mint_namespace), &mint_bincode[..])?; - db_set(zkas_db, &serialize(&burn_namespace), &burn_bincode[..])?; - */ - - db_set(zkas_db, &serialize(&MONEY_CONTRACT_ZKAS_MINT_NS_V1), &mint_v1_bincode[..])?; - db_set(zkas_db, &serialize(&MONEY_CONTRACT_ZKAS_BURN_NS_V1), &burn_v1_bincode[..])?; - db_set(zkas_db, &serialize(&MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1), &token_mint_v1_bincode[..])?; - db_set(zkas_db, &serialize(&MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1), &token_frz_v1_bincode[..])?; - db_set(zkas_db, &serialize(&MONEY_CONTRACT_ZKAS_LEAD_MINT_NS_V1), &lead_mint_v1_bincode[..])?; - db_set(zkas_db, &serialize(&MONEY_CONTRACT_ZKAS_LEAD_BURN_NS_V1), &lead_burn_v1_bincode[..])?; - - // Set up a database tree to hold Merkle roots of all coins - if db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE).is_err() { - db_init(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; - } - - // Set up a database tree to hold nullifiers of all spent coins - if db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE).is_err() { - db_init(cid, MONEY_CONTRACT_NULLIFIERS_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)?; - } - - /* - // Set up a database tree to hold lead Merkle roots - if db_lookup(cid, MONEY_CONTRACT_LEAD_COIN_ROOTS_TREE).is_err() { - db_init(cid, MONEY_CONTRACT_LEAD_COIN_ROOTS_TREE)?; - } - - // Set up a database tree to hold nullifiers - if db_lookup(cid, MONEY_CONTRACT_LEAD_NULLIFIERS_TREE).is_err() { - db_init(cid, MONEY_CONTRACT_LEAD_NULLIFIERS_TREE)?; - } - */ - - // Set up a database tree for arbitrary data - let info_db = match db_lookup(cid, MONEY_CONTRACT_INFO_TREE) { - Ok(v) => v, - Err(_) => { - let info_db = db_init(cid, MONEY_CONTRACT_INFO_TREE)?; - // Add a Merkle tree to the info db: - let coin_tree = MerkleTree::new(100); - let mut coin_tree_data = vec![]; - - coin_tree_data.write_u32(0)?; - coin_tree.encode(&mut coin_tree_data)?; - - db_set(info_db, &serialize(&MONEY_CONTRACT_COIN_MERKLE_TREE), &coin_tree_data)?; - info_db - } - }; - - // Whitelisted faucets - db_set(info_db, &serialize(&MONEY_CONTRACT_FAUCET_PUBKEYS), &serialize(&faucet_pubkeys))?; - - Ok(()) -} - -/// 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, calls): (u32, Vec) = deserialize(ix)?; - assert!(call_idx < calls.len() as u32); - - let self_ = &calls[call_idx as usize]; - - match MoneyFunction::try_from(self_.data[0])? { - 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..])?; - - let mut zk_public_values: Vec<(String, Vec)> = vec![]; - let mut signature_pubkeys: Vec = vec![]; - - 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(); - - zk_public_values.push(( - MONEY_CONTRACT_ZKAS_LEAD_MINT_NS_V1.to_string(), - vec![ - *value_coords.x(), - *value_coords.y(), - output.coin_pk_hash, - output.coin_commit_hash, - ], - )); - } - - let mut metadata = vec![]; - zk_public_values.encode(&mut metadata)?; - signature_pubkeys.encode(&mut metadata)?; - - // Using this, we pass the above data to the host. - set_return_data(&metadata)?; - Ok(()) - } - - MoneyFunction::Unstake => { - let params: MoneyUnstakeParams = deserialize(&self_.data[1..])?; - - let mut zk_public_values: Vec<(String, Vec)> = vec![]; - - for input in ¶ms.inputs { - let value_coords = input.value_commit.to_affine().coordinates().unwrap(); - zk_public_values.push(( - MONEY_CONTRACT_ZKAS_LEAD_BURN_NS_V1.to_string(), - vec![ - *value_coords.x(), - *value_coords.y(), - input.coin_pk_hash, - input.coin_commit_hash, - input.coin_commit_root.inner(), - input.sk_root.inner(), - input.nullifier.inner(), - ], - )); - } - - 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)?; - - // Using this, we pass the above data to the host. - set_return_data(&metadata)?; - Ok(()) - } - - MoneyFunction::Mint => { - 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 => { - msg!("[Freeze] Entered match arm"); - unimplemented!(); - } - } -} - -/// This function verifies a state transition and produces an -/// update if everything is successful. -#[cfg(not(feature = "no-entrypoint"))] -fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult { - let (call_idx, calls): (u32, Vec) = deserialize(ix)?; - - 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 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 update_data = money_otcswap_process_instruction(cid, call_idx, calls)?; - set_return_data(&update_data)?; - msg!("[OtcSwap] State update set!"); - Ok(()) - } - - MoneyFunction::Stake => { - msg!("[Stake] Entered match arm"); - let params: MoneyStakeParams = deserialize(&self_.data[1..])?; - - assert!(params.inputs.len() == params.outputs.len()); - - // Verify token commitment - let tokcom = pedersen_commitment_base(DARK_TOKEN_ID.inner(), params.token_blind); - if params.inputs.iter().any(|input| input.token_commit != tokcom) { - msg!("[Stake] Error: Tried to stake non-native token. Unable to proceed"); - return Err(ContractError::Custom(26)) - } - - let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_LEAD_NULLIFIERS_TREE)?; - let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_LEAD_COIN_ROOTS_TREE)?; - - // Accumulator for the value commitments - let mut valcom_total = pallas::Point::identity(); - - // State transition for payments - let mut new_nullifiers = Vec::with_capacity(params.inputs.len()); - - msg!("[Stake] 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!("[Stake] 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!("[Stake] Error: Duplicate nullifier found in input {}", i); - return Err(ContractError::Custom(22)) - } - - 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_commit_hash)) { - msg!("[Stake] Error: Duplicate coin found in output {}", i); - return Err(ContractError::Custom(23)) - } - new_coins.push(Coin::from(output.coin_commit_hash)); - 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!("[Stake] Error: Value commitments do not result in identity"); - return Err(ContractError::Custom(24)) - } - - // Create a state update - let update = MoneyStakeUpdate { nullifiers: new_nullifiers, coins: new_coins }; - let mut update_data = vec![]; - update_data.write_u8(MoneyFunction::Stake as u8)?; - update.encode(&mut update_data)?; - set_return_data(&update_data)?; - msg!("[Stake] State update set!"); - - Ok(()) - } - - MoneyFunction::Unstake => { - msg!("[Unstake] Entered match arm"); - let params: MoneyUnstakeParams = deserialize(&self_.data[1..])?; - - assert!(params.inputs.len() == params.outputs.len()); - - // Verify token commitment - let tokcom = pedersen_commitment_base(DARK_TOKEN_ID.inner(), params.token_blind); - if params.outputs.iter().any(|output| output.token_commit != tokcom) { - msg!("[Stake] Error: Tried to unstake non-native token. Unable to proceed"); - return Err(ContractError::Custom(26)) - } - - let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_LEAD_NULLIFIERS_TREE)?; - let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_LEAD_COIN_ROOTS_TREE)?; - //let sk_roots_db = db_lookup(cid, MONEY_CONTRACT_LEAD_SK_ROOTS_TREE)?; - - // Accumulator for the value commitments - let mut valcom_total = pallas::Point::identity(); - - // State transition for payments - let mut new_nullifiers = Vec::with_capacity(params.inputs.len()); - - msg!("[Stake] 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.coin_commit_root))? { - msg!("[Unstake] Error: Merkle root not found in previous state (input {})", i); - return Err(ContractError::Custom(21)) - } - - //TODO adde sk root to db. - /* - if !db_contains_key(sk_roots_db, &serialize(&input.sk_root))? { - msg!("[Unstake] Error: sk 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!("[Unstake] Error: Duplicate nullifier found in input {}", i); - return Err(ContractError::Custom(22)) - } - - 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!("[Unstake] Error: Duplicate coin found in output {}", i); - return Err(ContractError::Custom(23)) - } - 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!("[UnStake] Error: Value commitments do not result in identity"); - return Err(ContractError::Custom(24)) - } - - // Create a state update - let update = MoneyStakeUpdate { nullifiers: new_nullifiers, coins: new_coins }; - let mut update_data = vec![]; - update_data.write_u8(MoneyFunction::Unstake as u8)?; - update.encode(&mut update_data)?; - set_return_data(&update_data)?; - msg!("[Unstake] State update set!"); - - Ok(()) - } - - MoneyFunction::Mint => { - msg!("[Mint] Entered match arm"); - let update_data = money_mint_process_instruction(cid, call_idx, calls)?; - set_return_data(&update_data)?; - msg!("[Mint] State update set!"); - Ok(()) - } - - MoneyFunction::Freeze => { - msg!("[Freeze] Entered match arm"); - unimplemented!(); - } - } -} - -#[cfg(not(feature = "no-entrypoint"))] -fn process_update(cid: ContractId, update_data: &[u8]) -> ContractResult { - match MoneyFunction::try_from(update_data[0])? { - MoneyFunction::Transfer => { - let update: MoneyTransferUpdate = deserialize(&update_data[1..])?; - money_transfer_process_update(cid, update)?; - Ok(()) - } - - MoneyFunction::OtcSwap => { - let update: MoneyTransferUpdate = deserialize(&update_data[1..])?; - money_otcswap_process_update(cid, update)?; - Ok(()) - } - - MoneyFunction::Stake | MoneyFunction::Unstake => { - let update: MoneyStakeUpdate = deserialize(&update_data[1..])?; - - let info_db = db_lookup(cid, MONEY_CONTRACT_LEAD_INFO_TREE)?; - let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_LEAD_NULLIFIERS_TREE)?; - let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_LEAD_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_LEAD_COIN_MERKLE_TREE), - &coins, - )?; - - Ok(()) - } - - MoneyFunction::Mint => { - let update: MoneyMintUpdate = deserialize(&update_data[1..])?; - money_mint_process_update(cid, update)?; - Ok(()) - } - - MoneyFunction::Freeze => { - msg!("[Freeze] Entered match arm"); - unimplemented!(); - } - } -} diff --git a/src/contract/money/src/model.rs b/src/contract/money/src/model.rs index 6e178eb23..0f7e11ee2 100644 --- a/src/contract/money/src/model.rs +++ b/src/contract/money/src/model.rs @@ -16,102 +16,13 @@ * along with this program. If not, see . */ -use darkfi_sdk::crypto::{ - pallas, Coin, MerkleNode, Nullifier, PublicKey, TokenId, ValueBlind, ValueCommit, +use darkfi_sdk::{ + crypto::{note::AeadEncryptedNote, Coin, MerkleNode, Nullifier, PublicKey, TokenId}, + pasta::pallas, }; 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 { - /// Anonymous inputs - pub inputs: Vec, - /// Anonymous outputs for staking - pub outputs: Vec, - /// Token blind to reveal token ID - pub token_blind: ValueBlind, -} - -/// Inputs and outputs for unstaking coins -#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] -pub struct MoneyUnstakeParams { - /// Anonymous staked inputs - pub inputs: Vec, - /// Anonymous outputs - pub outputs: Vec, - /// Token blind to reveal token ID - pub token_blind: ValueBlind, -} - -/// Staked anonymous input -#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] -pub struct StakedInput { - /// Revealed nullifier - pub nullifier: Nullifier, - /// Pedersen commitment for the output's value - pub value_commit: ValueCommit, - /// Minted coin - pub coin_commit_hash: pallas::Base, - /// coin pk hash - pub coin_pk_hash: pallas::Base, - /// coin commitment root - pub coin_commit_root: MerkleNode, - /// sk root of merkle tree - pub sk_root: MerkleNode, -} - -/// Staked anonymous output -#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] -pub struct StakedOutput { - /// Pedersen commitment for the output's value - pub value_commit: ValueCommit, - /// Minted coin - pub coin_commit_hash: pallas::Base, - /// coin pk hash - pub coin_pk_hash: pallas::Base, -} - -/// Inputs and outputs for a payment -#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] -pub struct MoneyTransferParams { - /// Clear inputs - pub clear_inputs: Vec, - /// Anonymous inputs - pub inputs: Vec, - /// Anonymous outputs - pub outputs: Vec, -} - -/// State update produced by a payment -#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] -pub struct MoneyTransferUpdate { - /// Revealed nullifiers - pub nullifiers: Vec, - /// Minted coins - pub coins: Vec, -} - -/// State update produced by a staking -#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] -pub struct MoneyStakeUpdate { - /// Revealed nullifiers - pub nullifiers: Vec, - /// Minted coins - pub coins: Vec, -} - -/// A transaction's clear input +/// A contract call's clear input #[derive(Clone, Debug, SerialEncodable, SerialDecodable)] pub struct ClearInput { /// Input's value (amount) @@ -119,20 +30,20 @@ pub struct ClearInput { /// Input's token ID pub token_id: TokenId, /// Blinding factor for `value` - pub value_blind: ValueBlind, + pub value_blind: pallas::Scalar, /// Blinding factor for `token_id` - pub token_blind: ValueBlind, + pub token_blind: pallas::Scalar, /// Public key for the signature pub signature_public: PublicKey, } -/// A transaction's anonymous input +/// A contract call's anonymous input #[derive(Clone, Debug, SerialEncodable, SerialDecodable)] pub struct Input { /// Pedersen commitment for the input's value - pub value_commit: ValueCommit, + pub value_commit: pallas::Point, /// Pedersen commitment for the input's token ID - pub token_commit: ValueCommit, + pub token_commit: pallas::Point, /// Revealed nullifier pub nullifier: Nullifier, /// Revealed Merkle root @@ -142,25 +53,73 @@ pub struct Input { /// must have this value as its ID. pub spend_hook: pallas::Base, /// Encrypted user data field. An encrypted commitment to arbitrary data. - /// When spend hook is set (it is nonzero), then this field may be used + /// When spend hook is set (it is nonzero), then this field may be user /// to pass data to the invoked contract. pub user_data_enc: pallas::Base, /// Public key for the signature pub signature_public: PublicKey, } -/// A transaction's anonymous output +/// A contract call's anonymous output #[derive(Clone, Debug, SerialEncodable, SerialDecodable)] pub struct Output { /// Pedersen commitment for the output's value - pub value_commit: ValueCommit, + pub value_commit: pallas::Point, /// Pedersen commitment for the output's token ID - pub token_commit: ValueCommit, + pub token_commit: pallas::Point, /// Minted coin pub coin: pallas::Base, - //pub coin: Coin, - /// The encrypted note ciphertext - pub ciphertext: Vec, - /// The ephemeral public key - pub ephem_public: PublicKey, + /// AEAD encrypted note + pub note: AeadEncryptedNote, +} + +/// Parameters for `Money::Transfer` and `Money::OtcSwap` +#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] +pub struct MoneyTransferParamsV1 { + /// Clear inputs + pub clear_inputs: Vec, + /// Anonymous inputs + pub inputs: Vec, + /// Anonymous outputs + pub outputs: Vec, +} + +/// State update for `Money::Transfer` and `Money::OtcSwap` +#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] +pub struct MoneyTransferUpdateV1 { + /// Revealed nullifiers + pub nullifiers: Vec, + /// Minted coins + pub coins: Vec, +} + +/// Parameters for `Money::Mint` +#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] +pub struct MoneyMintParamsV1 { + /// Clear input + pub input: ClearInput, + /// Anonymous output + pub output: Output, +} + +/// State update for `Money::Mint` +#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] +pub struct MoneyMintUpdateV1 { + /// The newly minted coin + pub coin: Coin, +} + +/// Parameters for `Money::Freeze` +#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] +pub struct MoneyFreezeParamsV1 { + /// Mint authority public key + /// We also use this to derive the token ID + pub signature_public: PublicKey, +} + +/// State update for `Money::Freeze` +#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] +pub struct MoneyFreezeUpdateV1 { + /// Mint authority public key + pub signature_public: PublicKey, } diff --git a/src/contract/money/src/swap.rs b/src/contract/money/src/swap.rs deleted file mode 100644 index 4998bf398..000000000 --- a/src/contract/money/src/swap.rs +++ /dev/null @@ -1,150 +0,0 @@ -/* 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 deleted file mode 100644 index 523889778..000000000 --- a/src/contract/money/src/transfer.rs +++ /dev/null @@ -1,251 +0,0 @@ -/* 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(()) -}