diff --git a/Cargo.lock b/Cargo.lock index daa6549b1..e86ba8db4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1230,6 +1230,25 @@ dependencies = [ "wasmer-middlewares", ] +[[package]] +name = "darkfi-consensus-contract" +version = "0.4.1" +dependencies = [ + "async-std", + "chacha20poly1305", + "darkfi", + "darkfi-money-contract", + "darkfi-sdk", + "darkfi-serial", + "getrandom", + "halo2_proofs", + "log", + "rand", + "simplelog", + "sled", + "thiserror", +] + [[package]] name = "darkfi-dao-contract" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index 7777d878e..c5c3b8f39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ members = [ "src/contract/money", "src/contract/dao", + "src/contract/consensus", "example/dchat", ] diff --git a/src/consensus/validator.rs b/src/consensus/validator.rs index 66885bcf5..c7c278722 100644 --- a/src/consensus/validator.rs +++ b/src/consensus/validator.rs @@ -22,7 +22,7 @@ use async_std::sync::{Arc, RwLock}; use darkfi_sdk::{ crypto::{ constants::MERKLE_DEPTH, - contract_id::{DAO_CONTRACT_ID, MONEY_CONTRACT_ID}, + contract_id::{CONSENSUS_CONTRACT_ID, DAO_CONTRACT_ID, MONEY_CONTRACT_ID}, schnorr::{SchnorrPublic, SchnorrSecret}, MerkleNode, PublicKey, SecretKey, }, @@ -150,6 +150,7 @@ impl ValidatorState { // in the money contract. let money_contract_deploy_payload = serialize(&faucet_pubkeys); let dao_contract_deploy_payload = vec![]; + let consensus_contract_deploy_payload = vec![]; let native_contracts = vec![ ( @@ -164,6 +165,12 @@ impl ValidatorState { include_bytes!("../contract/dao/dao_contract.wasm").to_vec(), dao_contract_deploy_payload, ), + ( + "Consensus Contract", + *CONSENSUS_CONTRACT_ID, + include_bytes!("../contract/consensus/consensus_contract.wasm").to_vec(), + consensus_contract_deploy_payload, + ), ]; info!(target: "consensus::validator", "Deploying native wasm contracts"); diff --git a/src/contract/README.md b/src/contract/README.md index 42c783f95..58540cdd9 100644 --- a/src/contract/README.md +++ b/src/contract/README.md @@ -8,3 +8,7 @@ This directory contains native WASM contracts on DarkFi. ## DAO * https://darkrenaissance.github.io/darkfi/development/darkfi_dao_contract/index.html + +## Consensus + +* TODO: add link here diff --git a/src/contract/consensus/.gitignore b/src/contract/consensus/.gitignore new file mode 100644 index 000000000..f4dbc9827 --- /dev/null +++ b/src/contract/consensus/.gitignore @@ -0,0 +1,2 @@ +consensus_contract.wasm +proof/*.zk.bin diff --git a/src/contract/consensus/Cargo.toml b/src/contract/consensus/Cargo.toml new file mode 100644 index 000000000..78858cdf6 --- /dev/null +++ b/src/contract/consensus/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "darkfi-consensus-contract" +version = "0.4.1" +authors = ["Dyne.org foundation "] +license = "AGPL-3.0-only" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +darkfi-sdk = { path = "../../sdk" } +darkfi-serial = { path = "../../serial", features = ["derive", "crypto"] } +darkfi-money-contract = { path = "../money", features = ["no-entrypoint"] } +thiserror = "1.0.38" + +# The following dependencies are used for the client API and +# probably shouldn't be in WASM +chacha20poly1305 = { version = "0.10.1", optional = true } +darkfi = { path = "../../../", features = ["zk", "rpc", "blockchain"], optional = true } +halo2_proofs = { version = "0.2.0", optional = true } +log = { version = "0.4.17", optional = true } +rand = { version = "0.8.5", optional = true } + +# These are used just for the integration tests +[dev-dependencies] +async-std = {version = "1.12.0", features = ["attributes"]} +darkfi = {path = "../../../", features = ["tx", "blockchain"]} +darkfi-money-contract = { path = "../money", features = ["client", "no-entrypoint"] } +simplelog = "0.12.0" +sled = "0.34.7" + +# We need to disable random using "custom" which makes the crate a noop +# so the wasm32-unknown-unknown target is enabled. +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2.8", features = ["custom"] } + +[features] +default = [] +no-entrypoint = [] +client = [ + "darkfi", + "darkfi-money-contract/client", + "darkfi-money-contract/no-entrypoint", + "rand", + "chacha20poly1305", + "log", + "halo2_proofs", +] diff --git a/src/contract/consensus/Makefile b/src/contract/consensus/Makefile new file mode 100644 index 000000000..785729131 --- /dev/null +++ b/src/contract/consensus/Makefile @@ -0,0 +1,41 @@ +.POSIX: + +# Cargo binary +CARGO = cargo + +# zkas compiler binary +ZKAS = ../../../zkas + +# zkas circuits +PROOFS_SRC = $(shell find proof -type f -name '*.zk') +PROOFS_BIN = $(PROOFS_SRC:=.bin) + +# wasm source files +WASM_SRC = \ + $(shell find src -type f) \ + $(shell find ../../sdk -type f) \ + $(shell find ../../serial -type f) + +# wasm contract binary +WASM_BIN = consensus_contract.wasm + +all: $(WASM_BIN) + +$(WASM_BIN): $(WASM_SRC) $(PROOFS_BIN) + $(CARGO) build --release --package darkfi-consensus-contract --target wasm32-unknown-unknown + cp -f ../../../target/wasm32-unknown-unknown/release/darkfi_consensus_contract.wasm $@ + +$(PROOFS_BIN): $(ZKAS) $(PROOFS_SRC) + $(ZKAS) $(basename $@) -o $@ + +test-stake-unstake: all + $(CARGO) test --release --features=no-entrypoint,client \ + --package darkfi-consensus-contract \ + --test stake_unstake + +test: test-stake-unstake + +clean: + rm -f $(PROOFS_BIN) $(WASM_BIN) + +.PHONY: all test-stake-unstake test clean diff --git a/src/contract/consensus/src/client/mod.rs b/src/contract/consensus/src/client/mod.rs new file mode 100644 index 000000000..97fc9a6cb --- /dev/null +++ b/src/contract/consensus/src/client/mod.rs @@ -0,0 +1,29 @@ +/* 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 API for 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. +//! +//! Note that this API does not involve any wallet interaction, but only takes +//! the necessary objects provided by the caller. This is intentional, so we +//! are able to abstract away any wallet interfaces to client implementations. + +/// `Consensus::StakeV1` API +pub mod stake_v1; diff --git a/src/contract/consensus/src/client/stake_v1.rs b/src/contract/consensus/src/client/stake_v1.rs new file mode 100644 index 000000000..5b8f2b06c --- /dev/null +++ b/src/contract/consensus/src/client/stake_v1.rs @@ -0,0 +1,19 @@ +/* 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 crufty. Please rework it into something nice to read and nice to use. diff --git a/src/contract/consensus/src/entrypoint.rs b/src/contract/consensus/src/entrypoint.rs new file mode 100644 index 000000000..b8e4808b9 --- /dev/null +++ b/src/contract/consensus/src/entrypoint.rs @@ -0,0 +1,119 @@ +/* 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, + db::{db_init, db_lookup, set_return_data, SMART_CONTRACT_ZKAS_DB_NAME}, + error::{ContractError, ContractResult}, + msg, ContractCall, +}; +use darkfi_serial::deserialize; + +use crate::{model::ConsensusStakeUpdateV1, ConsensusFunction}; + +/// `Consensus::Stake` functions +mod stake_v1; +use stake_v1::{ + consensus_stake_get_metadata_v1, consensus_stake_process_instruction_v1, + consensus_stake_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 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)?, + }; + + // TODO: implement + + 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 ConsensusFunction::try_from(calls[call_idx as usize].data[0])? { + ConsensusFunction::StakeV1 => { + // 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 = consensus_stake_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 ConsensusFunction::try_from(calls[call_idx as usize].data[0])? { + ConsensusFunction::StakeV1 => { + // 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 = consensus_stake_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 ConsensusFunction::try_from(update_data[0])? { + ConsensusFunction::StakeV1 => { + let update: ConsensusStakeUpdateV1 = deserialize(&update_data[1..])?; + Ok(consensus_stake_process_update_v1(cid, update)?) + } + } +} diff --git a/src/contract/consensus/src/entrypoint/stake_v1.rs b/src/contract/consensus/src/entrypoint/stake_v1.rs new file mode 100644 index 000000000..eb46b748b --- /dev/null +++ b/src/contract/consensus/src/entrypoint/stake_v1.rs @@ -0,0 +1,81 @@ +/* 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, PublicKey}, + error::{ContractError, ContractResult}, + pasta::pallas, + ContractCall, +}; +use darkfi_serial::{deserialize, Encodable, WriteExt}; + +use crate::{ + model::{ConsensusStakeParamsV1, ConsensusStakeUpdateV1}, + ConsensusFunction, +}; + +/// `get_metadata` function for `Consensus::StakeV1` +pub(crate) fn consensus_stake_get_metadata_v1( + _cid: ContractId, + call_idx: u32, + calls: Vec, +) -> Result, ContractError> { + let self_ = &calls[call_idx as usize]; + let _params: ConsensusStakeParamsV1 = deserialize(&self_.data[1..])?; + + // Public inputs for the ZK proofs we have to verify + let zk_public_inputs: Vec<(String, Vec)> = vec![]; + // Public keys for the transaction signatures we have to verify + let signature_pubkeys: Vec = vec![]; + + // TODO: implement + + // 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 `Consensus::StakeV1` +pub(crate) fn consensus_stake_process_instruction_v1( + _cid: ContractId, + _call_idx: u32, + _calls: Vec, +) -> Result, ContractError> { + // TODO: implement + + // Create a state update. + let update = ConsensusStakeUpdateV1 {}; + let mut update_data = vec![]; + update_data.write_u8(ConsensusFunction::StakeV1 as u8)?; + update.encode(&mut update_data)?; + + Ok(update_data) +} + +/// `process_update` function for `Consensus::StakeV1` +pub(crate) fn consensus_stake_process_update_v1( + _cid: ContractId, + _update: ConsensusStakeUpdateV1, +) -> ContractResult { + // TODO: implement + + Ok(()) +} diff --git a/src/contract/consensus/src/error.rs b/src/contract/consensus/src/error.rs new file mode 100644 index 000000000..318651cdf --- /dev/null +++ b/src/contract/consensus/src/error.rs @@ -0,0 +1,33 @@ +/* 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 ConsensusError { + #[error("Error")] + SomeError, +} + +impl From for ContractError { + fn from(e: ConsensusError) -> Self { + match e { + ConsensusError::SomeError => Self::Custom(1), + } + } +} diff --git a/src/contract/consensus/src/lib.rs b/src/contract/consensus/src/lib.rs new file mode 100644 index 000000000..56d95ef1d --- /dev/null +++ b/src/contract/consensus/src/lib.rs @@ -0,0 +1,60 @@ +/* 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 . + */ + +//! Smart contract implementing staking, unstaking and evolving +//! of consensus tokens. + +//! Smart contract implementing money transfers, atomic swaps, token +//! minting and freezing, and staking/unstaking of consensus tokens. + +use darkfi_sdk::error::ContractError; + +/// Functions available in the contract +#[repr(u8)] +pub enum ConsensusFunction { + StakeV1 = 0x00, + //EvolveV1 = 0x01, + //UnstakeV1 = 0x02, +} + +impl TryFrom for ConsensusFunction { + type Error = ContractError; + + fn try_from(b: u8) -> core::result::Result { + match b { + 0x00 => Ok(Self::StakeV1), + //0x01 => Ok(Self::EvolveV1), + //0x02 => Ok(Self::UnstakeV1), + _ => Err(ContractError::InvalidFunction), + } + } +} + +/// Internal contract errors +pub mod error; + +/// Call parameters definitions +pub mod model; + +#[cfg(not(feature = "no-entrypoint"))] +/// WASM entrypoint functions +pub mod entrypoint; + +#[cfg(feature = "client")] +/// Client API for interaction with this smart contract +pub mod client; diff --git a/src/contract/consensus/src/model.rs b/src/contract/consensus/src/model.rs new file mode 100644 index 000000000..320e385a4 --- /dev/null +++ b/src/contract/consensus/src/model.rs @@ -0,0 +1,31 @@ +/* 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_serial::{SerialDecodable, SerialEncodable}; + +/// Parameters for `Consensus::Stake` +#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] +pub struct ConsensusStakeParamsV1 { + // TODO: implement +} + +/// State update for `Consensus::Stake` +#[derive(Clone, Debug, SerialEncodable, SerialDecodable)] +pub struct ConsensusStakeUpdateV1 { + // TODO: implement +} diff --git a/src/contract/consensus/tests/harness.rs b/src/contract/consensus/tests/harness.rs new file mode 100644 index 000000000..e18afd742 --- /dev/null +++ b/src/contract/consensus/tests/harness.rs @@ -0,0 +1,193 @@ +/* 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 std::collections::HashMap; + +use darkfi::{ + consensus::{ + ValidatorState, ValidatorStatePtr, TESTNET_BOOTSTRAP_TIMESTAMP, TESTNET_GENESIS_HASH_BYTES, + TESTNET_GENESIS_TIMESTAMP, TESTNET_INITIAL_DISTRIBUTION, + }, + tx::Transaction, + wallet::{WalletDb, WalletPtr}, + zk::{empty_witnesses, halo2::Field, ProvingKey, ZkCircuit}, + zkas::ZkBinary, + Result, +}; +use darkfi_money_contract::client::OwnCoin; +use darkfi_sdk::{ + crypto::{Keypair, MerkleTree, PublicKey, DARK_TOKEN_ID, MONEY_CONTRACT_ID}, + db::SMART_CONTRACT_ZKAS_DB_NAME, + pasta::pallas, + ContractCall, +}; +use darkfi_serial::{serialize, Encodable}; +use log::warn; +use rand::rngs::OsRng; + +use darkfi_money_contract::{ + client::transfer_v1::TransferCallBuilder, model::MoneyTransferParamsV1, MoneyFunction, + 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, +}; + +pub fn init_logger() { + let mut cfg = simplelog::ConfigBuilder::new(); + cfg.add_filter_ignore("sled".to_string()); + cfg.add_filter_ignore("blockchain::contractstore".to_string()); + // We check this error so we can execute same file tests in parallel, + // otherwise second one fails to init logger here. + if let Err(_) = simplelog::TermLogger::init( + //simplelog::LevelFilter::Info, + simplelog::LevelFilter::Debug, + //simplelog::LevelFilter::Trace, + cfg.build(), + simplelog::TerminalMode::Mixed, + simplelog::ColorChoice::Auto, + ) { + warn!(target: "money_harness", "Logger already initialized"); + } +} + +pub struct Wallet { + pub keypair: Keypair, + pub state: ValidatorStatePtr, + pub merkle_tree: MerkleTree, + pub wallet: WalletPtr, + pub coins: Vec, + pub spent_coins: Vec, +} + +impl Wallet { + async fn new(keypair: Keypair, faucet_pubkeys: &[PublicKey]) -> Result { + let wallet = WalletDb::new("sqlite::memory:", "foo").await?; + let sled_db = sled::Config::new().temporary(true).open()?; + + let state = ValidatorState::new( + &sled_db, + *TESTNET_BOOTSTRAP_TIMESTAMP, + *TESTNET_GENESIS_TIMESTAMP, + *TESTNET_GENESIS_HASH_BYTES, + *TESTNET_INITIAL_DISTRIBUTION, + wallet.clone(), + faucet_pubkeys.to_vec(), + false, + false, + ) + .await?; + + let merkle_tree = MerkleTree::new(100); + + let coins = vec![]; + let spent_coins = vec![]; + + Ok(Self { keypair, state, merkle_tree, wallet, coins, spent_coins }) + } +} + +pub struct ConsensusTestHarness { + pub faucet: Wallet, + pub alice: Wallet, + pub bob: Wallet, + pub charlie: Wallet, + pub proving_keys: HashMap<&'static str, (ProvingKey, ZkBinary)>, +} + +impl ConsensusTestHarness { + pub async fn new() -> Result { + let faucet_kp = Keypair::random(&mut OsRng); + let faucet_pubkeys = vec![faucet_kp.public]; + let faucet = Wallet::new(faucet_kp, &faucet_pubkeys).await?; + + let alice_kp = Keypair::random(&mut OsRng); + let alice = Wallet::new(alice_kp, &faucet_pubkeys).await?; + + let bob_kp = Keypair::random(&mut OsRng); + let bob = Wallet::new(bob_kp, &faucet_pubkeys).await?; + + let charlie_kp = Keypair::random(&mut OsRng); + let charlie = Wallet::new(charlie_kp, &faucet_pubkeys).await?; + + // Get the zkas circuits and build proving keys + let mut proving_keys = HashMap::new(); + let alice_sled = alice.state.read().await.blockchain.sled_db.clone(); + let db_handle = alice.state.read().await.blockchain.contracts.lookup( + &alice_sled, + &MONEY_CONTRACT_ID, + SMART_CONTRACT_ZKAS_DB_NAME, + )?; + + macro_rules! mkpk { + ($ns:expr) => { + let zkbin = db_handle.get(&serialize(&$ns))?.unwrap(); + let zkbin = ZkBinary::decode(&zkbin)?; + let witnesses = empty_witnesses(&zkbin); + let circuit = ZkCircuit::new(witnesses, zkbin.clone()); + let pk = ProvingKey::build(13, &circuit); + proving_keys.insert($ns, (pk, zkbin)); + }; + } + + mkpk!(MONEY_CONTRACT_ZKAS_MINT_NS_V1); + mkpk!(MONEY_CONTRACT_ZKAS_BURN_NS_V1); + mkpk!(MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1); + mkpk!(MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1); + + Ok(Self { faucet, alice, bob, charlie, proving_keys }) + } + + pub fn airdrop_native( + &self, + value: u64, + recipient: PublicKey, + ) -> Result<(Transaction, MoneyTransferParamsV1)> { + let (mint_pk, mint_zkbin) = self.proving_keys.get(&MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap(); + let (burn_pk, burn_zkbin) = self.proving_keys.get(&MONEY_CONTRACT_ZKAS_BURN_NS_V1).unwrap(); + + let builder = TransferCallBuilder { + keypair: self.faucet.keypair, + recipient, + value, + token_id: *DARK_TOKEN_ID, + rcpt_spend_hook: pallas::Base::zero(), + rcpt_user_data: pallas::Base::zero(), + rcpt_user_data_blind: pallas::Base::random(&mut OsRng), + change_spend_hook: pallas::Base::zero(), + change_user_data: pallas::Base::zero(), + change_user_data_blind: pallas::Base::random(&mut OsRng), + coins: vec![], + tree: self.faucet.merkle_tree.clone(), + mint_zkbin: mint_zkbin.clone(), + mint_pk: mint_pk.clone(), + burn_zkbin: burn_zkbin.clone(), + burn_pk: burn_pk.clone(), + clear_input: true, + }; + + let debris = builder.build()?; + + let mut data = vec![MoneyFunction::TransferV1 as u8]; + debris.params.encode(&mut data)?; + let calls = vec![ContractCall { contract_id: *MONEY_CONTRACT_ID, data }]; + let proofs = vec![debris.proofs]; + let mut tx = Transaction { calls, proofs, signatures: vec![] }; + let sigs = tx.create_sigs(&mut OsRng, &debris.signature_secrets)?; + tx.signatures = vec![sigs]; + + Ok((tx, debris.params)) + } +} diff --git a/src/contract/consensus/tests/stake_unstake.rs b/src/contract/consensus/tests/stake_unstake.rs new file mode 100644 index 000000000..1f8415599 --- /dev/null +++ b/src/contract/consensus/tests/stake_unstake.rs @@ -0,0 +1,78 @@ +/* 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 . + */ + +//! Integration test of consensus staking and unstaking for Alice. +//! +//! We first airdrop Alica native tokes, and then she can stake and unstake +//! them a couple of times. +//! +//! With this test, we want to confirm the consensus contract state +//! transitions work for a single party and are able to be verified. +//! +//! TODO: Malicious cases + +use darkfi::Result; +use darkfi_sdk::crypto::{merkle_prelude::*, poseidon_hash, Coin, MerkleNode, Nullifier}; +use log::info; + +use darkfi_money_contract::client::{MoneyNote, OwnCoin}; + +mod harness; +use harness::{init_logger, ConsensusTestHarness}; + +#[async_std::test] +async fn consensus_contract_stake_unstake() -> Result<()> { + init_logger(); + + const ALICE_AIRDROP: u64 = 1000; + + // Initialize harness + let mut th = ConsensusTestHarness::new().await?; + info!(target: "money", "[Faucet] ==================================================="); + info!(target: "money", "[Faucet] Building Money::Transfer params for Alice's airdrop"); + info!(target: "money", "[Faucet] ==================================================="); + let (airdrop_tx, airdrop_params) = th.airdrop_native(ALICE_AIRDROP, th.alice.keypair.public)?; + + info!(target: "money", "[Faucet] =========================="); + info!(target: "money", "[Faucet] Executing Alice airdrop tx"); + info!(target: "money", "[Faucet] =========================="); + th.faucet.state.read().await.verify_transactions(&[airdrop_tx.clone()], true).await?; + th.faucet.merkle_tree.append(&MerkleNode::from(airdrop_params.outputs[0].coin.inner())); + info!(target: "money", "[Alice] =========================="); + info!(target: "money", "[Alice] Executing Alice airdrop tx"); + info!(target: "money", "[Alice] =========================="); + th.alice.state.read().await.verify_transactions(&[airdrop_tx.clone()], true).await?; + th.alice.merkle_tree.append(&MerkleNode::from(airdrop_params.outputs[0].coin.inner())); + + assert!(th.faucet.merkle_tree.root(0).unwrap() == th.alice.merkle_tree.root(0).unwrap()); + + // Gather new owncoins + let mut owncoins = vec![]; + let leaf_position = th.alice.merkle_tree.witness().unwrap(); + let note: MoneyNote = airdrop_params.outputs[0].note.decrypt(&th.alice.keypair.secret)?; + owncoins.push(OwnCoin { + coin: Coin::from(airdrop_params.outputs[0].coin), + note: note.clone(), + secret: th.alice.keypair.secret, + nullifier: Nullifier::from(poseidon_hash([th.alice.keypair.secret.inner(), note.serial])), + leaf_position, + }); + + // Thanks for reading + Ok(()) +} diff --git a/src/sdk/src/crypto/contract_id.rs b/src/sdk/src/crypto/contract_id.rs index 63a12299f..9d0f735b9 100644 --- a/src/sdk/src/crypto/contract_id.rs +++ b/src/sdk/src/crypto/contract_id.rs @@ -36,6 +36,10 @@ lazy_static! { /// Contract ID for the native DAO contract pub static ref DAO_CONTRACT_ID: ContractId = ContractId::from(poseidon_hash([pallas::Base::zero(), pallas::Base::from(1)])); + + /// Contract ID for the native Consensus contract + pub static ref CONSENSUS_CONTRACT_ID: ContractId = + ContractId::from(poseidon_hash([pallas::Base::zero(), pallas::Base::from(2)])); } /// ContractId represents an on-chain identifier for a certain smart contract.