فهرست منبع

contract/consensus: added proposal contract calls and example transaction in test to verify it works.

In current state the proposal transaction consist of 3 parts: 1. burn staked token, 2. verify consensus parameters along with reward verification, 3. mint new staked token. In current impl, on step 2, we only verify the reward is correct. TODO: add consensus parameters check in step 2 proof.
aggstam 3 سال پیش
والد
کامیت
2c8da56b99

+ 27 - 0
src/contract/consensus/proof/reward_v1.zk

@@ -0,0 +1,27 @@
+constant "Reward_V1" {
+	EcFixedPointShort VALUE_COMMIT_VALUE,
+	EcFixedPoint VALUE_COMMIT_RANDOM,
+}
+
+contract "Reward_V1" {
+	# The value of the burnt coin
+	Base value,
+	# Random blinding factor for the value commitment
+	Scalar value_blind,
+}
+
+circuit "Reward_V1" {
+	# This is a dummy check for now.
+	# TODO: Here we will constrain all the consensus
+	# parameters for the slot.
+	# Pedersen commitment for coin's value
+	vcv = ec_mul_short(value, VALUE_COMMIT_VALUE);
+	vcr = ec_mul(value_blind, VALUE_COMMIT_RANDOM);
+	value_commit = ec_add(vcv, vcr);
+	# Since the value commit is a curve point, we fetch its coordinates
+	# and constrain them:
+	constrain_instance(ec_get_x(value_commit));
+	constrain_instance(ec_get_y(value_commit));
+
+	# At this point we've enforced all of our public inputs.
+}

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

@@ -28,5 +28,10 @@
 /// `Consensus::StakeV1` API
 pub mod stake_v1;
 
+/// Proposal transaction building API.
+/// This transaction is a chain of `Consensus::ProposalBurnV1`, `Consensus::ProposalRewardV1`
+/// and `Consensus::ProposalMintV1` contract calls.
+pub mod proposal_v1;
+
 /// `Consensus::UnstakeV1` API
 pub mod unstake_v1;

+ 250 - 0
src/contract/consensus/src/client/proposal_v1.rs

