contract/money: Refactor and cleanup.

Tests probably broken.
This commit is contained in:
parazyd
2023-02-22 22:03:48 +01:00
parent 65865fc52a
commit e9ffa10541
14 changed files with 1071 additions and 2530 deletions

View File

@@ -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

View File

@@ -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);
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
//! 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<MoneyMintParams> {
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<Proof> {
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)
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<PublicKey> = 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<ContractCall>) = 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<ContractCall>) = 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)?)
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ContractCall>,
) -> Result<Vec<u8>, 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<pallas::Base>)> = vec![];
// Public keys for the transaction signatures we have to verify
let signature_pubkeys: Vec<PublicKey> = 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<ContractCall>,
) -> Result<Vec<u8>, 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(())
}

View File

@@ -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<ContractCall>,
) -> Result<Vec<u8>, 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<pallas::Base>)> = vec![];
// Public inputs for the ZK proofs we have to verify
let mut zk_public_inputs: Vec<(String, Vec<pallas::Base>)> = vec![];
// Public keys for the transaction signatures we have to verify
let mut signature_pubkeys: Vec<PublicKey> = 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<ContractCall>,
) -> Result<Vec<u8>, 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(&params.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(())

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ContractCall>,
) -> Result<Vec<u8>, 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<ContractCall>,
) -> Result<Vec<u8>, 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)?)
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ContractCall>,
) -> Result<Vec<u8>, 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<pallas::Base>)> = vec![];
// Public keys for the transaction signatures we have to verify
let mut signature_pubkeys: Vec<PublicKey> = vec![];
// Take all the pubkeys from any clear inputs
for input in &params.clear_inputs {
signature_pubkeys.push(input.signature_public);
}
// Grab the pedersen commitments and signature pubkeys from the
// anonymous inputs
for input in &params.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 &params.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<ContractCall>,
) -> Result<Vec<u8>, 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<PublicKey> = 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(())
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<MoneyError> 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),
}
}
}

View File

@@ -16,98 +16,59 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#[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<u8> for MoneyFunction {
type Error = ContractError;
fn try_from(b: u8) -> core::result::Result<MoneyFunction, Self::Error> {
fn try_from(b: u8) -> core::result::Result<Self, Self::Error> {
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<PublicKey> = 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<ContractCall>) = 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<pallas::Base>)> = vec![];
let mut signature_pubkeys: Vec<PublicKey> = vec![];
for input in &params.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 &params.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<pallas::Base>)> = vec![];
for input in &params.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 &params.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<ContractCall>) = 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!();
}
}
}

View File

@@ -16,102 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<Input>,
/// Anonymous outputs for staking
pub outputs: Vec<StakedOutput>,
/// 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<StakedInput>,
/// Anonymous outputs
pub outputs: Vec<Output>,
/// 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<ClearInput>,
/// Anonymous inputs
pub inputs: Vec<Input>,
/// Anonymous outputs
pub outputs: Vec<Output>,
}
/// State update produced by a payment
#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
pub struct MoneyTransferUpdate {
/// Revealed nullifiers
pub nullifiers: Vec<Nullifier>,
/// Minted coins
pub coins: Vec<Coin>,
}
/// State update produced by a staking
#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
pub struct MoneyStakeUpdate {
/// Revealed nullifiers
pub nullifiers: Vec<Nullifier>,
/// Minted coins
pub coins: Vec<Coin>,
}
/// 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<u8>,
/// 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<ClearInput>,
/// Anonymous inputs
pub inputs: Vec<Input>,
/// Anonymous outputs
pub outputs: Vec<Output>,
}
/// State update for `Money::Transfer` and `Money::OtcSwap`
#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
pub struct MoneyTransferUpdateV1 {
/// Revealed nullifiers
pub nullifiers: Vec<Nullifier>,
/// Minted coins
pub coins: Vec<Coin>,
}
/// 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,
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ContractCall>,
) -> Result<Vec<u8>, ContractError> {
Ok(money_transfer_get_metadata(cid, call_idx, calls)?)
}
pub fn money_otcswap_process_instruction(
cid: ContractId,
call_idx: u32,
calls: Vec<ContractCall>,
) -> Result<Vec<u8>, 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)?)
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ContractCall>,
) -> Result<Vec<u8>, ContractError> {
let self_ = &calls[call_idx as usize];
let params: MoneyTransferParams = deserialize(&self_.data[1..])?;
let mut zk_public_values: Vec<(String, Vec<pallas::Base>)> = vec![];
let mut signature_pubkeys: Vec<PublicKey> = vec![];
for input in &params.clear_inputs {
signature_pubkeys.push(input.signature_public);
}
for input in &params.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 &params.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<ContractCall>,
) -> Result<Vec<u8>, 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<PublicKey> = 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(())
}