ソースを参照

contract/consensus: laid foundation

aggstam 3 年 前
コミット
4d294d1947

+ 19 - 0
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"

+ 1 - 0
Cargo.toml

@@ -48,6 +48,7 @@ members = [
 
     "src/contract/money",
     "src/contract/dao",
+    "src/contract/consensus",
 
     "example/dchat",
 ]

+ 8 - 1
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");

+ 4 - 0
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

+ 2 - 0
src/contract/consensus/.gitignore

@@ -0,0 +1,2 @@
+consensus_contract.wasm
+proof/*.zk.bin

+ 49 - 0
src/contract/consensus/Cargo.toml

@@ -0,0 +1,49 @@
+[package]
+name = "darkfi-consensus-contract"
+version = "0.4.1"
+authors = ["Dyne.org foundation <foundation@dyne.org>"]
+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",
+]

+ 41 - 0
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

+ 29 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+
+//! 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;

+ 19 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+
+//! This API is crufty. Please rework it into something nice to read and nice to use.

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

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

+ 33 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi_sdk::error::ContractError;
+
+#[derive(Debug, Clone, thiserror::Error)]
+pub enum ConsensusError {
+    #[error("Error")]
+    SomeError,
+}
+
+impl From<ConsensusError> for ContractError {
+    fn from(e: ConsensusError) -> Self {
+        match e {
+            ConsensusError::SomeError => Self::Custom(1),
+        }
+    }
+}

+ 60 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+
+//! 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<u8> for ConsensusFunction {
+    type Error = ContractError;
+
+    fn try_from(b: u8) -> core::result::Result<Self, Self::Error> {
+        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;

+ 31 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+
+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
+}

+ 193 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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<OwnCoin>,
+    pub spent_coins: Vec<OwnCoin>,
+}
+
+impl Wallet {
+    async fn new(keypair: Keypair, faucet_pubkeys: &[PublicKey]) -> Result<Self> {
+        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<Self> {
+        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))
+    }
+}

+ 78 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+
+//! 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(())
+}

+ 4 - 0
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.