@@ -0,0 +1,250 @@
+/* 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::{halo2::Value, Proof, ProvingKey, Witness, ZkCircuit},
+    zkas::ZkBinary,
+    Result,
+};
+use darkfi_money_contract::{
+    client::{MoneyNote, OwnCoin},
+    model::{ConsensusStakeParamsV1, ConsensusUnstakeParamsV1, Input, Output, StakeInput},
+};
+use darkfi_sdk::{
+    crypto::{
+        note::AeadEncryptedNote, pasta_prelude::*, pedersen_commitment_u64, MerkleTree, PublicKey,
+        SecretKey, CONSENSUS_CONTRACT_ID, DARK_TOKEN_ID,
+    },
+    incrementalmerkletree::Tree,
+    pasta::pallas,
+};
+use log::{debug, info};
+use rand::rngs::OsRng;
+
+use crate::{
+    client::{
+        stake_v1::{create_stake_mint_proof, TransactionBuilderOutputInfo as StakeTBOI},
+        unstake_v1::{create_unstake_burn_proof, TransactionBuilderInputInfo as UnstakeTBII},
+    },
+    model::{ConsensusRewardParamsV1, REWARD},
+};
+
+pub struct ConsensusProposalCallDebris {
+    pub unstake_params: ConsensusUnstakeParamsV1,
+    pub unstake_proofs: Vec<Proof>,
+    pub reward_params: ConsensusRewardParamsV1,
+    pub reward_proofs: Vec<Proof>,
+    pub stake_params: ConsensusStakeParamsV1,
+    pub stake_proofs: Vec<Proof>,
+    pub signature_secret: SecretKey,
+}
+
+pub struct ConsensusProposalRewardRevealed {
+    pub value_commit: pallas::Point,
+}
+
+impl ConsensusProposalRewardRevealed {
+    pub fn to_vec(&self) -> Vec<pallas::Base> {
+        let value_coords = self.value_commit.to_affine().coordinates().unwrap();
+
+        // NOTE: It's important to keep these in the same order
+        // as the `constrain_instance` calls in the zkas code.
+        vec![*value_coords.x(), *value_coords.y()]
+    }
+}
+
+/// Struct holding necessary information to build a proposal transaction.
+pub struct ConsensusProposalCallBuilder {
+    /// `OwnCoin` we're given to use in this builder
+    pub coin: OwnCoin,
+    /// Recipient's public key
+    pub recipient: PublicKey,
+    /// Merkle tree of coins used to create inclusion proofs
+    pub tree: MerkleTree,
+    /// `Burn_V1` zkas circuit ZkBinary
+    pub burn_zkbin: ZkBinary,
+    /// Proving key for the `Burn_V1` zk circuit
+    pub burn_pk: ProvingKey,
+    /// `Reward_V1` zkas circuit ZkBinary
+    pub reward_zkbin: ZkBinary,
+    /// Proving key for the `Reward_V1` zk circuit
+    pub reward_pk: ProvingKey,
+    /// `Mint_V1` zkas circuit ZkBinary
+    pub mint_zkbin: ZkBinary,
+    /// Proving key for the `Mint_V1` zk circuit
+    pub mint_pk: ProvingKey,
+}
+
+impl ConsensusProposalCallBuilder {
+    pub fn build(&self) -> Result<ConsensusProposalCallDebris> {
+        debug!("Building Consensus::UnstakeV1 contract call for proposal");
+        let value = self.coin.note.value;
+        let token_id = self.coin.note.token_id;
+        assert!(value != 0);
+        assert!(token_id == *DARK_TOKEN_ID);
+
+        debug!("Building anonymous input for proposal");
+        let leaf_position = self.coin.leaf_position;
+        let root = self.tree.root(0).unwrap();
+        let merkle_path = self.tree.authentication_path(leaf_position, &root).unwrap();
+        let input = UnstakeTBII {
+            leaf_position,
+            merkle_path,
+            secret: self.coin.secret,
+            note: self.coin.note.clone(),
+        };
+        debug!("Finished building input for proposal");
+
+        let value_blind = pallas::Scalar::random(&mut OsRng);
+        let token_blind = pallas::Scalar::random(&mut OsRng);
+        let signature_secret = SecretKey::random(&mut OsRng);
+        let user_data_blind = pallas::Base::random(&mut OsRng);
+        info!("Creating unstake burn proof for input for proposal");
+        let (proof, public_inputs) = create_unstake_burn_proof(
+            &self.burn_zkbin,
+            &self.burn_pk,
+            &input,
+            value_blind,
+            token_blind,
+            user_data_blind,
+            signature_secret,
+        )?;
+
+        let input = Input {
+            value_commit: public_inputs.value_commit,
+            token_commit: public_inputs.token_commit,
+            nullifier: public_inputs.nullifier,
+            merkle_root: public_inputs.merkle_root,
+            spend_hook: public_inputs.spend_hook,
+            user_data_enc: public_inputs.user_data_enc,
+            signature_public: public_inputs.signature_public,
+        };
+
+        // We now fill this with necessary stuff
+        let unstake_params = ConsensusUnstakeParamsV1 { token_blind, input: input.clone() };
+        let unstake_proofs = vec![proof];
+        let unstake_input = input;
+
+        debug!("Building Consensus::StakeV1 contract call for proposal");
+        let new_value = value + REWARD;
+        let nullifier = public_inputs.nullifier;
+        let merkle_root = public_inputs.merkle_root;
+        let signature_public = public_inputs.signature_public;
+
+        debug!("Building anonymous output for proposal");
+        let output = StakeTBOI { value: new_value, token_id, public_key: self.recipient };
+        debug!("Finished building output for proposal");
+
+        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 stake mint proof for output for proposal");
+        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,
+        };
+
+        let input = StakeInput {
+            token_blind,
+            value_commit: public_inputs.value_commit,
+            nullifier,
+            merkle_root,
+            signature_public,
+        };
+
+        // We now fill this with necessary stuff
+        let stake_params = ConsensusStakeParamsV1 { input: input.clone(), output: output.clone() };
+        let stake_proofs = vec![proof];
+        let stake_input = input;
+
+        debug!("Building Consensus::RewardV1 contract call for proposal");
+        let (proof, _public_inputs) =
+            create_proposal_reward_proof(&self.reward_zkbin, &self.reward_pk, value, value_blind)?;
+
+        // We now fill this with necessary stuff
+        let reward_params = ConsensusRewardParamsV1 { unstake_input, stake_input, output };
+        let reward_proofs = vec![proof];
+
+        // Now we should have all the params, zk proofs and signature secret.
+        // We return it all and let the caller deal with it.
+        let debris = ConsensusProposalCallDebris {
+            unstake_params,
+            unstake_proofs,
+            reward_params,
+            reward_proofs,
+            stake_params,
+            stake_proofs,
+            signature_secret,
+        };
+        Ok(debris)
+    }
+}
+
+pub fn create_proposal_reward_proof(
+    zkbin: &ZkBinary,
+    pk: &ProvingKey,
+    value: u64,
+    value_blind: pallas::Scalar,
+) -> Result<(Proof, ConsensusProposalRewardRevealed)> {
+    let value_commit = pedersen_commitment_u64(value, value_blind);
+
+    let public_inputs = ConsensusProposalRewardRevealed { value_commit };
+
+    let prover_witnesses = vec![
+        Witness::Base(Value::known(pallas::Base::from(value))),
+        Witness::Scalar(Value::known(value_blind)),
+    ];
+
+    let circuit = ZkCircuit::new(prover_witnesses, zkbin.clone());
+    let proof = Proof::create(pk, &[circuit], &public_inputs.to_vec(), &mut OsRng)?;
+
+    Ok((proof, public_inputs))
+}

+ 2 - 2
src/contract/consensus/src/client/stake_v1.rs

@@ -88,9 +88,9 @@ pub struct ConsensusStakeCallBuilder {
     pub merkle_root: MerkleNode,
     /// Public key for the signature
     pub signature_public: PublicKey,
-    /// `Consensus_Mint_V1` zkas circuit ZkBinary
+    /// `Mint_V1` zkas circuit ZkBinary
     pub mint_zkbin: ZkBinary,
-    /// Proving key for the `Consensus_Mint_V1` zk circuit
+    /// Proving key for the `Mint_V1` zk circuit
     pub mint_pk: ProvingKey,
 }
 

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

@@ -21,7 +21,8 @@ use darkfi_money_contract::{
     CONSENSUS_CONTRACT_COINS_TREE, CONSENSUS_CONTRACT_COIN_MERKLE_TREE,
     CONSENSUS_CONTRACT_COIN_ROOTS_TREE, CONSENSUS_CONTRACT_DB_VERSION,
     CONSENSUS_CONTRACT_INFO_TREE, CONSENSUS_CONTRACT_NULLIFIERS_TREE,
-    MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
+    CONSENSUS_CONTRACT_ZKAS_REWARD_NS_V1, MONEY_CONTRACT_ZKAS_BURN_NS_V1,
+    MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 use darkfi_sdk::{
     crypto::{ContractId, MerkleTree},
@@ -31,7 +32,7 @@ use darkfi_sdk::{
 };
 use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
 
-use crate::ConsensusFunction;
+use crate::{model::ConsensusRewardUpdateV1, ConsensusFunction};
 
 /// `Consensus::Stake` functions
 mod stake_v1;
@@ -40,6 +41,27 @@ use stake_v1::{
     consensus_stake_process_update_v1,
 };
 
+/// `Consensus::ProposalBurn` functions
+mod proposal_burn_v1;
+use proposal_burn_v1::{
+    consensus_proposal_burn_get_metadata_v1, consensus_proposal_burn_process_instruction_v1,
+    consensus_proposal_burn_process_update_v1,
+};
+
+/// `Consensus::ProposalRewardV1` functions
+mod proposal_reward_v1;
+use proposal_reward_v1::{
+    consensus_proposal_reward_get_metadata_v1, consensus_proposal_reward_process_instruction_v1,
+    consensus_proposal_reward_process_update_v1,
+};
+
+/// `Consensus::ProposalMintV1` functions
+mod proposal_mint_v1;
+use proposal_mint_v1::{
+    consensus_proposal_mint_get_metadata_v1, consensus_proposal_mint_process_instruction_v1,
+    consensus_proposal_mint_process_update_v1,
+};
+
 /// `Consensus::Unstake` functions
 mod unstake_v1;
 use unstake_v1::{
@@ -71,8 +93,10 @@ fn init_contract(cid: ContractId, _ix: &[u8]) -> ContractResult {
 
     let money_mint_v1_bincode = include_bytes!("../../money/proof/mint_v1.zk.bin");
     let money_burn_v1_bincode = include_bytes!("../../money/proof/burn_v1.zk.bin");
+    let reward_v1_bincode = include_bytes!("../proof/reward_v1.zk.bin");
     db_set(zkas_db, &serialize(&MONEY_CONTRACT_ZKAS_MINT_NS_V1), &money_mint_v1_bincode[..])?;
     db_set(zkas_db, &serialize(&MONEY_CONTRACT_ZKAS_BURN_NS_V1), &money_burn_v1_bincode[..])?;
+    db_set(zkas_db, &serialize(&CONSENSUS_CONTRACT_ZKAS_REWARD_NS_V1), &reward_v1_bincode[..])?;
 
     // Set up a database tree to hold Merkle roots of all coins
     // k=MerkleNode, v=[]
@@ -139,6 +163,18 @@ fn get_metadata(cid: ContractId, ix: &[u8]) -> ContractResult {
             let metadata = consensus_stake_get_metadata_v1(cid, call_idx, calls)?;
             Ok(set_return_data(&metadata)?)
         }
+        ConsensusFunction::ProposalBurnV1 => {
+            let metadata = consensus_proposal_burn_get_metadata_v1(cid, call_idx, calls)?;
+            Ok(set_return_data(&metadata)?)
+        }
+        ConsensusFunction::ProposalRewardV1 => {
+            let metadata = consensus_proposal_reward_get_metadata_v1(cid, call_idx, calls)?;
+            Ok(set_return_data(&metadata)?)
+        }
+        ConsensusFunction::ProposalMintV1 => {
+            let metadata = consensus_proposal_mint_get_metadata_v1(cid, call_idx, calls)?;
+            Ok(set_return_data(&metadata)?)
+        }
         ConsensusFunction::UnstakeV1 => {
             let metadata = consensus_unstake_get_metadata_v1(cid, call_idx, calls)?;
             Ok(set_return_data(&metadata)?)
@@ -166,6 +202,19 @@ fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult {
             let update_data = consensus_stake_process_instruction_v1(cid, call_idx, calls)?;
             Ok(set_return_data(&update_data)?)
         }
+        ConsensusFunction::ProposalBurnV1 => {
+            let update_data = consensus_proposal_burn_process_instruction_v1(cid, call_idx, calls)?;
+            Ok(set_return_data(&update_data)?)
+        }
+        ConsensusFunction::ProposalRewardV1 => {
+            let update_data =
+                consensus_proposal_reward_process_instruction_v1(cid, call_idx, calls)?;
+            Ok(set_return_data(&update_data)?)
+        }
+        ConsensusFunction::ProposalMintV1 => {
+            let update_data = consensus_proposal_mint_process_instruction_v1(cid, call_idx, calls)?;
+            Ok(set_return_data(&update_data)?)
+        }
         ConsensusFunction::UnstakeV1 => {
             let update_data = consensus_unstake_process_instruction_v1(cid, call_idx, calls)?;
             Ok(set_return_data(&update_data)?)
@@ -183,6 +232,18 @@ fn process_update(cid: ContractId, update_data: &[u8]) -> ContractResult {
             let update: ConsensusStakeUpdateV1 = deserialize(&update_data[1..])?;
             Ok(consensus_stake_process_update_v1(cid, update)?)
         }
+        ConsensusFunction::ProposalBurnV1 => {
+            let update: ConsensusUnstakeUpdateV1 = deserialize(&update_data[1..])?;
+            Ok(consensus_proposal_burn_process_update_v1(cid, update)?)
+        }
+        ConsensusFunction::ProposalRewardV1 => {
+            let update: ConsensusRewardUpdateV1 = deserialize(&update_data[1..])?;
+            Ok(consensus_proposal_reward_process_update_v1(cid, update)?)
+        }
+        ConsensusFunction::ProposalMintV1 => {
+            let update: ConsensusStakeUpdateV1 = deserialize(&update_data[1..])?;
+            Ok(consensus_proposal_mint_process_update_v1(cid, update)?)
+        }
         ConsensusFunction::UnstakeV1 => {
             let update: ConsensusUnstakeUpdateV1 = deserialize(&update_data[1..])?;
             Ok(consensus_unstake_process_update_v1(cid, update)?)

+ 188 - 0
src/contract/consensus/src/entrypoint/proposal_burn_v1.rs

@@ -0,0 +1,188 @@
+/* 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::{ConsensusUnstakeParamsV1, ConsensusUnstakeUpdateV1},
+    CONSENSUS_CONTRACT_COIN_ROOTS_TREE, CONSENSUS_CONTRACT_NULLIFIERS_TREE,
+    MONEY_CONTRACT_ZKAS_BURN_NS_V1,
+};
+use darkfi_sdk::{
+    crypto::{
+        pasta_prelude::*, pedersen_commitment_base, ContractId, PublicKey, CONSENSUS_CONTRACT_ID,
+        DARK_TOKEN_ID,
+    },
+    db::{db_contains_key, db_lookup, db_set},
+    error::{ContractError, ContractResult},
+    msg,
+    pasta::pallas,
+    ContractCall,
+};
+use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
+
+use crate::{model::ConsensusRewardParamsV1, ConsensusFunction};
+
+/// `get_metadata` function for `Consensus::ProposalBurnV1`
+pub(crate) fn consensus_proposal_burn_get_metadata_v1(
+    _cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: ConsensusUnstakeParamsV1 = deserialize(&self_.data[1..])?;
+
+    // Public inputs for the ZK proofs we have to verify
+    let mut zk_public_inputs: Vec<(String, Vec<pallas::Base>)> = vec![];
+    // Public keys for the transaction signatures we have to verify
+    let mut signature_pubkeys: Vec<PublicKey> = vec![];
+
+    // Grab the pedersen commitments and signature pubkeys from the
+    // anonymous input
+    let input = &params.input;
+    let value_coords = input.value_commit.to_affine().coordinates().unwrap();
+    let token_coords = input.token_commit.to_affine().coordinates().unwrap();
+    let (sig_x, sig_y) = input.signature_public.xy();
+
+    // It is very important that these are in the same order as the
+    // `constrain_instance` calls in the zkas code.
+    // Otherwise verification will fail.
+    zk_public_inputs.push((
+        MONEY_CONTRACT_ZKAS_BURN_NS_V1.to_string(),
+        vec![
+            input.nullifier.inner(),
+            *value_coords.x(),
+            *value_coords.y(),
+            *token_coords.x(),
+            *token_coords.y(),
+            input.merkle_root.inner(),
+            input.user_data_enc,
+            sig_x,
+            sig_y,
+        ],
+    ));
+
+    signature_pubkeys.push(input.signature_public);
+
+    // 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::ProposalBurnV1`
+pub(crate) fn consensus_proposal_burn_process_instruction_v1(
+    cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: ConsensusUnstakeParamsV1 = deserialize(&self_.data[1..])?;
+
+    // Access the necessary databases where there is information to
+    // validate this state transition.
+    let nullifiers_db = db_lookup(cid, CONSENSUS_CONTRACT_NULLIFIERS_TREE)?;
+    let coin_roots_db = db_lookup(cid, CONSENSUS_CONTRACT_COIN_ROOTS_TREE)?;
+
+    // ===================================
+    // Perform the actual state transition
+    // ===================================
+
+    msg!("[ConsensusProposalBurnV1] Validating anonymous input");
+    let input = &params.input;
+
+    // Only native token can be burned in a proposal
+    if input.token_commit != pedersen_commitment_base(DARK_TOKEN_ID.inner(), params.token_blind) {
+        msg!("[ConsensusProposalBurnV1] Error: Input used non-native token");
+        return Err(MoneyError::StakeInputNonNativeToken.into())
+    }
+
+    // The Merkle root is used to know whether this is a coin that
+    // existed in a previous state.
+    if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? {
+        msg!("[ConsensusProposalBurnV1] Error: Merkle root not found in previous state");
+        return Err(MoneyError::TransferMerkleRootNotFound.into())
+    }
+
+    // The nullifiers should not already exist. It is the double-spend protection.
+    if db_contains_key(nullifiers_db, &serialize(&input.nullifier))? {
+        msg!("[ConsensusProposalBurnV1] Error: Duplicate nullifier found");
+        return Err(MoneyError::DuplicateNullifier.into())
+    }
+
+    // Check next call is consensus contract
+    let next_call_idx = call_idx + 1;
+    if next_call_idx >= calls.len() as u32 {
+        msg!("[ConsensusProposalBurnV1] Error: next_call_idx out of bounds");
+        return Err(MoneyError::SpendHookOutOfBounds.into())
+    }
+
+    let next = &calls[next_call_idx as usize];
+    if next.contract_id.inner() != CONSENSUS_CONTRACT_ID.inner() {
+        msg!("[ConsensusProposalBurnV1] Error: Next contract call is not consensus contract");
+        return Err(MoneyError::StakeNextCallNotConsensusContract.into())
+    }
+
+    // Check if spend hook is set and its correctness
+    if input.spend_hook == pallas::Base::zero() {
+        msg!("[ConsensusProposalBurnV1] Error: Missing spend hook");
+        return Err(MoneyError::StakeMissingSpendHook.into())
+    }
+
+    if input.spend_hook != CONSENSUS_CONTRACT_ID.inner() {
+        msg!("[ConsensusProposalBurnV1] Error: Spend hook is not consensus contract");
+        return Err(MoneyError::UnstakeSpendHookNotConsensusContract.into())
+    }
+
+    // Verify next call corresponds to Consensus::ProposalRewardV1 (0x02)
+    if next.data[0] != 0x02 {
+        msg!("[ConsensusProposalBurnV1] Error: Next call function mismatch");
+        return Err(MoneyError::NextCallFunctionMissmatch.into())
+    }
+
+    // Verify next call StakeInput is the same as this calls input
+    let next_params: ConsensusRewardParamsV1 = deserialize(&next.data[1..])?;
+    if input != &next_params.unstake_input {
+        msg!("[ConsensusProposalBurnV1] Error: Next call input mismatch");
+        return Err(MoneyError::NextCallInputMissmatch.into())
+    }
+
+    // At this point the state transition has passed, so we create a state update
+    let update = ConsensusUnstakeUpdateV1 { nullifier: input.nullifier };
+    let mut update_data = vec![];
+    update_data.write_u8(ConsensusFunction::UnstakeV1 as u8)?;
+    update.encode(&mut update_data)?;
+
+    // and return it
+    Ok(update_data)
+}
+
+/// `process_update` function for `Consensus::ProposalBurnV1`
+pub(crate) fn consensus_proposal_burn_process_update_v1(
+    cid: ContractId,
+    update: ConsensusUnstakeUpdateV1,
+) -> ContractResult {
+    // Grab all necessary db handles for where we want to write
+    let nullifiers_db = db_lookup(cid, CONSENSUS_CONTRACT_NULLIFIERS_TREE)?;
+
+    msg!("[ConsensusProposalBurnV1] Adding new nullifier to the set");
+    db_set(nullifiers_db, &serialize(&update.nullifier), &[])?;
+
+    Ok(())
+}

+ 204 - 0
src/contract/consensus/src/entrypoint/proposal_mint_v1.rs

@@ -0,0 +1,204 @@
+/* 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::{ConsensusStakeParamsV1, ConsensusStakeUpdateV1},
+    CONSENSUS_CONTRACT_COINS_TREE, CONSENSUS_CONTRACT_COIN_MERKLE_TREE,
+    CONSENSUS_CONTRACT_COIN_ROOTS_TREE, CONSENSUS_CONTRACT_INFO_TREE,
+    CONSENSUS_CONTRACT_NULLIFIERS_TREE, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
+};
+use darkfi_sdk::{
+    crypto::{
+        pasta_prelude::*, pedersen_commitment_base, Coin, ContractId, MerkleNode, PublicKey,
+        CONSENSUS_CONTRACT_ID, DARK_TOKEN_ID,
+    },
+    db::{db_contains_key, db_lookup, db_set},
+    error::{ContractError, ContractResult},
+    merkle_add, msg,
+    pasta::pallas,
+    ContractCall,
+};
+use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
+
+use crate::{model::ConsensusRewardParamsV1, ConsensusFunction};
+
+/// `get_metadata` function for `Consensus::ProposalMintV1`
+pub(crate) fn consensus_proposal_mint_get_metadata_v1(
+    _cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: ConsensusStakeParamsV1 = deserialize(&self_.data[1..])?;
+
+    // Public inputs for the ZK proofs we have to verify
+    let mut zk_public_inputs: Vec<(String, Vec<pallas::Base>)> = vec![];
+    // Public keys for the transaction signatures we have to verify
+    let mut signature_pubkeys: Vec<PublicKey> = vec![];
+
+    // 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(),
+        ],
+    ));
+
+    signature_pubkeys.push(params.input.signature_public);
+
+    // 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::ProposalMintV1`
+pub(crate) fn consensus_proposal_mint_process_instruction_v1(
+    cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: ConsensusStakeParamsV1 = deserialize(&self_.data[1..])?;
+
+    // Access the necessary databases where there is information to
+    // validate this state transition.
+    let coins_db = db_lookup(cid, CONSENSUS_CONTRACT_COINS_TREE)?;
+    let nullifiers_db = db_lookup(cid, CONSENSUS_CONTRACT_NULLIFIERS_TREE)?;
+    let coin_roots_db = db_lookup(cid, CONSENSUS_CONTRACT_COIN_ROOTS_TREE)?;
+
+    // ===================================
+    // Perform the actual state transition
+    // ===================================
+
+    msg!("[ConsensusProposalMintV1] Validating anonymous output");
+    let input = &params.input;
+    let output = &params.output;
+
+    // Only native token can be minted in a proposal
+    if output.token_commit != pedersen_commitment_base(DARK_TOKEN_ID.inner(), input.token_blind) {
+        msg!("[ConsensusProposalMintV1] Error: Input used non-native token");
+        return Err(MoneyError::StakeInputNonNativeToken.into())
+    }
+
+    // Verify value commits match
+    if output.value_commit != input.value_commit {
+        msg!("[ConsensusProposalMintV1] Error: Value commitments do not match");
+        return Err(MoneyError::ValueMismatch.into())
+    }
+
+    // The Merkle root is used to know whether this is a coin that
+    // existed in a previous state.
+    if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? {
+        msg!("[ConsensusProposalMintV1] Error: Merkle root not found in previous state");
+        return Err(MoneyError::TransferMerkleRootNotFound.into())
+    }
+
+    // The nullifiers should already exist. It is the double-mint protection.
+    if db_contains_key(nullifiers_db, &serialize(&input.nullifier))? {
+        msg!("[ConsensusProposalMintV1] Error: Missing nullifier");
+        return Err(MoneyError::StakeMissingNullifier.into())
+    }
+
+    // Check previous call is consensus contract
+    if call_idx == 0 {
+        msg!("[ConsensusProposalMintV1] Error: previous_call_idx will be out of bounds");
+        return Err(MoneyError::SpendHookOutOfBounds.into())
+    }
+
+    let previous_call_idx = call_idx - 1;
+    let previous = &calls[previous_call_idx as usize];
+    if previous.contract_id.inner() != CONSENSUS_CONTRACT_ID.inner() {
+        msg!("[ConsensusProposalMintV1] Error: Previous contract call is not consensus contract");
+        return Err(MoneyError::UnstakePreviousCallNotConsensusContract.into())
+    }
+
+    // Verify previous call corresponds to Consensus::ProposalRewardV1 (0x02)
+    if previous.data[0] != 0x02 {
+        msg!("[ConsensusProposalMintV1] Error: Previous call function mismatch");
+        return Err(MoneyError::PreviousCallFunctionMissmatch.into())
+    }
+
+    // Verify previous call input is the same as this calls StakeInput
+    let previous_params: ConsensusRewardParamsV1 = deserialize(&previous.data[1..])?;
+    let previous_input = &previous_params.stake_input;
+    if &previous_input != &input {
+        msg!("[ConsensusProposalMintV1] Error: Previous call input mismatch");
+        return Err(MoneyError::PreviousCallInputMissmatch.into())
+    }
+    if &previous_params.output != output {
+        msg!("[ConsensusProposalMintV1] Error: Previous call input mismatch");
+        return Err(MoneyError::PreviousCallInputMissmatch.into())
+    }
+
+    // If spend hook is set, check its correctness
+    let previous_input = &previous_params.unstake_input;
+    if previous_input.spend_hook != pallas::Base::zero() &&
+        previous_input.spend_hook != CONSENSUS_CONTRACT_ID.inner()
+    {
+        msg!("[ConsensusProposalMintV1] Error: Invoking contract call does not match spend hook in input");
+        return Err(MoneyError::SpendHookMismatch.into())
+    }
+
+    // Newly created coin for this call is in the output. Here we gather it,
+    // and we also check that it hasn't existed before.
+    if db_contains_key(coins_db, &serialize(&output.coin))? {
+        msg!("[ConsensusProposalMintV1] Error: Duplicate coin found in output");
+        return Err(MoneyError::DuplicateCoin.into())
+    }
+    let coin = Coin::from(output.coin);
+
+    // Create a state update.
+    let update = ConsensusStakeUpdateV1 { coin };
+    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::ProposalMintV1`
+pub(crate) fn consensus_proposal_mint_process_update_v1(
+    cid: ContractId,
+    update: ConsensusStakeUpdateV1,
+) -> ContractResult {
+    // Grab all necessary db handles for where we want to write
+    let info_db = db_lookup(cid, CONSENSUS_CONTRACT_INFO_TREE)?;
+    let coins_db = db_lookup(cid, CONSENSUS_CONTRACT_COINS_TREE)?;
+    let coin_roots_db = db_lookup(cid, CONSENSUS_CONTRACT_COIN_ROOTS_TREE)?;
+
+    msg!("[ConsensusProposalMintV1] Adding new coin to the set");
+    db_set(coins_db, &serialize(&update.coin), &[])?;
+
+    msg!("[ConsensusProposalMintV1] Adding new coin to the Merkle tree");
+    let coins: Vec<_> = vec![MerkleNode::from(update.coin.inner())];
+    merkle_add(info_db, coin_roots_db, &serialize(&CONSENSUS_CONTRACT_COIN_MERKLE_TREE), &coins)?;
+
+    Ok(())
+}

+ 190 - 0
src/contract/consensus/src/entrypoint/proposal_reward_v1.rs

@@ -0,0 +1,190 @@
+/* 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::{ConsensusStakeParamsV1, ConsensusUnstakeParamsV1},
+    CONSENSUS_CONTRACT_ZKAS_REWARD_NS_V1,
+};
+use darkfi_sdk::{
+    crypto::{
+        pasta_prelude::*, pedersen_commitment_base, pedersen_commitment_u64, ContractId, PublicKey,
+        CONSENSUS_CONTRACT_ID, DARK_TOKEN_ID,
+    },
+    error::{ContractError, ContractResult},
+    msg,
+    pasta::pallas,
+    ContractCall,
+};
+use darkfi_serial::{deserialize, Encodable, WriteExt};
+
+use crate::{
+    model::{ConsensusRewardParamsV1, ConsensusRewardUpdateV1, REWARD},
+    ConsensusFunction,
+};
+
+/// `get_metadata` function for `Consensus::ProposalRewardV1`
+pub(crate) fn consensus_proposal_reward_get_metadata_v1(
+    _cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: ConsensusRewardParamsV1 = deserialize(&self_.data[1..])?;
+
+    // Public inputs for the ZK proofs we have to verify
+    let mut zk_public_inputs: Vec<(String, Vec<pallas::Base>)> = vec![];
+    // Public keys for the transaction signatures we have to verify
+    let mut signature_pubkeys: Vec<PublicKey> = vec![];
+
+    // Grab the pedersen commitment for the burnt value
+    let value_coords = &params.unstake_input.value_commit.to_affine().coordinates().unwrap();
+
+    zk_public_inputs.push((
+        CONSENSUS_CONTRACT_ZKAS_REWARD_NS_V1.to_string(),
+        vec![*value_coords.x(), *value_coords.y()],
+    ));
+
+    signature_pubkeys.push(params.stake_input.signature_public);
+
+    // 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::ProposalRewardV1`
+pub(crate) fn consensus_proposal_reward_process_instruction_v1(
+    _cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: ConsensusRewardParamsV1 = deserialize(&self_.data[1..])?;
+
+    // ===================================
+    // Perform the actual state transition
+    // ===================================
+
+    msg!("[ConsensusProposalRewardV1] Validating anonymous inputs");
+    let unstake_input = &params.unstake_input;
+    let stake_input = &params.stake_input;
+    let output = &params.output;
+
+    // Only native token can be rewarded in a proposal
+    let dark_token_commit =
+        pedersen_commitment_base(DARK_TOKEN_ID.inner(), stake_input.token_blind);
+    if unstake_input.token_commit != dark_token_commit || output.token_commit != dark_token_commit {
+        msg!("[ConsensusProposalRewardV1] Error: Input used non-native token");
+        return Err(MoneyError::StakeInputNonNativeToken.into())
+    }
+
+    // Verify value commits match
+    let mut valcom_total = pallas::Point::identity();
+    valcom_total += unstake_input.value_commit;
+    valcom_total += pedersen_commitment_u64(REWARD, pallas::Scalar::zero());
+    valcom_total -= stake_input.value_commit;
+    if valcom_total != pallas::Point::identity() {
+        msg!("[ConsensusProposalRewardV1] Error: Value commitments do not result in identity");
+        return Err(MoneyError::ValueMismatch.into())
+    }
+
+    // Check previous call is consensus contract
+    if call_idx == 0 {
+        msg!("[ConsensusProposalRewardV1] Error: previous_call_idx will be out of bounds");
+        return Err(MoneyError::SpendHookOutOfBounds.into())
+    }
+
+    let previous_call_idx = call_idx - 1;
+    let previous = &calls[previous_call_idx as usize];
+    if previous.contract_id.inner() != CONSENSUS_CONTRACT_ID.inner() {
+        msg!("[ConsensusProposalRewardV1] Error: Previous contract call is not consensus contract");
+        return Err(MoneyError::UnstakePreviousCallNotConsensusContract.into())
+    }
+
+    // Verify previous call corresponds to Consensus::ProposalBurnV1 (0x01)
+    if previous.data[0] != 0x01 {
+        msg!("[ConsensusProposalRewardV1] Error: Previous call function mismatch");
+        return Err(MoneyError::PreviousCallFunctionMissmatch.into())
+    }
+
+    // Verify previous call input is the same as this calls StakeInput
+    let previous_params: ConsensusUnstakeParamsV1 = deserialize(&previous.data[1..])?;
+    let previous_input = &previous_params.input;
+    if &previous_input != &unstake_input {
+        msg!("[ConsensusProposalRewardV1] Error: Previous call input mismatch");
+        return Err(MoneyError::PreviousCallInputMissmatch.into())
+    }
+
+    // If spend hook is set, check its correctness
+    if previous_input.spend_hook != pallas::Base::zero() &&
+        previous_input.spend_hook != CONSENSUS_CONTRACT_ID.inner()
+    {
+        msg!("[ConsensusProposalRewardV1] Error: Invoking contract call does not match spend hook in input");
+        return Err(MoneyError::SpendHookMismatch.into())
+    }
+
+    // Check next call is consensus contract
+    let next_call_idx = call_idx + 1;
+    if next_call_idx >= calls.len() as u32 {
+        msg!("[ConsensusProposalRewardV1] Error: next_call_idx out of bounds");
+        return Err(MoneyError::SpendHookOutOfBounds.into())
+    }
+
+    let next = &calls[next_call_idx as usize];
+    if next.contract_id.inner() != CONSENSUS_CONTRACT_ID.inner() {
+        msg!("[ConsensusProposalRewardV1] Error: Next contract call is not consensus contract");
+        return Err(MoneyError::StakeNextCallNotConsensusContract.into())
+    }
+
+    // Verify next call corresponds to Consensus::ProposalMintV1 (0x03)
+    if next.data[0] != 0x03 {
+        msg!("[ConsensusProposalRewardV1] Error: Next call function mismatch");
+        return Err(MoneyError::NextCallFunctionMissmatch.into())
+    }
+
+    // Verify next call StakeInput is the same as this calls input
+    let next_params: ConsensusStakeParamsV1 = deserialize(&next.data[1..])?;
+    if stake_input != &next_params.input {
+        msg!("[ConsensusProposalRewardV1] Error: Next call input mismatch");
+        return Err(MoneyError::NextCallInputMissmatch.into())
+    }
+    if output != &next_params.output {
+        msg!("[ConsensusProposalRewardV1] Error: Next call input mismatch");
+        return Err(MoneyError::NextCallInputMissmatch.into())
+    }
+
+    // Create a state update.
+    let update = ConsensusRewardUpdateV1 {};
+    let mut update_data = vec![];
+    update_data.write_u8(ConsensusFunction::ProposalRewardV1 as u8)?;
+    update.encode(&mut update_data)?;
+
+    Ok(update_data)
+}
+
+/// `process_update` function for `Consensus::ProposalRewardV1`
+pub(crate) fn consensus_proposal_reward_process_update_v1(
+    _cid: ContractId,
+    _update: ConsensusRewardUpdateV1,
+) -> ContractResult {
+    // This contract call doesn't produce any updates
+    Ok(())
+}

+ 11 - 4
src/contract/consensus/src/lib.rs

@@ -25,8 +25,10 @@ use darkfi_sdk::error::ContractError;
 #[repr(u8)]
 pub enum ConsensusFunction {
     StakeV1 = 0x00,
-    //EvolveV1 = 0x01,
-    UnstakeV1 = 0x02,
+    ProposalBurnV1 = 0x01,
+    ProposalRewardV1 = 0x02,
+    ProposalMintV1 = 0x03,
+    UnstakeV1 = 0x04,
 }
 
 impl TryFrom<u8> for ConsensusFunction {
@@ -35,13 +37,18 @@ 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::EvolveV1),
-            0x02 => Ok(Self::UnstakeV1),
+            0x01 => Ok(Self::ProposalBurnV1),
+            0x02 => Ok(Self::ProposalRewardV1),
+            0x03 => Ok(Self::ProposalMintV1),
+            0x04 => Ok(Self::UnstakeV1),
             _ => Err(ContractError::InvalidFunction),
         }
     }
 }
 
+/// Call parameters definitions
+pub mod model;
+
 #[cfg(not(feature = "no-entrypoint"))]
 /// WASM entrypoint functions
 pub mod entrypoint;

+ 38 - 0
src/contract/consensus/src/model.rs

@@ -0,0 +1,38 @@
+/* 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::model::{Input, Output, StakeInput};
+use darkfi_serial::{SerialDecodable, SerialEncodable};
+
+// TODO: Don't set this here
+pub const REWARD: u64 = 1;
+
+/// Parameters for `Consensus::Reward`
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub struct ConsensusRewardParamsV1 {
+    /// Anonymous input of `Consensus::Unstake`
+    pub unstake_input: Input,
+    /// Burnt token revealed info of `Consensus::Stake`
+    pub stake_input: StakeInput,
+    /// Anonymous output
+    pub output: Output,
+}
+
+/// State update for `Consensus::Reward`
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub struct ConsensusRewardUpdateV1 {}

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

@@ -43,7 +43,8 @@ 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,
+    CONSENSUS_CONTRACT_ZKAS_REWARD_NS_V1, MONEY_CONTRACT_ZKAS_BURN_NS_V1,
+    MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 
 pub fn init_logger() {
@@ -147,6 +148,7 @@ impl ConsensusTestHarness {
         )?;
         mkpk!(MONEY_CONTRACT_ZKAS_MINT_NS_V1);
         mkpk!(MONEY_CONTRACT_ZKAS_BURN_NS_V1);
+        mkpk!(CONSENSUS_CONTRACT_ZKAS_REWARD_NS_V1);
 
         Ok(Self { faucet, alice, proving_keys })
     }

+ 147 - 5
src/contract/consensus/tests/stake_unstake.rs

@@ -44,11 +44,16 @@ use darkfi_money_contract::{
     client::{
         stake_v1::MoneyStakeCallBuilder, unstake_v1::MoneyUnstakeCallBuilder, MoneyNote, OwnCoin,
     },
-    MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
+    MoneyFunction, CONSENSUS_CONTRACT_ZKAS_REWARD_NS_V1, MONEY_CONTRACT_ZKAS_BURN_NS_V1,
+    MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 
 use darkfi_consensus_contract::{
-    client::{stake_v1::ConsensusStakeCallBuilder, unstake_v1::ConsensusUnstakeCallBuilder},
+    client::{
+        proposal_v1::ConsensusProposalCallBuilder, stake_v1::ConsensusStakeCallBuilder,
+        unstake_v1::ConsensusUnstakeCallBuilder,
+    },
+    model::REWARD,
     ConsensusFunction,
 };
 
@@ -64,6 +69,10 @@ async fn consensus_contract_stake_unstake() -> Result<()> {
     let mut stake_broadcasted_sizes = vec![];
     let mut stake_creation_times = vec![];
     let mut stake_verify_times = vec![];
+    let mut proposal_sizes = vec![];
+    let mut proposal_broadcasted_sizes = vec![];
+    let mut proposal_creation_times = vec![];
+    let mut proposal_verify_times = vec![];
     let mut unstake_sizes = vec![];
     let mut unstake_broadcasted_sizes = vec![];
     let mut unstake_creation_times = vec![];
@@ -80,6 +89,8 @@ async fn consensus_contract_stake_unstake() -> Result<()> {
     let (airdrop_tx, airdrop_params) = th.airdrop_native(ALICE_AIRDROP, th.alice.keypair.public)?;
     let (mint_pk, mint_zkbin) = th.proving_keys.get(&MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
     let (burn_pk, burn_zkbin) = th.proving_keys.get(&MONEY_CONTRACT_ZKAS_BURN_NS_V1).unwrap();
+    let (reward_pk, reward_zkbin) =
+        th.proving_keys.get(&CONSENSUS_CONTRACT_ZKAS_REWARD_NS_V1).unwrap();
 
     info!(target: "consensus", "[Faucet] ==========================");
     info!(target: "consensus", "[Faucet] Executing Alice airdrop tx");
@@ -215,13 +226,131 @@ async fn consensus_contract_stake_unstake() -> Result<()> {
     // Verify values match
     assert!(alice_oc.note.value == alice_staked_oc.note.value);
 
+    // We asume alice became the slot proposer, so she creates a
+    // proposal transaction to burn her staked coin, reward herself
+    // and mint the new coin. Burn and Mint use the same parameters
+    // as Unstake and Stake.
+    info!(target: "consensus", "[Alice] ====================================");
+    info!(target: "consensus", "[Alice] Building proposal transaction params");
+    info!(target: "consensus", "[Alice] ====================================");
+    let timer = Instant::now();
+    let alice_consensus_proposal_call_debris = ConsensusProposalCallBuilder {
+        coin: alice_staked_oc.clone(),
+        recipient: th.alice.keypair.public,
+        tree: th.alice.consensus_merkle_tree.clone(),
+        burn_zkbin: burn_zkbin.clone(),
+        burn_pk: burn_pk.clone(),
+        reward_zkbin: reward_zkbin.clone(),
+        reward_pk: reward_pk.clone(),
+        mint_zkbin: mint_zkbin.clone(),
+        mint_pk: mint_pk.clone(),
+    }
+    .build()?;
+    let (
+        alice_consensus_unstake_params,
+        alice_consensus_unstake_proofs,
+        alice_consensus_reward_params,
+        alice_consensus_reward_proofs,
+        alice_consensus_stake_params,
+        alice_consensus_stake_proofs,
+        alice_consensus_proposal_secret_key,
+    ) = (
+        alice_consensus_proposal_call_debris.unstake_params,
+        alice_consensus_proposal_call_debris.unstake_proofs,
+        alice_consensus_proposal_call_debris.reward_params,
+        alice_consensus_proposal_call_debris.reward_proofs,
+        alice_consensus_proposal_call_debris.stake_params,
+        alice_consensus_proposal_call_debris.stake_proofs,
+        alice_consensus_proposal_call_debris.signature_secret,
+    );
+
+    info!(target: "consensus", "[Alice] ====================");
+    info!(target: "consensus", "[Alice] Building proposal tx");
+    info!(target: "consensus", "[Alice] ====================");
+    let mut data = vec![ConsensusFunction::ProposalBurnV1 as u8];
+    alice_consensus_unstake_params.encode(&mut data)?;
+    let consensus_unstake_call = ContractCall { contract_id: *CONSENSUS_CONTRACT_ID, data };
+
+    let mut data = vec![ConsensusFunction::ProposalRewardV1 as u8];
+    alice_consensus_reward_params.encode(&mut data)?;
+    let consensus_reward_call = ContractCall { contract_id: *CONSENSUS_CONTRACT_ID, data };
+
+    let mut data = vec![ConsensusFunction::ProposalMintV1 as u8];
+    alice_consensus_stake_params.encode(&mut data)?;
+    let consensus_stake_call = ContractCall { contract_id: *CONSENSUS_CONTRACT_ID, data };
+
+    let calls = vec![consensus_unstake_call, consensus_reward_call, consensus_stake_call];
+    let proofs = vec![
+        alice_consensus_unstake_proofs,
+        alice_consensus_reward_proofs,
+        alice_consensus_stake_proofs,
+    ];
+    let mut alice_proposal_tx = Transaction { calls, proofs, signatures: vec![] };
+    let consensus_unstake_sigs =
+        alice_proposal_tx.create_sigs(&mut OsRng, &[alice_consensus_proposal_secret_key])?;
+    let consensus_reward_sigs =
+        alice_proposal_tx.create_sigs(&mut OsRng, &[alice_consensus_proposal_secret_key])?;
+    let consensus_stake_sigs =
+        alice_proposal_tx.create_sigs(&mut OsRng, &[alice_consensus_proposal_secret_key])?;
+    alice_proposal_tx.signatures =
+        vec![consensus_unstake_sigs, consensus_reward_sigs, consensus_stake_sigs];
+    proposal_creation_times.push(timer.elapsed());
+
+    // Calculate transaction sizes
+    let encoded: Vec<u8> = serialize(&alice_proposal_tx);
+    let size = ::std::mem::size_of_val(&*encoded);
+    proposal_sizes.push(size);
+    let base58 = bs58::encode(&encoded).into_string();
+    let size = ::std::mem::size_of_val(&*base58);
+    proposal_broadcasted_sizes.push(size);
+
+    info!(target: "consensus", "[Faucet] ===========================");
+    info!(target: "consensus", "[Faucet] Executing Alice proposal tx");
+    info!(target: "consensus", "[Faucet] ===========================");
+    let timer = Instant::now();
+    th.faucet.state.read().await.verify_transactions(&[alice_proposal_tx.clone()], true).await?;
+    th.faucet
+        .consensus_merkle_tree
+        .append(&MerkleNode::from(alice_consensus_stake_params.output.coin.inner()));
+    proposal_verify_times.push(timer.elapsed());
+
+    info!(target: "consensus", "[Alice] ===========================");
+    info!(target: "consensus", "[Alice] Executing Alice proposal tx");
+    info!(target: "consensus", "[Alice] ===========================");
+    let timer = Instant::now();
+    th.alice.state.read().await.verify_transactions(&[alice_proposal_tx.clone()], true).await?;
+    th.alice
+        .consensus_merkle_tree
+        .append(&MerkleNode::from(alice_consensus_stake_params.output.coin.inner()));
+    proposal_verify_times.push(timer.elapsed());
+
+    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 =
+        alice_consensus_stake_params.output.note.decrypt(&th.alice.keypair.secret)?;
+    let alice_rewarded_staked_oc = OwnCoin {
+        coin: Coin::from(alice_consensus_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_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 Consensus::Unstake params");
     info!(target: "consensus", "[Alice] ==================================");
     let timer = Instant::now();
     let alice_consensus_unstake_call_debris = ConsensusUnstakeCallBuilder {
-        coin: alice_staked_oc.clone(),
+        coin: alice_rewarded_staked_oc.clone(),
         tree: th.alice.consensus_merkle_tree.clone(),
         burn_zkbin: burn_zkbin.clone(),
         burn_pk: burn_pk.clone(),
@@ -243,7 +372,7 @@ async fn consensus_contract_stake_unstake() -> Result<()> {
     info!(target: "consensus", "[Alice] Building Money::Unstake params");
     info!(target: "consensus", "[Alice] ==============================");
     let alice_money_unstake_call_debris = MoneyUnstakeCallBuilder {
-        coin: alice_staked_oc.clone(),
+        coin: alice_rewarded_staked_oc.clone(),
         recipient: th.alice.keypair.public,
         value_blind: alice_consensus_unstake_value_blind,
         token_blind: alice_consensus_unstake_params.token_blind,
@@ -321,7 +450,7 @@ async fn consensus_contract_stake_unstake() -> Result<()> {
     };
 
     // Verify values match
-    assert!(alice_staked_oc.note.value == alice_unstaked_oc.note.value);
+    assert!(alice_rewarded_staked_oc.note.value == alice_unstaked_oc.note.value);
 
     // Statistics
     let stake_avg = stake_sizes.iter().sum::<usize>();
@@ -337,6 +466,19 @@ async fn consensus_contract_stake_unstake() -> Result<()> {
     let stake_avg = stake_avg / stake_verify_times.len() as u32;
     info!("Average Stake verification time: {:?}", stake_avg);
 
+    let proposal_avg = proposal_sizes.iter().sum::<usize>();
+    let proposal_avg = proposal_avg / proposal_sizes.len();
+    info!("Average Proposal size: {:?} Bytes", proposal_avg);
+    let proposal_avg = proposal_broadcasted_sizes.iter().sum::<usize>();
+    let proposal_avg = proposal_avg / proposal_broadcasted_sizes.len();
+    info!("Average Proposal broadcasted size: {:?} Bytes", proposal_avg);
+    let proposal_avg = proposal_creation_times.iter().sum::<Duration>();
+    let proposal_avg = proposal_avg / proposal_creation_times.len() as u32;
+    info!("Average Proposal creation time: {:?}", proposal_avg);
+    let proposal_avg = proposal_verify_times.iter().sum::<Duration>();
+    let proposal_avg = proposal_avg / proposal_verify_times.len() as u32;
+    info!("Average Proposal verification time: {:?}", proposal_avg);
+
     let unstake_avg = unstake_sizes.iter().sum::<usize>();
     let unstake_avg = unstake_avg / unstake_sizes.len();
     info!("Average Unstake size: {:?} Bytes", unstake_avg);

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

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

+ 2 - 0
src/contract/money/src/error.rs

@@ -19,6 +19,8 @@
 use darkfi_sdk::error::ContractError;
 
 #[derive(Debug, Clone, thiserror::Error)]
+// TODO: Make generic contract common errors like
+// NextCallFunctionMissmatch
 pub enum MoneyError {
     #[error("Missing inputs in transfer call")]
     TransferMissingInputs,

+ 3 - 0
src/contract/money/src/lib.rs

@@ -97,3 +97,6 @@ pub const CONSENSUS_CONTRACT_NULLIFIERS_TREE: &str = "consensus_nullifiers";
 // These are keys inside the consensus info tree
 pub const CONSENSUS_CONTRACT_DB_VERSION: &str = env!("CARGO_PKG_VERSION");
 pub const CONSENSUS_CONTRACT_COIN_MERKLE_TREE: &str = "consensus_coin_tree";
+
+/// zkas reward circuit namespace
+pub const CONSENSUS_CONTRACT_ZKAS_REWARD_NS_V1: &str = "Reward_V1";

+ 1 - 1
src/contract/money/src/model.rs

@@ -85,7 +85,7 @@ impl PartialEq<StakeInput> for Input {
 }
 
 /// A contract call's anonymous output
-#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+#[derive(Clone, Debug, PartialEq, SerialEncodable, SerialDecodable)]
 pub struct Output {
     /// Pedersen commitment for the output's value
     pub value_commit: pallas::Point,