Browse Source

contract/consensus: GenesisStake contract call added

aggstam 3 years ago
parent
commit
4d7617ad59

+ 7 - 2
src/contract/consensus/Makefile

@@ -33,9 +33,14 @@ test-stake-unstake: all
 		--package darkfi-consensus-contract \
 		--test stake_unstake
 
-test: test-stake-unstake
+test-genesis-stake-unstake: all
+	$(CARGO) test --release --features=no-entrypoint,client \
+		--package darkfi-consensus-contract \
+		--test genesis_stake_unstake
+
+test: test-genesis-stake-unstake test-stake-unstake
 
 clean:
 	rm -f $(PROOFS_BIN) $(WASM_BIN)
 
-.PHONY: all test-stake-unstake test clean
+.PHONY: all test-genesis-stake-unstake test-stake-unstake test clean

+ 147 - 0
src/contract/consensus/src/client/genesis_stake_v1.rs

@@ -0,0 +1,147 @@
+/* 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.
+
+use darkfi::{
+    zk::{Proof, ProvingKey},
+    zkas::ZkBinary,
+    Result,
+};
+use darkfi_money_contract::{
+    client::{transfer_v1::TransactionBuilderClearInputInfo, MoneyNote},
+    model::{ClearInput, Output},
+};
+use darkfi_sdk::{
+    crypto::{
+        note::AeadEncryptedNote, pasta_prelude::*, Keypair, PublicKey, CONSENSUS_CONTRACT_ID,
+        DARK_TOKEN_ID,
+    },
+    pasta::pallas,
+};
+use log::{debug, info};
+use rand::rngs::OsRng;
+
+use crate::{
+    client::stake_v1::{create_stake_mint_proof, TransactionBuilderOutputInfo},
+    model::ConsensusGenesisStakeParamsV1,
+};
+
+pub struct ConsensusGenesisStakeCallDebris {
+    pub params: ConsensusGenesisStakeParamsV1,
+    pub proofs: Vec<Proof>,
+}
+
+/// Struct holding necessary information to build a `Consensus::GenesisStakeV1` contract call.
+pub struct ConsensusGenesisStakeCallBuilder {
+    /// Caller's keypair
+    pub keypair: Keypair,
+    /// Amount of tokens we want to mint and stake
+    pub amount: u64,
+    /// `Mint_V1` zkas circuit ZkBinary
+    pub mint_zkbin: ZkBinary,
+    /// Proving key for the `Mint_V1` zk circuit
+    pub mint_pk: ProvingKey,
+}
+
+impl ConsensusGenesisStakeCallBuilder {
+    pub fn build(&self) -> Result<ConsensusGenesisStakeCallDebris> {
+        debug!("Building Consensus::GenesisStakeV1 contract call");
+        assert!(self.amount != 0);
+
+        // In this call, we will build one clear input and one anonymous output.
+        // Only DARK_TOKEN_ID can be minted and staked on genesis slot.
+        let token_id = *DARK_TOKEN_ID;
+
+        let input = TransactionBuilderClearInputInfo {
+            value: self.amount,
+            token_id,
+            signature_secret: self.keypair.secret,
+        };
+
+        let output = TransactionBuilderOutputInfo {
+            value: self.amount,
+            token_id,
+            public_key: self.keypair.public,
+        };
+
+        // We just create the pedersen commitment blinds here. We simply
+        // enforce that the clear input and the anon output have the same
+        // commitments. Not sure if this can be avoided, but also is it
+        // really necessary to avoid?
+        let value_blind = pallas::Scalar::random(&mut OsRng);
+        let token_blind = pallas::Scalar::random(&mut OsRng);
+
+        let c_input = ClearInput {
+            value: input.value,
+            token_id: input.token_id,
+            value_blind,
+            token_blind,
+            signature_public: PublicKey::from_secret(input.signature_secret),
+        };
+
+        let serial = pallas::Base::random(&mut OsRng);
+        let spend_hook = CONSENSUS_CONTRACT_ID.inner();
+        let user_data = pallas::Base::random(&mut OsRng);
+        let coin_blind = pallas::Base::random(&mut OsRng);
+
+        info!("Creating genesis stake mint proof for output");
+        let (proof, public_inputs) = create_stake_mint_proof(
+            &self.mint_zkbin,
+            &self.mint_pk,
+            &output,
+            value_blind,
+            token_blind,
+            serial,
+            spend_hook,
+            user_data,
+            coin_blind,
+        )?;
+
+        // Encrypted note
+        let note = MoneyNote {
+            serial,
+            value: output.value,
+            token_id: output.token_id,
+            spend_hook,
+            user_data,
+            coin_blind,
+            value_blind,
+            token_blind,
+            memo: vec![],
+        };
+
+        let encrypted_note = AeadEncryptedNote::encrypt(&note, &output.public_key, &mut OsRng)?;
+
+        let output = Output {
+            value_commit: public_inputs.value_commit,
+            token_commit: public_inputs.token_commit,
+            coin: public_inputs.coin,
+            note: encrypted_note,
+        };
+
+        // We now fill this with necessary stuff
+        let params = ConsensusGenesisStakeParamsV1 { input: c_input, output };
+        let proofs = vec![proof];
+
+        // Now we should have all the params and zk proof.
+        // We return it all and let the caller deal with it.
+        let debris = ConsensusGenesisStakeCallDebris { params, proofs };
+        Ok(debris)
+    }
+}

+ 3 - 0
src/contract/consensus/src/client/mod.rs

@@ -25,6 +25,9 @@
 //! the necessary objects provided by the caller. This is intentional, so we
 //! are able to abstract away any wallet interfaces to client implementations.
 
+/// `Consensus::GenesisStakeV1` API
+pub mod genesis_stake_v1;
+
 /// `Consensus::StakeV1` API
 pub mod stake_v1;
 

+ 21 - 2
src/contract/consensus/src/entrypoint.rs

@@ -34,6 +34,12 @@ use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
 
 use crate::{model::ConsensusProposalRewardUpdateV1, ConsensusFunction};
 
+/// `Consensus::GenesisStake` functions
+mod genesis_stake_v1;
+use genesis_stake_v1::{
+    consensus_genesis_stake_get_metadata_v1, consensus_genesis_stake_process_instruction_v1,
+};
+
 /// `Consensus::Stake` functions
 mod stake_v1;
 use stake_v1::{
@@ -153,11 +159,15 @@ fn get_metadata(cid: ContractId, ix: &[u8]) -> ContractResult {
     }
 
     match ConsensusFunction::try_from(calls[call_idx as usize].data[0])? {
-        ConsensusFunction::StakeV1 => {
+        ConsensusFunction::GenesisStakeV1 => {
             // 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_genesis_stake_get_metadata_v1(cid, call_idx, calls)?;
+            Ok(set_return_data(&metadata)?)
+        }
+        ConsensusFunction::StakeV1 => {
             let metadata = consensus_stake_get_metadata_v1(cid, call_idx, calls)?;
             Ok(set_return_data(&metadata)?)
         }
@@ -191,12 +201,16 @@ fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult {
     }
 
     match ConsensusFunction::try_from(calls[call_idx as usize].data[0])? {
-        ConsensusFunction::StakeV1 => {
+        ConsensusFunction::GenesisStakeV1 => {
             // 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_genesis_stake_process_instruction_v1(cid, call_idx, calls)?;
+            Ok(set_return_data(&update_data)?)
+        }
+        ConsensusFunction::StakeV1 => {
             let update_data = consensus_stake_process_instruction_v1(cid, call_idx, calls)?;
             Ok(set_return_data(&update_data)?)
         }
@@ -226,6 +240,11 @@ fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult {
 /// 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::GenesisStakeV1 => {
+            // GenesisStake uses the same update as normal Stake
+            let update: ConsensusStakeUpdateV1 = deserialize(&update_data[1..])?;
+            Ok(consensus_stake_process_update_v1(cid, update)?)
+        }
         ConsensusFunction::StakeV1 => {
             let update: ConsensusStakeUpdateV1 = deserialize(&update_data[1..])?;
             Ok(consensus_stake_process_update_v1(cid, update)?)

+ 133 - 0
src/contract/consensus/src/entrypoint/genesis_stake_v1.rs

@@ -0,0 +1,133 @@
+/* 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_money_contract::{
+    error::MoneyError, model::ConsensusStakeUpdateV1, CONSENSUS_CONTRACT_COINS_TREE,
+    MONEY_CONTRACT_ZKAS_MINT_NS_V1,
+};
+use darkfi_sdk::{
+    crypto::{
+        pasta_prelude::*, pedersen_commitment_base, pedersen_commitment_u64, ContractId,
+        DARK_TOKEN_ID,
+    },
+    db::{db_contains_key, db_lookup},
+    error::ContractError,
+    msg,
+    pasta::pallas,
+    util::get_verifying_slot,
+    ContractCall,
+};
+use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
+
+use crate::{model::ConsensusGenesisStakeParamsV1, ConsensusFunction};
+
+/// `get_metadata` function for `Consensus::GenesisStakeV1`
+pub(crate) fn consensus_genesis_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: ConsensusGenesisStakeParamsV1 = 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![params.input.signature_public];
+
+    // Grab the pedersen commitment from the anonymous output
+    let output = &params.output;
+    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.inner(),
+            *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 `Consensus::GenesisStakeV1`
+pub(crate) fn consensus_genesis_stake_process_instruction_v1(
+    cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: ConsensusGenesisStakeParamsV1 = deserialize(&self_.data[1..])?;
+
+    // Verify this contract call is verified against on genesis slot(0).
+    let verifying_slot = get_verifying_slot();
+    if verifying_slot != 0 {
+        msg!("[GenesisStakeV1] Error: Call is executed for slot {}, not genesis", verifying_slot);
+        return Err(MoneyError::GenesisCallNonGenesisSlot.into())
+    }
+
+    // Only DARK_TOKEN_ID can be minted and staked on genesis slot.
+    if params.input.token_id != *DARK_TOKEN_ID {
+        msg!("[GenesisStakeV1] Error: Clear input used non-native token");
+        return Err(MoneyError::TransferClearInputNonNativeToken.into())
+    }
+
+    // Access the necessary databases where there is information to
+    // validate this state transition.
+    let consensus_coins_db = db_lookup(cid, CONSENSUS_CONTRACT_COINS_TREE)?;
+
+    // Check that the coin from the output hasn't existed before.
+    if db_contains_key(consensus_coins_db, &serialize(&params.output.coin))? {
+        msg!("[GenesisStakeV1] Error: Duplicate coin in output");
+        return Err(MoneyError::DuplicateCoin.into())
+    }
+
+    // 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!("[GenesisStakeV1] 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!("[GenesisStakeV1] Error: Token commitment mismatch");
+        return Err(MoneyError::TokenMismatch.into())
+    }
+
+    // Create a state update.
+    let update = ConsensusStakeUpdateV1 { coin: params.output.coin };
+    let mut update_data = vec![];
+    update_data.write_u8(ConsensusFunction::StakeV1 as u8)?;
+    update.encode(&mut update_data)?;
+
+    Ok(update_data)
+}

+ 2 - 2
src/contract/consensus/src/entrypoint/proposal_burn_v1.rs

@@ -149,8 +149,8 @@ pub(crate) fn consensus_proposal_burn_process_instruction_v1(
         return Err(MoneyError::UnstakeSpendHookNotConsensusContract.into())
     }
 
-    // Verify next call corresponds to Consensus::ProposalRewardV1 (0x02)
-    if next.data[0] != 0x02 {
+    // Verify next call corresponds to Consensus::ProposalRewardV1 (0x03)
+    if next.data[0] != 0x03 {
         msg!("[ConsensusProposalBurnV1] Error: Next call function mismatch");
         return Err(MoneyError::NextCallFunctionMissmatch.into())
     }

+ 2 - 2
src/contract/consensus/src/entrypoint/proposal_mint_v1.rs

@@ -142,8 +142,8 @@ pub(crate) fn consensus_proposal_mint_process_instruction_v1(
         return Err(MoneyError::UnstakePreviousCallNotConsensusContract.into())
     }
 
-    // Verify previous call corresponds to Consensus::ProposalRewardV1 (0x02)
-    if previous.data[0] != 0x02 {
+    // Verify previous call corresponds to Consensus::ProposalRewardV1 (0x03)
+    if previous.data[0] != 0x03 {
         msg!("[ConsensusProposalMintV1] Error: Previous call function mismatch");
         return Err(MoneyError::PreviousCallFunctionMissmatch.into())
     }

+ 4 - 4
src/contract/consensus/src/entrypoint/proposal_reward_v1.rs

@@ -182,8 +182,8 @@ pub(crate) fn consensus_proposal_reward_process_instruction_v1(
         return Err(MoneyError::UnstakePreviousCallNotConsensusContract.into())
     }
 
-    // Verify previous call corresponds to Consensus::ProposalBurnV1 (0x01)
-    if previous.data[0] != 0x01 {
+    // Verify previous call corresponds to Consensus::ProposalBurnV1 (0x02)
+    if previous.data[0] != 0x02 {
         msg!("[ConsensusProposalRewardV1] Error: Previous call function mismatch");
         return Err(MoneyError::PreviousCallFunctionMissmatch.into())
     }
@@ -217,8 +217,8 @@ pub(crate) fn consensus_proposal_reward_process_instruction_v1(
         return Err(MoneyError::StakeNextCallNotConsensusContract.into())
     }
 
-    // Verify next call corresponds to Consensus::ProposalMintV1 (0x03)
-    if next.data[0] != 0x03 {
+    // Verify next call corresponds to Consensus::ProposalMintV1 (0x04)
+    if next.data[0] != 0x04 {
         msg!("[ConsensusProposalRewardV1] Error: Next call function mismatch");
         return Err(MoneyError::NextCallFunctionMissmatch.into())
     }

+ 1 - 1
src/contract/consensus/src/entrypoint/unstake_v1.rs

@@ -50,7 +50,7 @@ pub(crate) fn consensus_unstake_get_metadata_v1(
     // 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![input.signature_public];
+    let signature_pubkeys = vec![input.signature_public];
 
     // Grab the pedersen commitments and signature pubkeys from the
     // anonymous input

+ 12 - 10
src/contract/consensus/src/lib.rs

@@ -24,11 +24,12 @@ use darkfi_sdk::error::ContractError;
 /// Functions available in the contract
 #[repr(u8)]
 pub enum ConsensusFunction {
-    StakeV1 = 0x00,
-    ProposalBurnV1 = 0x01,
-    ProposalRewardV1 = 0x02,
-    ProposalMintV1 = 0x03,
-    UnstakeV1 = 0x04,
+    GenesisStakeV1 = 0x00,
+    StakeV1 = 0x01,
+    ProposalBurnV1 = 0x02,
+    ProposalRewardV1 = 0x03,
+    ProposalMintV1 = 0x04,
+    UnstakeV1 = 0x05,
 }
 
 impl TryFrom<u8> for ConsensusFunction {
@@ -36,11 +37,12 @@ impl TryFrom<u8> for ConsensusFunction {
 
     fn try_from(b: u8) -> core::result::Result<Self, Self::Error> {
         match b {
-            0x00 => Ok(Self::StakeV1),
-            0x01 => Ok(Self::ProposalBurnV1),
-            0x02 => Ok(Self::ProposalRewardV1),
-            0x03 => Ok(Self::ProposalMintV1),
-            0x04 => Ok(Self::UnstakeV1),
+            0x00 => Ok(Self::GenesisStakeV1),
+            0x01 => Ok(Self::StakeV1),
+            0x02 => Ok(Self::ProposalBurnV1),
+            0x03 => Ok(Self::ProposalRewardV1),
+            0x04 => Ok(Self::ProposalMintV1),
+            0x05 => Ok(Self::UnstakeV1),
             _ => Err(ContractError::InvalidFunction),
         }
     }

+ 10 - 1
src/contract/consensus/src/model.rs

@@ -16,13 +16,22 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use darkfi_money_contract::model::{Input, Output, StakeInput};
+use darkfi_money_contract::model::{ClearInput, Input, Output, StakeInput};
 use darkfi_sdk::{
     crypto::{ecvrf::VrfProof, PublicKey},
     pasta::pallas,
 };
 use darkfi_serial::{SerialDecodable, SerialEncodable};
 
+/// Parameters for `Consensus::GenesisStake`
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub struct ConsensusGenesisStakeParamsV1 {
+    /// Clear input
+    pub input: ClearInput,
+    /// Anonymous output
+    pub output: Output,
+}
+
 /// Parameters for `Consensus::ProposalBurn`
 #[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
 pub struct ConsensusProposalBurnParamsV1 {

+ 214 - 0
src/contract/consensus/tests/genesis_stake_unstake.rs

@@ -0,0 +1,214 @@
+/* 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 genesis staking and unstaking for Alice.
+//!
+//! We first stake Alice some native tokes on genesis slot, and then she can
+//! propose 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.
+
+use darkfi::Result;
+use darkfi_sdk::crypto::{merkle_prelude::*, poseidon_hash, Coin, Nullifier};
+use log::info;
+
+use darkfi_consensus_contract::model::REWARD;
+use darkfi_money_contract::client::{MoneyNote, OwnCoin};
+
+mod harness;
+use harness::{init_logger, ConsensusTestHarness, Holder};
+
+#[async_std::test]
+async fn consensus_contract_genesis_stake_unstake() -> Result<()> {
+    init_logger();
+
+    // Some numbers we want to assert
+    const ALICE_INITIAL: u64 = 1000;
+
+    // Slot to verify against
+    let current_slot = 0;
+
+    // Initialize harness
+    let mut th = ConsensusTestHarness::new().await?;
+
+    // Now Alice can craate a genesis stake transaction to mint
+    // some staked coins
+    info!(target: "consensus", "[Alice] =========================");
+    info!(target: "consensus", "[Alice] Building genesis stake tx");
+    info!(target: "consensus", "[Alice] =========================");
+    let (genesis_stake_tx, genesis_stake_params) =
+        th.genesis_stake_native(Holder::Alice, ALICE_INITIAL)?;
+
+    // We are going to use alice genesis mint transaction to
+    // test some malicious cases.
+    info!(target: "consensus", "[Malicious] ===================================");
+    info!(target: "consensus", "[Malicious] Checking duplicate genesis stake tx");
+    info!(target: "consensus", "[Malicious] ===================================");
+    th.execute_erroneous_genesis_stake_native_txs(
+        Holder::Alice,
+        vec![genesis_stake_tx.clone(), genesis_stake_tx.clone()],
+        current_slot,
+        1,
+    )
+    .await?;
+
+    info!(target: "consensus", "[Malicious] =============================================");
+    info!(target: "consensus", "[Malicious] Checking genesis stake tx not on genesis slot");
+    info!(target: "consensus", "[Malicious] =============================================");
+    th.execute_erroneous_genesis_stake_native_txs(
+        Holder::Alice,
+        vec![genesis_stake_tx.clone()],
+        current_slot + 1,
+        1,
+    )
+    .await?;
+    info!(target: "consensus", "[Malicious] ===========================");
+    info!(target: "consensus", "[Malicious] Malicious test cases passed");
+    info!(target: "consensus", "[Malicious] ===========================");
+
+    info!(target: "consensus", "[Faucet] ================================");
+    info!(target: "consensus", "[Faucet] Executing Alice genesis stake tx");
+    info!(target: "consensus", "[Faucet] ================================");
+    th.execute_genesis_stake_native_tx(
+        Holder::Faucet,
+        genesis_stake_tx.clone(),
+        &genesis_stake_params,
+        current_slot,
+    )
+    .await?;
+
+    info!(target: "consensus", "[Alice] ================================");
+    info!(target: "consensus", "[Alice] Executing Alice genesis stake tx");
+    info!(target: "consensus", "[Alice] ================================");
+    th.execute_genesis_stake_native_tx(
+        Holder::Alice,
+        genesis_stake_tx,
+        &genesis_stake_params,
+        current_slot,
+    )
+    .await?;
+
+    assert!(
+        th.faucet.consensus_merkle_tree.root(0).unwrap() ==
+            th.alice.consensus_merkle_tree.root(0).unwrap()
+    );
+
+    // Gather new staked owncoin
+    let leaf_position = th.alice.consensus_merkle_tree.witness().unwrap();
+    let note: MoneyNote = genesis_stake_params.output.note.decrypt(&th.alice.keypair.secret)?;
+    let alice_staked_oc = OwnCoin {
+        coin: Coin::from(genesis_stake_params.output.coin),
+        note: note.clone(),
+        secret: th.alice.keypair.secret,
+        nullifier: Nullifier::from(poseidon_hash([th.alice.keypair.secret.inner(), note.serial])),
+        leaf_position,
+    };
+
+    // Verify values match
+    assert!(ALICE_INITIAL == alice_staked_oc.note.value);
+
+    // We simulate the proposal of genesis slot
+    let slot_checkpoint =
+        th.alice.state.read().await.blockchain.get_slot_checkpoints_by_slot(&[current_slot])?[0]
+            .clone()
+            .unwrap();
+
+    // With alice's current coin value she can become the slot proposer,
+    // so she creates a proposal transaction to burn her staked coin,
+    // reward herself and mint the new coin.
+    info!(target: "consensus", "[Alice] ====================");
+    info!(target: "consensus", "[Alice] Building proposal tx");
+    info!(target: "consensus", "[Alice] ====================");
+    let (proposal_tx, proposal_params) =
+        th.proposal(Holder::Alice, slot_checkpoint, alice_staked_oc.clone())?;
+
+    info!(target: "consensus", "[Faucet] ===========================");
+    info!(target: "consensus", "[Faucet] Executing Alice proposal tx");
+    info!(target: "consensus", "[Faucet] ===========================");
+    th.execute_proposal_tx(Holder::Faucet, proposal_tx.clone(), &proposal_params, current_slot)
+        .await?;
+
+    info!(target: "consensus", "[Alice] ===========================");
+    info!(target: "consensus", "[Alice] Executing Alice proposal tx");
+    info!(target: "consensus", "[Alice] ===========================");
+    th.execute_proposal_tx(Holder::Alice, proposal_tx, &proposal_params, current_slot).await?;
+
+    assert!(
+        th.faucet.consensus_merkle_tree.root(0).unwrap() ==
+            th.alice.consensus_merkle_tree.root(0).unwrap()
+    );
+
+    // Gather new staked owncoin which includes the reward
+    let leaf_position = th.alice.consensus_merkle_tree.witness().unwrap();
+    let note: MoneyNote = proposal_params.output.note.decrypt(&th.alice.keypair.secret)?;
+    let alice_rewarded_staked_oc = OwnCoin {
+        coin: Coin::from(proposal_params.output.coin),
+        note: note.clone(),
+        secret: th.alice.keypair.secret,
+        nullifier: Nullifier::from(poseidon_hash([th.alice.keypair.secret.inner(), note.serial])),
+        leaf_position,
+    };
+
+    // Verify values match
+    assert!((alice_staked_oc.note.value + REWARD) == alice_rewarded_staked_oc.note.value);
+
+    // Now Alice can unstake her owncoin
+    info!(target: "consensus", "[Alice] ===================");
+    info!(target: "consensus", "[Alice] Building unstake tx");
+    info!(target: "consensus", "[Alice] ===================");
+    let (unstake_tx, unstake_params) =
+        th.unstake_native(Holder::Alice, alice_rewarded_staked_oc.clone())?;
+
+    info!(target: "consensus", "[Faucet] ==========================");
+    info!(target: "consensus", "[Faucet] Executing Alice unstake tx");
+    info!(target: "consensus", "[Faucet] ==========================");
+    th.execute_unstake_native_tx(Holder::Faucet, unstake_tx.clone(), &unstake_params, current_slot)
+        .await?;
+
+    info!(target: "consensus", "[Alice] ==========================");
+    info!(target: "consensus", "[Alice] Executing Alice unstake tx");
+    info!(target: "consensus", "[Alice] ==========================");
+    th.execute_unstake_native_tx(Holder::Alice, unstake_tx, &unstake_params, current_slot).await?;
+
+    assert!(th.faucet.merkle_tree.root(0).unwrap() == th.alice.merkle_tree.root(0).unwrap());
+    assert!(
+        th.faucet.consensus_merkle_tree.root(0).unwrap() ==
+            th.alice.consensus_merkle_tree.root(0).unwrap()
+    );
+
+    // Gather new unstaked owncoin
+    let leaf_position = th.alice.merkle_tree.witness().unwrap();
+    let note: MoneyNote = unstake_params.output.note.decrypt(&th.alice.keypair.secret)?;
+    let alice_unstaked_oc = OwnCoin {
+        coin: Coin::from(unstake_params.output.coin),
+        note: note.clone(),
+        secret: th.alice.keypair.secret,
+        nullifier: Nullifier::from(poseidon_hash([th.alice.keypair.secret.inner(), note.serial])),
+        leaf_position,
+    };
+
+    // Verify values match
+    assert!(alice_rewarded_staked_oc.note.value == alice_unstaked_oc.note.value);
+
+    // Statistics
+    th.statistics();
+
+    // Thanks for reading
+    Ok(())
+}

+ 98 - 1
src/contract/consensus/tests/harness.rs

@@ -47,10 +47,11 @@ use rand::rngs::OsRng;
 
 use darkfi_consensus_contract::{
     client::{
+        genesis_stake_v1::ConsensusGenesisStakeCallBuilder,
         proposal_v1::ConsensusProposalCallBuilder, stake_v1::ConsensusStakeCallBuilder,
         unstake_v1::ConsensusUnstakeCallBuilder,
     },
-    model::ConsensusProposalMintParamsV1,
+    model::{ConsensusGenesisStakeParamsV1, ConsensusProposalMintParamsV1},
     ConsensusFunction,
 };
 use darkfi_money_contract::{
@@ -93,6 +94,7 @@ pub enum Holder {
 #[derive(Debug, Eq, Hash, PartialEq)]
 pub enum TxAction {
     Airdrop,
+    GenesisStake,
     Stake,
     Proposal,
     Unstake,
@@ -227,6 +229,7 @@ impl ConsensusTestHarness {
         // Build benchmarks map
         let mut tx_action_benchmarks = HashMap::new();
         tx_action_benchmarks.insert(TxAction::Airdrop, TxActionBenchmarks::new());
+        tx_action_benchmarks.insert(TxAction::GenesisStake, TxActionBenchmarks::new());
         tx_action_benchmarks.insert(TxAction::Stake, TxActionBenchmarks::new());
         tx_action_benchmarks.insert(TxAction::Proposal, TxActionBenchmarks::new());
         tx_action_benchmarks.insert(TxAction::Unstake, TxActionBenchmarks::new());
@@ -309,6 +312,100 @@ impl ConsensusTestHarness {
         Ok(())
     }
 
+    pub fn genesis_stake_native(
+        &mut self,
+        holder: Holder,
+        amount: u64,
+    ) -> Result<(Transaction, ConsensusGenesisStakeParamsV1)> {
+        let wallet = match holder {
+            Holder::Faucet => &self.faucet,
+            Holder::Alice => &self.alice,
+        };
+        let (mint_pk, mint_zkbin) = self.proving_keys.get(&MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
+        let tx_action_benchmark =
+            self.tx_action_benchmarks.get_mut(&TxAction::GenesisStake).unwrap();
+        let timer = Instant::now();
+
+        // Building Consensus::GenesisStake params
+        let genesis_stake_call_debris = ConsensusGenesisStakeCallBuilder {
+            keypair: wallet.keypair,
+            amount,
+            mint_zkbin: mint_zkbin.clone(),
+            mint_pk: mint_pk.clone(),
+        }
+        .build()?;
+        let (genesis_stake_params, genesis_stake_proofs) =
+            (genesis_stake_call_debris.params, genesis_stake_call_debris.proofs);
+
+        // Building genesis stake tx
+        let mut data = vec![ConsensusFunction::GenesisStakeV1 as u8];
+        genesis_stake_params.encode(&mut data)?;
+        let contract_call = ContractCall { contract_id: *CONSENSUS_CONTRACT_ID, data };
+        let calls = vec![contract_call];
+        let proofs = vec![genesis_stake_proofs];
+        let mut genesis_stake_tx = Transaction { calls, proofs, signatures: vec![] };
+        let sigs = genesis_stake_tx.create_sigs(&mut OsRng, &[wallet.keypair.secret])?;
+        genesis_stake_tx.signatures = vec![sigs];
+        tx_action_benchmark.creation_times.push(timer.elapsed());
+
+        // Calculate transaction sizes
+        let encoded: Vec<u8> = serialize(&genesis_stake_tx);
+        let size = ::std::mem::size_of_val(&*encoded);
+        tx_action_benchmark.sizes.push(size);
+        let base58 = bs58::encode(&encoded).into_string();
+        let size = ::std::mem::size_of_val(&*base58);
+        tx_action_benchmark.broadcasted_sizes.push(size);
+
+        Ok((genesis_stake_tx, genesis_stake_params))
+    }
+
+    pub async fn execute_genesis_stake_native_tx(
+        &mut self,
+        holder: Holder,
+        tx: Transaction,
+        params: &ConsensusGenesisStakeParamsV1,
+        slot: u64,
+    ) -> Result<()> {
+        let wallet = match holder {
+            Holder::Faucet => &mut self.faucet,
+            Holder::Alice => &mut self.alice,
+        };
+        let tx_action_benchmark =
+            self.tx_action_benchmarks.get_mut(&TxAction::GenesisStake).unwrap();
+        let timer = Instant::now();
+
+        let erroneous_txs =
+            wallet.state.read().await.verify_transactions(&[tx], slot, true).await?;
+        assert!(erroneous_txs.is_empty());
+        wallet.consensus_merkle_tree.append(&MerkleNode::from(params.output.coin.inner()));
+        tx_action_benchmark.verify_times.push(timer.elapsed());
+
+        Ok(())
+    }
+
+    pub async fn execute_erroneous_genesis_stake_native_txs(
+        &mut self,
+        holder: Holder,
+        txs: Vec<Transaction>,
+        slot: u64,
+        erroneous: usize,
+    ) -> Result<()> {
+        let wallet = match holder {
+            Holder::Faucet => &mut self.faucet,
+            Holder::Alice => &mut self.alice,
+        };
+        let tx_action_benchmark =
+            self.tx_action_benchmarks.get_mut(&TxAction::GenesisStake).unwrap();
+        let timer = Instant::now();
+
+        let erroneous_txs =
+            wallet.state.read().await.verify_transactions(&txs, slot, false).await?;
+        assert_eq!(erroneous_txs.len(), erroneous);
+        tx_action_benchmark.verify_times.push(timer.elapsed());
+
+        Ok(())
+    }
+
     pub fn stake_native(
         &mut self,
         holder: Holder,

+ 2 - 2
src/contract/consensus/tests/stake_unstake.rs

@@ -18,8 +18,8 @@
 
 //! 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.
+//! We first airdrop Alice native tokes, and then she can stake,
+//! propose 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.

+ 2 - 2
src/contract/money/src/entrypoint/stake_v1.rs

@@ -142,8 +142,8 @@ pub(crate) fn money_stake_process_instruction_v1(
         return Err(MoneyError::SpendHookMismatch.into())
     }
 
-    // Verify next call corresponds to Consensus::StakeV1 (0x00)
-    if next.data[0] != 0x00 {
+    // Verify next call corresponds to Consensus::StakeV1 (0x01)
+    if next.data[0] != 0x01 {
         msg!("[MoneyStakeV1] Error: Next call function mismatch");
         return Err(MoneyError::NextCallFunctionMissmatch.into())
     }

+ 4 - 4
src/contract/money/src/entrypoint/unstake_v1.rs

@@ -18,8 +18,8 @@
 
 use darkfi_sdk::{
     crypto::{
-        pasta_prelude::*, pedersen_commitment_base, ContractId, MerkleNode, PublicKey,
-        CONSENSUS_CONTRACT_ID, DARK_TOKEN_ID,
+        pasta_prelude::*, pedersen_commitment_base, ContractId, MerkleNode, CONSENSUS_CONTRACT_ID,
+        DARK_TOKEN_ID,
     },
     db::{db_contains_key, db_lookup, db_set},
     error::{ContractError, ContractResult},
@@ -138,8 +138,8 @@ pub(crate) fn money_unstake_process_instruction_v1(
         return Err(MoneyError::UnstakePreviousCallNotConsensusContract.into())
     }
 
-    // Verify previous call corresponds to Consensus::UnstakeV1 (0x04)
-    if previous.data[0] != 0x04 {
+    // Verify previous call corresponds to Consensus::UnstakeV1 (0x05)
+    if previous.data[0] != 0x05 {
         msg!("[MoneyUnstakeV1] Error: Previous call function mismatch");
         return Err(MoneyError::PreviousCallFunctionMissmatch.into())
     }