Przeglądaj źródła

contract/consensus: introduce unstake request and unstake timelock

Staker will have to request to unstake their token. Once request pass, tokens are locked and they can't use them to participate in proposals or unstake them. After grace(lock) period has passed, they can normaly unstake them(move them to Money).
aggstam 3 lat temu
rodzic
commit
b6a58ef597

+ 1 - 2
src/contract/consensus/src/client/common.rs

@@ -60,11 +60,10 @@ pub struct ConsensusMintRevealed {
 impl ConsensusMintRevealed {
     pub fn to_vec(&self) -> Vec<pallas::Base> {
         let valcom_coords = self.value_commit.to_affine().coordinates().unwrap();
-        let epoch_palas = pallas::Base::from(self.epoch);
 
         // NOTE: It's important to keep these in the same order
         // as the `constrain_instance` calls in the zkas code.
-        vec![epoch_palas, self.coin.inner(), *valcom_coords.x(), *valcom_coords.y()]
+        vec![self.epoch.into(), self.coin.inner(), *valcom_coords.x(), *valcom_coords.y()]
     }
 }
 

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

@@ -37,5 +37,8 @@ pub mod stake_v1;
 /// Proposal transaction building API.
 pub mod proposal_v1;
 
+/// `Consensus::UnstakeRequestV1` API
+pub mod unstake_request_v1;
+
 /// `Consensus::UnstakeV1` API
 pub mod unstake_v1;

+ 146 - 0
src/contract/consensus/src/client/unstake_request_v1.rs

@@ -0,0 +1,146 @@
+/* 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::{ConsensusNote, ConsensusOwnCoin},
+    model::{ConsensusInput, ConsensusOutput, ConsensusStakeParamsV1},
+};
+use darkfi_sdk::{
+    crypto::{note::AeadEncryptedNote, pasta_prelude::*, MerkleTree, SecretKey},
+    incrementalmerkletree::Tree,
+    pasta::pallas,
+};
+use log::{debug, info};
+use rand::rngs::OsRng;
+
+use crate::client::common::{
+    create_consensus_burn_proof, create_consensus_mint_proof, ConsensusBurnInputInfo,
+    ConsensusMintOutputInfo,
+};
+
+pub struct ConsensusUnstakeRequestCallDebris {
+    pub params: ConsensusStakeParamsV1,
+    pub proofs: Vec<Proof>,
+    pub signature_secret: SecretKey,
+}
+
+/// Struct holding necessary information to build a `Consensus::UnstakeRequestV1` contract call.
+pub struct ConsensusUnstakeRequestCallBuilder {
+    /// `ConsensusOwnCoin` we're given to use in this builder
+    pub coin: ConsensusOwnCoin,
+    /// Epoch unstaked coin is minted
+    pub epoch: u64,
+    /// Merkle tree of coins used to create inclusion proofs
+    pub tree: MerkleTree,
+    /// `ConsensusBurn_V1` zkas circuit ZkBinary
+    pub burn_zkbin: ZkBinary,
+    /// Proving key for the `ConsensusBurn_V1` zk circuit
+    pub burn_pk: ProvingKey,
+    /// `ConsensusMint_V1` zkas circuit ZkBinary
+    pub mint_zkbin: ZkBinary,
+    /// Proving key for the `ConsensusMint_V1` zk circuit
+    pub mint_pk: ProvingKey,
+}
+
+impl ConsensusUnstakeRequestCallBuilder {
+    pub fn build(&self) -> Result<ConsensusUnstakeRequestCallDebris> {
+        debug!("Building Consensus::UnstakeRequestV1 contract call");
+        assert!(self.coin.note.value != 0);
+
+        debug!("Building anonymous input");
+        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 value_blind = pallas::Scalar::random(&mut OsRng);
+        let input = ConsensusBurnInputInfo {
+            leaf_position,
+            merkle_path,
+            secret: self.coin.secret,
+            note: self.coin.note.clone(),
+            value_blind,
+        };
+        debug!("Finished building input");
+
+        info!("Creating unstake burn proof for input");
+        let value_blind = input.value_blind;
+        let (burn_proof, public_inputs, signature_secret) =
+            create_consensus_burn_proof(&self.burn_zkbin, &self.burn_pk, &input)?;
+
+        let input = ConsensusInput {
+            epoch: self.coin.note.epoch,
+            value_commit: public_inputs.value_commit,
+            nullifier: public_inputs.nullifier,
+            merkle_root: public_inputs.merkle_root,
+            signature_public: public_inputs.signature_public,
+        };
+
+        debug!("Building anonymous output");
+        let serial = pallas::Base::random(&mut OsRng);
+        let coin_blind = pallas::Base::random(&mut OsRng);
+        let public_key = public_inputs.signature_public;
+
+        let output = ConsensusMintOutputInfo {
+            value: self.coin.note.value,
+            epoch: self.epoch,
+            public_key,
+            value_blind,
+            serial,
+            coin_blind,
+        };
+        debug!("Finished building output");
+
+        info!("Creating stake mint proof for output");
+        let (mint_proof, public_inputs) =
+            create_consensus_mint_proof(&self.mint_zkbin, &self.mint_pk, &output)?;
+
+        // Encrypted note
+        let note = ConsensusNote {
+            serial,
+            value: output.value,
+            epoch: self.epoch,
+            coin_blind,
+            value_blind,
+            reward: 0,
+            reward_blind: value_blind,
+        };
+
+        let encrypted_note = AeadEncryptedNote::encrypt(&note, &output.public_key, &mut OsRng)?;
+
+        let output = ConsensusOutput {
+            value_commit: public_inputs.value_commit,
+            coin: public_inputs.coin,
+            note: encrypted_note,
+        };
+
+        // We now fill this with necessary stuff
+        let params = ConsensusStakeParamsV1 { input, output };
+        let proofs = vec![burn_proof, mint_proof];
+
+        // Now we should have all the params, zk proof, and signature secret.
+        // We return it all and let the caller deal with it.
+        let debris = ConsensusUnstakeRequestCallDebris { params, proofs, signature_secret };
+        Ok(debris)
+    }
+}

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

@@ -89,7 +89,7 @@ impl ConsensusUnstakeCallBuilder {
         };
 
         // We now fill this with necessary stuff
-        let params = ConsensusUnstakeParamsV1 { input };
+        let params = ConsensusUnstakeParamsV1 { input, coin: self.coin.coin };
         let proofs = vec![proof];
 
         // Now we should have all the params, zk proof, signature secret and token blind.

+ 27 - 0
src/contract/consensus/src/entrypoint.rs

@@ -21,6 +21,7 @@ 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,
+    CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE,
 };
 use darkfi_sdk::{
     crypto::{ContractId, MerkleTree},
@@ -54,6 +55,13 @@ use proposal_v1::{
     consensus_proposal_process_update_v1,
 };
 
+/// `Consensus::UnstakeRequest` functions
+mod unstake_request_v1;
+use unstake_request_v1::{
+    consensus_unstake_request_get_metadata_v1, consensus_unstake_request_process_instruction_v1,
+    consensus_unstake_request_process_update_v1,
+};
+
 /// `Consensus::Unstake` functions
 mod unstake_v1;
 use unstake_v1::{
@@ -104,6 +112,12 @@ fn init_contract(cid: ContractId, _ix: &[u8]) -> ContractResult {
         db_init(cid, CONSENSUS_CONTRACT_NULLIFIERS_TREE)?;
     }
 
+    // Set up a database tree to hold all unstaked coins ever seen
+    // k=Coin, v=[]
+    if db_lookup(cid, CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE).is_err() {
+        db_init(cid, CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE)?;
+    }
+
     // Set up a database tree for arbitrary data
     let info_db = match db_lookup(cid, CONSENSUS_CONTRACT_INFO_TREE) {
         Ok(v) => v,
@@ -159,6 +173,10 @@ fn get_metadata(cid: ContractId, ix: &[u8]) -> ContractResult {
             let metadata = consensus_proposal_get_metadata_v1(cid, call_idx, calls)?;
             Ok(set_return_data(&metadata)?)
         }
+        ConsensusFunction::UnstakeRequestV1 => {
+            let metadata = consensus_unstake_request_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)?)
@@ -194,6 +212,11 @@ fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult {
             let update_data = consensus_proposal_process_instruction_v1(cid, call_idx, calls)?;
             Ok(set_return_data(&update_data)?)
         }
+        ConsensusFunction::UnstakeRequestV1 => {
+            let update_data =
+                consensus_unstake_request_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)?)
@@ -220,6 +243,10 @@ fn process_update(cid: ContractId, update_data: &[u8]) -> ContractResult {
             let update: ConsensusProposalUpdateV1 = deserialize(&update_data[1..])?;
             Ok(consensus_proposal_process_update_v1(cid, update)?)
         }
+        ConsensusFunction::UnstakeRequestV1 => {
+            let update: ConsensusProposalUpdateV1 = deserialize(&update_data[1..])?;
+            Ok(consensus_unstake_request_process_update_v1(cid, update)?)
+        }
         ConsensusFunction::UnstakeV1 => {
             let update: ConsensusUnstakeUpdateV1 = deserialize(&update_data[1..])?;
             Ok(consensus_unstake_process_update_v1(cid, update)?)

+ 12 - 3
src/contract/consensus/src/entrypoint/genesis_stake_v1.rs

@@ -19,7 +19,8 @@
 use darkfi_money_contract::{
     error::MoneyError,
     model::{ConsensusStakeUpdateV1, PALLAS_ZERO},
-    CONSENSUS_CONTRACT_COINS_TREE, CONSENSUS_CONTRACT_ZKAS_MINT_NS_V1,
+    CONSENSUS_CONTRACT_COINS_TREE, CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE,
+    CONSENSUS_CONTRACT_ZKAS_MINT_NS_V1,
 };
 use darkfi_sdk::{
     crypto::{pasta_prelude::*, pedersen_commitment_u64, ContractId, DARK_TOKEN_ID},
@@ -92,10 +93,18 @@ pub(crate) fn consensus_genesis_stake_process_instruction_v1(
 
     // Access the necessary databases where there is information to
     // validate this state transition.
-    let consensus_coins_db = db_lookup(cid, CONSENSUS_CONTRACT_COINS_TREE)?;
+    let coins_db = db_lookup(cid, CONSENSUS_CONTRACT_COINS_TREE)?;
+    let unstaked_coins_db = db_lookup(cid, CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE)?;
 
     // Check that the coin from the output hasn't existed before.
-    if db_contains_key(consensus_coins_db, &serialize(&params.output.coin))? {
+    let coin = serialize(&params.output.coin);
+    if db_contains_key(coins_db, &coin)? {
+        msg!("[GenesisStakeV1] Error: Duplicate coin in output");
+        return Err(MoneyError::DuplicateCoin.into())
+    }
+
+    // Check that the coin from the output hasn't existed before in unstake set.
+    if db_contains_key(unstaked_coins_db, &coin)? {
         msg!("[GenesisStakeV1] Error: Duplicate coin in output");
         return Err(MoneyError::DuplicateCoin.into())
     }

+ 14 - 7
src/contract/consensus/src/entrypoint/proposal_v1.rs

@@ -19,7 +19,8 @@
 use darkfi_money_contract::{
     error::MoneyError, CONSENSUS_CONTRACT_COINS_TREE, CONSENSUS_CONTRACT_COIN_MERKLE_TREE,
     CONSENSUS_CONTRACT_COIN_ROOTS_TREE, CONSENSUS_CONTRACT_INFO_TREE,
-    CONSENSUS_CONTRACT_NULLIFIERS_TREE, CONSENSUS_CONTRACT_ZKAS_PROPOSAL_NS_V1,
+    CONSENSUS_CONTRACT_NULLIFIERS_TREE, CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE,
+    CONSENSUS_CONTRACT_ZKAS_PROPOSAL_NS_V1,
 };
 use darkfi_sdk::{
     crypto::{pasta_prelude::*, pedersen_commitment_u64, poseidon_hash, ContractId, MerkleNode},
@@ -159,25 +160,24 @@ pub(crate) fn consensus_proposal_process_instruction_v1(
 ) -> Result<Vec<u8>, ContractError> {
     let self_ = &calls[call_idx as usize];
     let params: ConsensusProposalParamsV1 = deserialize(&self_.data[1..])?;
+    let input = &params.input;
+    let output = &params.output;
 
     // Access the necessary databases where there is information to
     // validate this state transition.
     let nullifiers_db = db_lookup(cid, CONSENSUS_CONTRACT_NULLIFIERS_TREE)?;
     let coins_db = db_lookup(cid, CONSENSUS_CONTRACT_COINS_TREE)?;
     let coin_roots_db = db_lookup(cid, CONSENSUS_CONTRACT_COIN_ROOTS_TREE)?;
+    let unstaked_coins_db = db_lookup(cid, CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE)?;
 
     // ===================================
     // Perform the actual state transition
     // ===================================
 
     msg!("[ConsensusProposalV1] Validating anonymous input");
-    let input = &params.input;
-    let output = &params.output;
 
     // The coin has passed through the grace period and is allowed to propose.
-    if params.input.epoch != 0 &&
-        get_verifying_slot_epoch() - params.input.epoch <= calculate_grace_period()
-    {
+    if input.epoch != 0 && get_verifying_slot_epoch() - input.epoch <= calculate_grace_period() {
         msg!("[ConsensusProposalV1] Error: Coin is not allowed to make proposals yet");
         return Err(ConsensusError::CoinStillInGracePeriod.into())
     }
@@ -207,11 +207,18 @@ pub(crate) fn consensus_proposal_process_instruction_v1(
 
     // 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))? {
+    let coin = serialize(&output.coin);
+    if db_contains_key(coins_db, &coin)? {
         msg!("[ConsensusProposalV1] Error: Duplicate coin found in output");
         return Err(MoneyError::DuplicateCoin.into())
     }
 
+    // Check that the coin hasn't existed before in unstake set.
+    if db_contains_key(unstaked_coins_db, &coin)? {
+        msg!("[ConsensusProposalV1] Error: Unstaked coin found in output");
+        return Err(MoneyError::DuplicateCoin.into())
+    }
+
     // At this point the state transition has passed, so we create a state update
     let update = ConsensusProposalUpdateV1 { nullifier: input.nullifier, coin: output.coin };
     let mut update_data = vec![];

+ 12 - 4
src/contract/consensus/src/entrypoint/stake_v1.rs

@@ -21,8 +21,8 @@ use darkfi_money_contract::{
     model::{ConsensusStakeParamsV1, ConsensusStakeUpdateV1, MoneyStakeParamsV1, PALLAS_ZERO},
     CONSENSUS_CONTRACT_COINS_TREE, CONSENSUS_CONTRACT_COIN_MERKLE_TREE,
     CONSENSUS_CONTRACT_COIN_ROOTS_TREE, CONSENSUS_CONTRACT_INFO_TREE,
-    CONSENSUS_CONTRACT_ZKAS_MINT_NS_V1, MONEY_CONTRACT_COIN_ROOTS_TREE,
-    MONEY_CONTRACT_NULLIFIERS_TREE,
+    CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE, CONSENSUS_CONTRACT_ZKAS_MINT_NS_V1,
+    MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_NULLIFIERS_TREE,
 };
 use darkfi_sdk::{
     crypto::{pasta_prelude::*, ContractId, MerkleNode, CONSENSUS_CONTRACT_ID, MONEY_CONTRACT_ID},
@@ -83,6 +83,7 @@ pub(crate) fn consensus_stake_process_instruction_v1(
     // Access the necessary databases where there is information to
     // validate this state transition.
     let consensus_coins_db = db_lookup(cid, CONSENSUS_CONTRACT_COINS_TREE)?;
+    let consensus_unstaked_coins_db = db_lookup(cid, CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE)?;
     let money_nullifiers_db = db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_NULLIFIERS_TREE)?;
     let money_coin_roots_db = db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_COIN_ROOTS_TREE)?;
 
@@ -107,7 +108,7 @@ pub(crate) fn consensus_stake_process_instruction_v1(
         return Err(MoneyError::TransferMerkleRootNotFound.into())
     }
 
-    // The nullifiers should already exist. It is the double-mint protection.
+    // The nullifiers should not already exist. It is the double-mint protection.
     if db_contains_key(money_nullifiers_db, &serialize(&input.nullifier))? {
         msg!("[ConsensusStakeV1] Error: Missing nullifier");
         return Err(MoneyError::StakeMissingNullifier.into())
@@ -150,11 +151,18 @@ pub(crate) fn consensus_stake_process_instruction_v1(
 
     // 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(consensus_coins_db, &serialize(&output.coin))? {
+    let coin = serialize(&output.coin);
+    if db_contains_key(consensus_coins_db, &coin)? {
         msg!("[ConsensusStakeV1] Error: Duplicate coin found in output");
         return Err(MoneyError::DuplicateCoin.into())
     }
 
+    // Check that the coin hasn't existed before in unstake set.
+    if db_contains_key(consensus_unstaked_coins_db, &coin)? {
+        msg!("[ConsensusStakeV1] Error: Unstaked coin found in output");
+        return Err(MoneyError::DuplicateCoin.into())
+    }
+
     // Create a state update.
     let update = ConsensusStakeUpdateV1 { coin: output.coin };
     let mut update_data = vec![];

+ 187 - 0
src/contract/consensus/src/entrypoint/unstake_request_v1.rs

@@ -0,0 +1,187 @@
+/* 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, CONSENSUS_CONTRACT_COIN_MERKLE_TREE,
+    CONSENSUS_CONTRACT_COIN_ROOTS_TREE, CONSENSUS_CONTRACT_INFO_TREE,
+    CONSENSUS_CONTRACT_NULLIFIERS_TREE, CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE,
+    CONSENSUS_CONTRACT_ZKAS_BURN_NS_V1, CONSENSUS_CONTRACT_ZKAS_MINT_NS_V1,
+};
+use darkfi_sdk::{
+    crypto::{pasta_prelude::*, ContractId, MerkleNode},
+    db::{db_contains_key, db_lookup, db_set},
+    error::{ContractError, ContractResult},
+    merkle_add, msg,
+    pasta::pallas,
+    util::get_verifying_slot_epoch,
+    ContractCall,
+};
+use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
+
+use crate::{
+    error::ConsensusError,
+    model::{calculate_grace_period, ConsensusProposalUpdateV1},
+    ConsensusFunction,
+};
+
+/// `get_metadata` function for `Consensus::UnstakeRequestV1`
+pub(crate) fn consensus_unstake_request_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..])?;
+    let input = &params.input;
+    let output = &params.output;
+
+    // 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![input.signature_public];
+
+    // Grab the pedersen commitments and signature pubkeys from the
+    // anonymous input
+    let value_coords = input.value_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((
+        CONSENSUS_CONTRACT_ZKAS_BURN_NS_V1.to_string(),
+        vec![
+            input.nullifier.inner(),
+            input.epoch.into(),
+            sig_x,
+            sig_y,
+            input.merkle_root.inner(),
+            *value_coords.x(),
+            *value_coords.y(),
+        ],
+    ));
+
+    // Grab the minting epoch of the verifying slot
+    let epoch = get_verifying_slot_epoch();
+
+    // Grab the pedersen commitment from the anonymous output
+    let value_coords = output.value_commit.to_affine().coordinates().unwrap();
+
+    zk_public_inputs.push((
+        CONSENSUS_CONTRACT_ZKAS_MINT_NS_V1.to_string(),
+        vec![epoch.into(), output.coin.inner(), *value_coords.x(), *value_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::UnstakeRequestV1`
+pub(crate) fn consensus_unstake_request_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..])?;
+    let input = &params.input;
+    let output = &params.output;
+
+    // Access the necessary databases where there is information to
+    // validate this state transition.
+    let coins_roots_db = db_lookup(cid, CONSENSUS_CONTRACT_COIN_ROOTS_TREE)?;
+    let nullifiers_db = db_lookup(cid, CONSENSUS_CONTRACT_NULLIFIERS_TREE)?;
+    let unstaked_coins_db = db_lookup(cid, CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE)?;
+
+    // ===================================
+    // Perform the actual state transition
+    // ===================================
+
+    msg!("[ConsensusUnstakeRequestV1] Validating anonymous input");
+
+    // The coin has passed through the grace period and is allowed to request unstake.
+    if input.epoch != 0 && get_verifying_slot_epoch() - input.epoch <= calculate_grace_period() {
+        msg!("[ConsensusUnstakeRequestV1] Error: Coin is not allowed to request unstake yet");
+        return Err(ConsensusError::CoinStillInGracePeriod.into())
+    }
+
+    // The Merkle root is used to know whether this is a coin that
+    // existed in a previous state.
+    if !db_contains_key(coins_roots_db, &serialize(&input.merkle_root))? {
+        msg!("[ConsensusUnstakeRequestV1] 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!("[ConsensusUnstakeRequestV1] Error: Duplicate nullifier found");
+        return Err(MoneyError::DuplicateNullifier.into())
+    }
+
+    msg!("[ConsensusUnstakeRequestV1] Validating anonymous output");
+
+    // Verify value commits match
+    if output.value_commit != input.value_commit {
+        msg!("[ConsensusUnstakeRequestV1] Error: Value commitments do not match");
+        return Err(MoneyError::ValueMismatch.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(unstaked_coins_db, &serialize(&output.coin))? {
+        msg!("[ConsensusUnstakeRequestV1] Error: Duplicate coin found in output");
+        return Err(MoneyError::DuplicateCoin.into())
+    }
+
+    // At this point the state transition has passed, so we create a state update
+    let update = ConsensusProposalUpdateV1 { nullifier: input.nullifier, coin: output.coin };
+    let mut update_data = vec![];
+    update_data.write_u8(ConsensusFunction::UnstakeRequestV1 as u8)?;
+    update.encode(&mut update_data)?;
+
+    // and return it
+    Ok(update_data)
+}
+
+/// `process_update` function for `Consensus::UnstakeRequestV1`
+pub(crate) fn consensus_unstake_request_process_update_v1(
+    cid: ContractId,
+    update: ConsensusProposalUpdateV1,
+) -> ContractResult {
+    // Grab all necessary db handles for where we want to write
+    let nullifiers_db = db_lookup(cid, CONSENSUS_CONTRACT_NULLIFIERS_TREE)?;
+    let info_db = db_lookup(cid, CONSENSUS_CONTRACT_INFO_TREE)?;
+    let coin_roots_db = db_lookup(cid, CONSENSUS_CONTRACT_COIN_ROOTS_TREE)?;
+    let unstaked_coins_db = db_lookup(cid, CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE)?;
+
+    msg!("[ConsensusUnstakeRequestV1] Adding new nullifier to the set");
+    db_set(nullifiers_db, &serialize(&update.nullifier), &[])?;
+
+    msg!("[ConsensusUnstakeRequestV1] Adding new coin to the set");
+    db_set(unstaked_coins_db, &serialize(&update.coin), &[])?;
+
+    msg!("[ConsensusUnstakeRequestV1] 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(())
+}

+ 18 - 4
src/contract/consensus/src/entrypoint/unstake_v1.rs

@@ -20,7 +20,7 @@ use darkfi_money_contract::{
     error::MoneyError,
     model::{ConsensusUnstakeParamsV1, ConsensusUnstakeUpdateV1, MoneyUnstakeParamsV1},
     CONSENSUS_CONTRACT_COIN_ROOTS_TREE, CONSENSUS_CONTRACT_NULLIFIERS_TREE,
-    CONSENSUS_CONTRACT_ZKAS_BURN_NS_V1,
+    CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE, CONSENSUS_CONTRACT_ZKAS_BURN_NS_V1,
 };
 use darkfi_sdk::{
     crypto::{pasta_prelude::*, ContractId, MONEY_CONTRACT_ID},
@@ -28,11 +28,12 @@ use darkfi_sdk::{
     error::{ContractError, ContractResult},
     msg,
     pasta::pallas,
+    util::get_verifying_slot_epoch,
     ContractCall,
 };
 use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
 
-use crate::ConsensusFunction;
+use crate::{error::ConsensusError, model::calculate_grace_period, ConsensusFunction};
 
 /// `get_metadata` function for `Consensus::UnstakeV1`
 pub(crate) fn consensus_unstake_get_metadata_v1(
@@ -87,20 +88,33 @@ pub(crate) fn consensus_unstake_process_instruction_v1(
 ) -> Result<Vec<u8>, ContractError> {
     let self_ = &calls[call_idx as usize];
     let params: ConsensusUnstakeParamsV1 = deserialize(&self_.data[1..])?;
+    let input = &params.input;
 
     // 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)?;
+    let unstaked_coins_db = db_lookup(cid, CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE)?;
 
     // ===================================
     // Perform the actual state transition
     // ===================================
 
     msg!("[ConsensusUnstakeV1] Validating anonymous input");
-    let input = &params.input;
 
-    // The Merkle root is used to know whether this is a coin that
+    // The coin has passed through the grace period and is allowed to get unstaked.
+    if get_verifying_slot_epoch() - input.epoch <= calculate_grace_period() {
+        msg!("[ConsensusUnstakeV1] Error: Coin is not allowed to get unstaked yet");
+        return Err(ConsensusError::CoinStillInGracePeriod.into())
+    }
+
+    // Check that the coin exists in unstake set.
+    if !db_contains_key(unstaked_coins_db, &serialize(&params.coin))? {
+        msg!("[GenesisStakeV1] Error: Unstaked coin is not in unstake set");
+        return Err(ConsensusError::CoinNotInUnstakeSet.into())
+    }
+
+    // The Merkle root is used to know whether this is an unstaked coin that
     // existed in a previous state.
     if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? {
         msg!("[ConsensusUnstakeV1] Error: Merkle root not found in previous state");

+ 4 - 0
src/contract/consensus/src/error.rs

@@ -28,6 +28,9 @@ pub enum ConsensusError {
 
     #[error("Coin is still in grace period")]
     CoinStillInGracePeriod,
+
+    #[error("Coin doesn't exist in unstake set")]
+    CoinNotInUnstakeSet,
 }
 
 impl From<ConsensusError> for ContractError {
@@ -36,6 +39,7 @@ impl From<ConsensusError> for ContractError {
             ConsensusError::ProposalMissingSlotCheckpoint => Self::Custom(1),
             ConsensusError::ProposalErroneousVrfProof => Self::Custom(2),
             ConsensusError::CoinStillInGracePeriod => Self::Custom(3),
+            ConsensusError::CoinNotInUnstakeSet => Self::Custom(4),
         }
     }
 }

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

@@ -27,7 +27,8 @@ pub enum ConsensusFunction {
     GenesisStakeV1 = 0x00,
     StakeV1 = 0x01,
     ProposalV1 = 0x02,
-    UnstakeV1 = 0x03,
+    UnstakeRequestV1 = 0x03,
+    UnstakeV1 = 0x04,
 }
 
 impl TryFrom<u8> for ConsensusFunction {
@@ -38,7 +39,8 @@ impl TryFrom<u8> for ConsensusFunction {
             0x00 => Ok(Self::GenesisStakeV1),
             0x01 => Ok(Self::StakeV1),
             0x02 => Ok(Self::ProposalV1),
-            0x03 => Ok(Self::UnstakeV1),
+            0x03 => Ok(Self::UnstakeRequestV1),
+            0x04 => Ok(Self::UnstakeV1),
             _ => Err(ContractError::InvalidFunction),
         }
     }

+ 11 - 53
src/contract/consensus/src/model.rs

@@ -16,11 +16,9 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use darkfi_money_contract::model::{
-    ClearInput, Coin, ConsensusInput, ConsensusOutput, Input, Output,
-};
+use darkfi_money_contract::model::{ClearInput, Coin, ConsensusInput, ConsensusOutput, Output};
 use darkfi_sdk::{
-    crypto::{ecvrf::VrfProof, Nullifier, PublicKey},
+    crypto::{ecvrf::VrfProof, Nullifier},
     pasta::pallas,
 };
 use darkfi_serial::{SerialDecodable, SerialEncodable};
@@ -34,55 +32,6 @@ pub struct ConsensusGenesisStakeParamsV1 {
     pub output: ConsensusOutput,
 }
 
-/// Parameters for `Consensus::ProposalBurn`
-#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
-pub struct ConsensusProposalBurnParamsV1 {
-    /// Blinding factor for `token_id`
-    pub token_blind: pallas::Scalar,
-    /// Anonymous input
-    pub input: Input,
-    /// Burnt coin public key used in VRF
-    pub public_key: PublicKey,
-}
-
-/// Parameters for `Consensus::ProposalReward`
-#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
-pub struct ConsensusProposalRewardParamsV1 {
-    /// Anonymous input of `Consensus::ProposalBurn`
-    pub burnt_input: Input,
-    /// Burnt coin public key used in VRF
-    pub burnt_public_key: PublicKey,
-    /// Burnt token revealed info of `Consensus::ProposalMint`
-    pub mint_input: ConsensusInput,
-    /// Anonymous output
-    pub output: Output,
-    /// Pedersen commitment for the output's serial number
-    pub new_serial_commit: pallas::Point,
-    /// Rewarded slot
-    pub slot: u64,
-    /// VRF proof for eta calculation
-    pub vrf_proof: VrfProof,
-    /// Coin y
-    pub y: pallas::Base,
-    /// Lottery rho used
-    pub rho: pallas::Base,
-}
-
-/// Parameters for `Consensus::ProposalMint`
-#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
-pub struct ConsensusProposalMintParamsV1 {
-    /// Burnt token revealed info
-    pub input: ConsensusInput,
-    /// Anonymous output
-    pub output: Output,
-    /// Pedersen commitment for the output's serial number
-    pub serial_commit: pallas::Point,
-}
-
-/// State update for `Consensus::Reward`
-#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
-pub struct ConsensusProposalRewardUpdateV1 {}
-
 /// Parameters for `Consensus::Proposal`
 #[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
 pub struct ConsensusProposalParamsV1 {
@@ -115,6 +64,15 @@ pub struct ConsensusProposalUpdateV1 {
     pub coin: Coin,
 }
 
+/// Parameters for `Consensus::UnstakeRequest`
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub struct ConsensusUnstakeRequestParamsV1 {
+    /// Burnt token revealed info
+    pub input: ConsensusInput,
+    /// Anonymous output
+    pub output: Output,
+}
+
 /// Consensus parameters configuration.
 /// Note: Always verify `pallas::Base` are correct, in case of changes,
 /// using pallas_constants tool.

+ 59 - 11
src/contract/consensus/tests/genesis_stake_unstake.rs

@@ -27,7 +27,7 @@
 use darkfi::Result;
 use log::info;
 
-use darkfi_consensus_contract::model::REWARD;
+use darkfi_consensus_contract::model::{calculate_grace_period, EPOCH_LENGTH, REWARD};
 
 mod harness;
 use harness::{init_logger, ConsensusTestHarness, Holder};
@@ -40,7 +40,7 @@ async fn consensus_contract_genesis_stake_unstake() -> Result<()> {
     const ALICE_INITIAL: u64 = 1000;
 
     // Slot to verify against
-    let current_slot = 0;
+    let mut current_slot = 0;
 
     // Initialize harness
     let mut th = ConsensusTestHarness::new().await?;
@@ -51,14 +51,14 @@ async fn consensus_contract_genesis_stake_unstake() -> Result<()> {
     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)?;
+        th.genesis_stake(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(
+    th.execute_erroneous_genesis_stake_txs(
         Holder::Alice,
         vec![genesis_stake_tx.clone(), genesis_stake_tx.clone()],
         current_slot,
@@ -69,7 +69,7 @@ async fn consensus_contract_genesis_stake_unstake() -> Result<()> {
     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(
+    th.execute_erroneous_genesis_stake_txs(
         Holder::Alice,
         vec![genesis_stake_tx.clone()],
         current_slot + 1,
@@ -83,7 +83,7 @@ async fn consensus_contract_genesis_stake_unstake() -> Result<()> {
     info!(target: "consensus", "[Faucet] ================================");
     info!(target: "consensus", "[Faucet] Executing Alice genesis stake tx");
     info!(target: "consensus", "[Faucet] ================================");
-    th.execute_genesis_stake_native_tx(
+    th.execute_genesis_stake_tx(
         Holder::Faucet,
         genesis_stake_tx.clone(),
         &genesis_stake_params,
@@ -94,7 +94,7 @@ async fn consensus_contract_genesis_stake_unstake() -> Result<()> {
     info!(target: "consensus", "[Alice] ================================");
     info!(target: "consensus", "[Alice] Executing Alice genesis stake tx");
     info!(target: "consensus", "[Alice] ================================");
-    th.execute_genesis_stake_native_tx(
+    th.execute_genesis_stake_tx(
         Holder::Alice,
         genesis_stake_tx,
         &genesis_stake_params,
@@ -146,23 +146,71 @@ async fn consensus_contract_genesis_stake_unstake() -> Result<()> {
     // Verify values match
     assert!((alice_staked_oc.note.value + REWARD) == alice_rewarded_staked_oc.note.value);
 
+    // We progress after grace period
+    current_slot += calculate_grace_period() * EPOCH_LENGTH;
+    th.generate_slot_checkpoint(current_slot).await?;
+
+    // Alice can request for her owncoin to get unstaked
+    info!(target: "consensus", "[Alice] ===========================");
+    info!(target: "consensus", "[Alice] Building unstake request tx");
+    info!(target: "consensus", "[Alice] ===========================");
+    let (unstake_request_tx, unstake_request_params, unstake_request_secret_key) =
+        th.unstake_request(Holder::Alice, current_slot, alice_rewarded_staked_oc.clone()).await?;
+
+    info!(target: "consensus", "[Faucet] ==================================");
+    info!(target: "consensus", "[Faucet] Executing Alice unstake request tx");
+    info!(target: "consensus", "[Faucet] ==================================");
+    th.execute_unstake_request_tx(
+        Holder::Faucet,
+        unstake_request_tx.clone(),
+        &unstake_request_params,
+        current_slot,
+    )
+    .await?;
+
+    info!(target: "consensus", "[Alice] ==================================");
+    info!(target: "consensus", "[Alice] Executing Alice unstake request tx");
+    info!(target: "consensus", "[Alice] ==================================");
+    th.execute_unstake_request_tx(
+        Holder::Alice,
+        unstake_request_tx,
+        &unstake_request_params,
+        current_slot,
+    )
+    .await?;
+
+    th.assert_trees();
+
+    // Gather new unstake request owncoin
+    let alice_unstake_request_oc = th.gather_consensus_owncoin(
+        Holder::Alice,
+        unstake_request_params.output,
+        Some(unstake_request_secret_key),
+    )?;
+
+    // Verify values match
+    assert!(alice_rewarded_staked_oc.note.value == alice_unstake_request_oc.note.value);
+
+    // We progress after grace period
+    current_slot += (calculate_grace_period() * EPOCH_LENGTH) + EPOCH_LENGTH;
+
     // 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, unstake_secret_key) =
-        th.unstake_native(Holder::Alice, alice_rewarded_staked_oc.clone())?;
+        th.unstake(Holder::Alice, alice_unstake_request_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)
+    th.execute_unstake_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?;
+    th.execute_unstake_tx(Holder::Alice, unstake_tx, &unstake_params, current_slot).await?;
 
     th.assert_trees();
 
@@ -171,7 +219,7 @@ async fn consensus_contract_genesis_stake_unstake() -> Result<()> {
         th.gather_owncoin(Holder::Alice, unstake_params.output, Some(unstake_secret_key))?;
 
     // Verify values match
-    assert!(alice_rewarded_staked_oc.note.value == alice_unstaked_oc.note.value);
+    assert!(alice_unstake_request_oc.note.value == alice_unstaked_oc.note.value);
 
     // Statistics
     th.statistics();

+ 110 - 8
src/contract/consensus/tests/harness.rs

@@ -48,6 +48,7 @@ use darkfi_consensus_contract::{
     client::{
         genesis_stake_v1::ConsensusGenesisStakeCallBuilder,
         proposal_v1::ConsensusProposalCallBuilder, stake_v1::ConsensusStakeCallBuilder,
+        unstake_request_v1::ConsensusUnstakeRequestCallBuilder,
         unstake_v1::ConsensusUnstakeCallBuilder,
     },
     model::{ConsensusGenesisStakeParamsV1, ConsensusProposalParamsV1},
@@ -99,6 +100,7 @@ pub enum TxAction {
     GenesisStake,
     Stake,
     Proposal,
+    UnstakeRequest,
     Unstake,
 }
 
@@ -244,6 +246,7 @@ impl ConsensusTestHarness {
         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::UnstakeRequest, TxActionBenchmarks::new());
         tx_action_benchmarks.insert(TxAction::Unstake, TxActionBenchmarks::new());
 
         Ok(Self { holders, proving_keys, tx_action_benchmarks })
@@ -323,7 +326,7 @@ impl ConsensusTestHarness {
         Ok(())
     }
 
-    pub fn genesis_stake_native(
+    pub fn genesis_stake(
         &mut self,
         holder: Holder,
         amount: u64,
@@ -368,7 +371,7 @@ impl ConsensusTestHarness {
         Ok((genesis_stake_tx, genesis_stake_params))
     }
 
-    pub async fn execute_genesis_stake_native_tx(
+    pub async fn execute_genesis_stake_tx(
         &mut self,
         holder: Holder,
         tx: Transaction,
@@ -389,7 +392,7 @@ impl ConsensusTestHarness {
         Ok(())
     }
 
-    pub async fn execute_erroneous_genesis_stake_native_txs(
+    pub async fn execute_erroneous_genesis_stake_txs(
         &mut self,
         holder: Holder,
         txs: Vec<Transaction>,
@@ -409,10 +412,10 @@ impl ConsensusTestHarness {
         Ok(())
     }
 
-    pub fn stake_native(
+    pub async fn stake(
         &mut self,
         holder: Holder,
-        epoch: u64,
+        slot: u64,
         owncoin: OwnCoin,
     ) -> Result<(Transaction, ConsensusStakeParamsV1, SecretKey)> {
         let wallet = self.holders.get_mut(&holder).unwrap();
@@ -420,6 +423,7 @@ impl ConsensusTestHarness {
             self.proving_keys.get(&CONSENSUS_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
         let (burn_pk, burn_zkbin) = self.proving_keys.get(&MONEY_CONTRACT_ZKAS_BURN_NS_V1).unwrap();
         let tx_action_benchmark = self.tx_action_benchmarks.get_mut(&TxAction::Stake).unwrap();
+        let epoch = wallet.state.read().await.consensus.time_keeper.slot_epoch(slot);
         let timer = Instant::now();
 
         // Building Money::Stake params
@@ -487,7 +491,7 @@ impl ConsensusTestHarness {
         Ok((stake_tx, consensus_stake_params, consensus_stake_secret_key))
     }
 
-    pub async fn execute_stake_native_tx(
+    pub async fn execute_stake_tx(
         &mut self,
         holder: Holder,
         tx: Transaction,
@@ -595,7 +599,103 @@ impl ConsensusTestHarness {
         Ok(())
     }
 
-    pub fn unstake_native(
+    pub async fn unstake_request(
+        &mut self,
+        holder: Holder,
+        slot: u64,
+        staked_oc: ConsensusOwnCoin,
+    ) -> Result<(Transaction, ConsensusStakeParamsV1, SecretKey)> {
+        let wallet = self.holders.get_mut(&holder).unwrap();
+        let (burn_pk, burn_zkbin) =
+            self.proving_keys.get(&CONSENSUS_CONTRACT_ZKAS_BURN_NS_V1).unwrap();
+        let (mint_pk, mint_zkbin) =
+            self.proving_keys.get(&CONSENSUS_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
+        let tx_action_benchmark =
+            self.tx_action_benchmarks.get_mut(&TxAction::UnstakeRequest).unwrap();
+        let epoch = wallet.state.read().await.consensus.time_keeper.slot_epoch(slot);
+        let timer = Instant::now();
+
+        // Building Consensus::Unstake params
+        let unstake_request_call_debris = ConsensusUnstakeRequestCallBuilder {
+            coin: staked_oc.clone(),
+            epoch,
+            tree: wallet.consensus_merkle_tree.clone(),
+            burn_zkbin: burn_zkbin.clone(),
+            burn_pk: burn_pk.clone(),
+            mint_zkbin: mint_zkbin.clone(),
+            mint_pk: mint_pk.clone(),
+        }
+        .build()?;
+        let (unstake_request_params, unstake_request_proofs, unstake_request_secret_key) = (
+            unstake_request_call_debris.params,
+            unstake_request_call_debris.proofs,
+            unstake_request_call_debris.signature_secret,
+        );
+
+        // Building unstake request tx
+        let mut data = vec![ConsensusFunction::UnstakeRequestV1 as u8];
+        unstake_request_params.encode(&mut data)?;
+        let call = ContractCall { contract_id: *CONSENSUS_CONTRACT_ID, data };
+        let calls = vec![call];
+        let proofs = vec![unstake_request_proofs];
+        let mut unstake_request_tx = Transaction { calls, proofs, signatures: vec![] };
+        let sigs = unstake_request_tx.create_sigs(&mut OsRng, &[unstake_request_secret_key])?;
+        unstake_request_tx.signatures = vec![sigs];
+        tx_action_benchmark.creation_times.push(timer.elapsed());
+
+        // Calculate transaction sizes
+        let encoded: Vec<u8> = serialize(&unstake_request_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((unstake_request_tx, unstake_request_params, unstake_request_secret_key))
+    }
+
+    pub async fn execute_unstake_request_tx(
+        &mut self,
+        holder: Holder,
+        tx: Transaction,
+        params: &ConsensusStakeParamsV1,
+        slot: u64,
+    ) -> Result<()> {
+        let wallet = self.holders.get_mut(&holder).unwrap();
+        let tx_action_benchmark =
+            self.tx_action_benchmarks.get_mut(&TxAction::UnstakeRequest).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_unstake_request_txs(
+        &mut self,
+        holder: Holder,
+        txs: Vec<Transaction>,
+        slot: u64,
+        erroneous: usize,
+    ) -> Result<()> {
+        let wallet = self.holders.get_mut(&holder).unwrap();
+        let tx_action_benchmark =
+            self.tx_action_benchmarks.get_mut(&TxAction::UnstakeRequest).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 unstake(
         &mut self,
         holder: Holder,
         staked_oc: ConsensusOwnCoin,
@@ -669,7 +769,7 @@ impl ConsensusTestHarness {
         Ok((unstake_tx, money_unstake_params, consensus_unstake_secret_key))
     }
 
-    pub async fn execute_unstake_native_tx(
+    pub async fn execute_unstake_tx(
         &mut self,
         holder: Holder,
         tx: Transaction,
@@ -770,9 +870,11 @@ impl ConsensusTestHarness {
         let faucet = self.holders.get(&Holder::Faucet).unwrap();
         let money_root = faucet.merkle_tree.root(0).unwrap();
         let consensus_root = faucet.consensus_merkle_tree.root(0).unwrap();
+        let consensus_unstake_root = faucet.consensus_merkle_tree.root(0).unwrap();
         for wallet in self.holders.values() {
             assert!(money_root == wallet.merkle_tree.root(0).unwrap());
             assert!(consensus_root == wallet.consensus_merkle_tree.root(0).unwrap());
+            assert!(consensus_unstake_root == wallet.consensus_merkle_tree.root(0).unwrap());
         }
     }
 

+ 82 - 21
src/contract/consensus/tests/stake_unstake.rs

@@ -43,7 +43,6 @@ async fn consensus_contract_stake_unstake() -> Result<()> {
 
     // Slot to verify against
     let mut current_slot = 1;
-    let mut current_epoch = 1;
 
     // Initialize harness
     let mut th = ConsensusTestHarness::new().await?;
@@ -73,18 +72,17 @@ async fn consensus_contract_stake_unstake() -> Result<()> {
     info!(target: "consensus", "[Alice] Building stake tx");
     info!(target: "consensus", "[Alice] =================");
     let (stake_tx, stake_params, stake_secret_key) =
-        th.stake_native(Holder::Alice, current_epoch, alice_oc.clone())?;
+        th.stake(Holder::Alice, current_slot, alice_oc.clone()).await?;
 
     info!(target: "consensus", "[Faucet] ========================");
     info!(target: "consensus", "[Faucet] Executing Alice stake tx");
     info!(target: "consensus", "[Faucet] ========================");
-    th.execute_stake_native_tx(Holder::Faucet, stake_tx.clone(), &stake_params, current_slot)
-        .await?;
+    th.execute_stake_tx(Holder::Faucet, stake_tx.clone(), &stake_params, current_slot).await?;
 
     info!(target: "consensus", "[Alice] ========================");
     info!(target: "consensus", "[Alice] Executing Alice stake tx");
     info!(target: "consensus", "[Alice] ========================");
-    th.execute_stake_native_tx(Holder::Alice, stake_tx, &stake_params, current_slot).await?;
+    th.execute_stake_tx(Holder::Alice, stake_tx, &stake_params, current_slot).await?;
 
     th.assert_trees();
 
@@ -97,27 +95,18 @@ async fn consensus_contract_stake_unstake() -> Result<()> {
 
     // We progress one slot
     current_slot += 1;
-
-    // We generate current slot checkpoint to simulate its proposal
     let slot_checkpoint = th.generate_slot_checkpoint(current_slot).await?;
 
     // Since alice didn't wait for the grace period to pass, her proposal should fail
-    info!(target: "consensus", "[Alice] ====================");
-    info!(target: "consensus", "[Alice] Building proposal tx");
-    info!(target: "consensus", "[Alice] ====================");
-    let (proposal_tx, _, _) =
-        th.proposal(Holder::Alice, slot_checkpoint, alice_staked_oc.clone())?;
-
     info!(target: "consensus", "[Malicious] =====================================");
     info!(target: "consensus", "[Malicious] Checking proposal before grace period");
     info!(target: "consensus", "[Malicious] =====================================");
+    let (proposal_tx, _, _) =
+        th.proposal(Holder::Alice, slot_checkpoint, alice_staked_oc.clone())?;
     th.execute_erroneous_proposal_txs(Holder::Alice, vec![proposal_tx], current_slot, 1).await?;
 
     // We progress after grace period
-    current_epoch += calculate_grace_period();
-    current_slot += current_epoch * EPOCH_LENGTH;
-
-    // We generate current slot checkpoint to simulate its proposal
+    current_slot += (calculate_grace_period() * EPOCH_LENGTH) + EPOCH_LENGTH;
     let slot_checkpoint = th.generate_slot_checkpoint(current_slot).await?;
 
     // With alice's current coin value she can become the slot proposer,
@@ -152,23 +141,95 @@ async fn consensus_contract_stake_unstake() -> Result<()> {
     // Verify values match
     assert!((alice_staked_oc.note.value + REWARD) == alice_rewarded_staked_oc.note.value);
 
+    // We progress one slot
+    current_slot += 1;
+    th.generate_slot_checkpoint(current_slot).await?;
+
+    // Alice can request for her owncoin to get unstaked
+    info!(target: "consensus", "[Alice] ===========================");
+    info!(target: "consensus", "[Alice] Building unstake request tx");
+    info!(target: "consensus", "[Alice] ===========================");
+    let (unstake_request_tx, unstake_request_params, unstake_request_secret_key) =
+        th.unstake_request(Holder::Alice, current_slot, alice_rewarded_staked_oc.clone()).await?;
+
+    info!(target: "consensus", "[Faucet] ==================================");
+    info!(target: "consensus", "[Faucet] Executing Alice unstake request tx");
+    info!(target: "consensus", "[Faucet] ==================================");
+    th.execute_unstake_request_tx(
+        Holder::Faucet,
+        unstake_request_tx.clone(),
+        &unstake_request_params,
+        current_slot,
+    )
+    .await?;
+
+    info!(target: "consensus", "[Alice] ==================================");
+    info!(target: "consensus", "[Alice] Executing Alice unstake request tx");
+    info!(target: "consensus", "[Alice] ==================================");
+    th.execute_unstake_request_tx(
+        Holder::Alice,
+        unstake_request_tx,
+        &unstake_request_params,
+        current_slot,
+    )
+    .await?;
+
+    th.assert_trees();
+
+    // Gather new unstake request owncoin
+    let alice_unstake_request_oc = th.gather_consensus_owncoin(
+        Holder::Alice,
+        unstake_request_params.output,
+        Some(unstake_request_secret_key),
+    )?;
+
+    // Verify values match
+    assert!(alice_rewarded_staked_oc.note.value == alice_unstake_request_oc.note.value);
+
+    // Now we will test if we can reuse token in proposal or unstake it again
+    current_slot += 1;
+    let slot_checkpoint = th.generate_slot_checkpoint(current_slot).await?;
+
+    info!(target: "consensus", "[Malicious] ========================================");
+    info!(target: "consensus", "[Malicious] Checking using unstaked coin in proposal");
+    info!(target: "consensus", "[Malicious] ========================================");
+    let (proposal_tx, _, _) =
+        th.proposal(Holder::Alice, slot_checkpoint, alice_unstake_request_oc.clone())?;
+    th.execute_erroneous_proposal_txs(Holder::Alice, vec![proposal_tx], current_slot, 1).await?;
+
+    info!(target: "consensus", "[Malicious] =============================");
+    info!(target: "consensus", "[Malicious] Checking unstaking coin again");
+    info!(target: "consensus", "[Malicious] =============================");
+    let (unstake_request_tx, _, _) =
+        th.unstake_request(Holder::Alice, current_slot, alice_unstake_request_oc.clone()).await?;
+    th.execute_erroneous_unstake_request_txs(
+        Holder::Alice,
+        vec![unstake_request_tx],
+        current_slot,
+        1,
+    )
+    .await?;
+
+    // We progress after grace period
+    current_slot += (calculate_grace_period() * EPOCH_LENGTH) + EPOCH_LENGTH;
+
     // 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, unstake_secret_key) =
-        th.unstake_native(Holder::Alice, alice_rewarded_staked_oc.clone())?;
+        th.unstake(Holder::Alice, alice_unstake_request_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)
+    th.execute_unstake_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?;
+    th.execute_unstake_tx(Holder::Alice, unstake_tx, &unstake_params, current_slot).await?;
 
     th.assert_trees();
 
@@ -177,7 +238,7 @@ async fn consensus_contract_stake_unstake() -> Result<()> {
         th.gather_owncoin(Holder::Alice, unstake_params.output, Some(unstake_secret_key))?;
 
     // Verify values match
-    assert!(alice_rewarded_staked_oc.note.value == alice_unstaked_oc.note.value);
+    assert!(alice_unstake_request_oc.note.value == alice_unstaked_oc.note.value);
 
     // Statistics
     th.statistics();

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

@@ -142,8 +142,8 @@ pub(crate) fn money_unstake_process_instruction_v1(
         return Err(MoneyError::UnstakePreviousCallNotConsensusContract.into())
     }
 
-    // Verify previous call corresponds to Consensus::UnstakeV1 (0x03)
-    if previous.data[0] != 0x03 {
+    // 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())
     }

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

@@ -95,6 +95,7 @@ pub const CONSENSUS_CONTRACT_INFO_TREE: &str = "consensus_info";
 pub const CONSENSUS_CONTRACT_COINS_TREE: &str = "consensus_coins";
 pub const CONSENSUS_CONTRACT_COIN_ROOTS_TREE: &str = "consensus_coin_roots";
 pub const CONSENSUS_CONTRACT_NULLIFIERS_TREE: &str = "consensus_nullifiers";
+pub const CONSENSUS_CONTRACT_UNSTAKED_COINS_TREE: &str = "consensus_unstaked_coins";
 
 // These are keys inside the consensus info tree
 pub const CONSENSUS_CONTRACT_DB_VERSION: &str = "db_version";

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

@@ -256,6 +256,8 @@ pub struct ConsensusStakeUpdateV1 {
 pub struct ConsensusUnstakeParamsV1 {
     /// Anonymous input
     pub input: ConsensusInput,
+    /// The unstaked coin
+    pub coin: Coin,
 }
 
 /// State update for `Consensus::Unstake`