Bladeren bron

contract/consensus: introduce stake timelock

With this check added, a staker is only able to propose after grace(lock) period has passed
aggstam 3 jaren geleden
bovenliggende
commit
e2ad94dfff

+ 11 - 3
src/contract/consensus/src/entrypoint/proposal_v1.rs

@@ -27,7 +27,7 @@ use darkfi_sdk::{
     error::{ContractError, ContractResult},
     merkle_add, msg,
     pasta::{group::ff::FromUniformBytes, pallas},
-    util::get_slot_checkpoint,
+    util::{get_slot_checkpoint, get_verifying_slot_epoch},
     ContractCall,
 };
 use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
@@ -35,8 +35,8 @@ use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
 use crate::{
     error::ConsensusError,
     model::{
-        ConsensusProposalParamsV1, ConsensusProposalUpdateV1, SlotCheckpoint, HEADSTART,
-        MU_RHO_PREFIX, MU_Y_PREFIX,
+        calculate_grace_period, ConsensusProposalParamsV1, ConsensusProposalUpdateV1,
+        SlotCheckpoint, HEADSTART, MU_RHO_PREFIX, MU_Y_PREFIX,
     },
     ConsensusFunction,
 };
@@ -174,6 +174,14 @@ pub(crate) fn consensus_proposal_process_instruction_v1(
     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()
+    {
+        msg!("[ConsensusProposalV1] Error: Coin is not allowed to make proposals 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(coin_roots_db, &serialize(&input.merkle_root))? {

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

@@ -25,6 +25,9 @@ pub enum ConsensusError {
 
     #[error("Eta VRF proof couldn't be verified")]
     ProposalErroneousVrfProof,
+
+    #[error("Coin is still in grace period")]
+    CoinStillInGracePeriod,
 }
 
 impl From<ConsensusError> for ContractError {
@@ -32,6 +35,7 @@ impl From<ConsensusError> for ContractError {
         match e {
             ConsensusError::ProposalMissingSlotCheckpoint => Self::Custom(1),
             ConsensusError::ProposalErroneousVrfProof => Self::Custom(2),
+            ConsensusError::CoinStillInGracePeriod => Self::Custom(3),
         }
     }
 }

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

@@ -115,22 +115,28 @@ pub struct ConsensusProposalUpdateV1 {
     pub coin: Coin,
 }
 
-// Consensus parameters configuration.
-// Note: Always verify `pallas::Base` are correct, in case of changes,
-// using pallas_constants tool.
-// Configured reward
+/// Consensus parameters configuration.
+/// Note: Always verify `pallas::Base` are correct, in case of changes,
+/// using pallas_constants tool.
+/// Number of slots in one epoch
+pub const EPOCH_LENGTH: u64 = 10;
+/// Slot time in seconds
+pub const SLOT_TIME: u64 = 90;
+/// Grace period days target
+pub const GRACE_PERIOD_DAYS: u64 = 2;
+/// Configured reward
 pub const REWARD: u64 = 1;
-// Reward `pallas::Base`, calculated by: pallas::Base::from(REWARD)
+/// Reward `pallas::Base`, calculated by: pallas::Base::from(REWARD)
 pub const REWARD_PALLAS: pallas::Base = pallas::Base::from_raw([1, 0, 0, 0]);
-// Serial prefix, calculated by: pallas::Base::from(2)
+/// Serial prefix, calculated by: pallas::Base::from(2)
 pub const SERIAL_PREFIX: pallas::Base = pallas::Base::from_raw([2, 0, 0, 0]);
-// Seed prefix, calculated by: pallas::Base::from(3)
+/// Seed prefix, calculated by: pallas::Base::from(3)
 pub const SEED_PREFIX: pallas::Base = pallas::Base::from_raw([3, 0, 0, 0]);
-// Election seed y prefix, calculated by: pallas::Base::from(22)
+/// Election seed y prefix, calculated by: pallas::Base::from(22)
 pub const MU_Y_PREFIX: pallas::Base = pallas::Base::from_raw([22, 0, 0, 0]);
-// Election seed rho prefix, calculated by: pallas::Base::from(5)
+/// Election seed rho prefix, calculated by: pallas::Base::from(5)
 pub const MU_RHO_PREFIX: pallas::Base = pallas::Base::from_raw([5, 0, 0, 0]);
-// Lottery headstart, calculated by: darkfi::consensus::LeadCoin::headstart()
+/// Lottery headstart, calculated by: darkfi::consensus::LeadCoin::headstart()
 pub const HEADSTART: pallas::Base = pallas::Base::from_raw([
     11731824086999220879,
     11830614503713258191,
@@ -151,3 +157,10 @@ pub struct SlotCheckpoint {
     /// Slot sigma2
     pub sigma2: pallas::Base,
 }
+
+/// Auxiliary function to calculate the grace(locked) period, denominated
+/// in epochs.
+pub fn calculate_grace_period() -> u64 {
+    // 86400 seconds in a day
+    (86400 * GRACE_PERIOD_DAYS) / (SLOT_TIME * EPOCH_LENGTH)
+}

+ 1 - 1
src/contract/consensus/tests/genesis_stake_unstake.rs

@@ -112,7 +112,7 @@ async fn consensus_contract_genesis_stake_unstake() -> Result<()> {
     assert!(ALICE_INITIAL == alice_staked_oc.note.value);
 
     // We simulate the proposal of genesis slot
-    let slot_checkpoint = th.get_slot_checkpoints_by_slot(current_slot).await?;
+    let slot_checkpoint = th.get_slot_checkpoint_by_slot(current_slot).await?;
 
     // With alice's current coin value she can become the slot proposer,
     // so she creates a proposal transaction to burn her staked coin,

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

@@ -576,6 +576,25 @@ impl ConsensusTestHarness {
         Ok(())
     }
 
+    pub async fn execute_erroneous_proposal_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::Proposal).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_native(
         &mut self,
         holder: Holder,
@@ -718,7 +737,7 @@ impl ConsensusTestHarness {
         Ok(oc)
     }
 
-    pub async fn get_slot_checkpoints_by_slot(&self, slot: u64) -> Result<SlotCheckpoint> {
+    pub async fn get_slot_checkpoint_by_slot(&self, slot: u64) -> Result<SlotCheckpoint> {
         let faucet = self.holders.get(&Holder::Faucet).unwrap();
         let slot_checkpoint =
             faucet.state.read().await.blockchain.get_slot_checkpoints_by_slot(&[slot])?[0]
@@ -728,6 +747,25 @@ impl ConsensusTestHarness {
         Ok(slot_checkpoint)
     }
 
+    pub async fn generate_slot_checkpoint(&self, slot: u64) -> Result<SlotCheckpoint> {
+        // We grab the genesis slot to generate slot checkpoint
+        // using same consensus parameters
+        let genesis_slot = self.get_slot_checkpoint_by_slot(0).await?;
+        let slot_checkpoint = SlotCheckpoint {
+            slot,
+            eta: genesis_slot.eta,
+            sigma1: genesis_slot.sigma1,
+            sigma2: genesis_slot.sigma2,
+        };
+
+        // Store generated slot checkpoint
+        for wallet in self.holders.values() {
+            wallet.state.write().await.receive_slot_checkpoints(&[slot_checkpoint.clone()]).await?;
+        }
+
+        Ok(slot_checkpoint)
+    }
+
     pub fn assert_trees(&self) {
         let faucet = self.holders.get(&Holder::Faucet).unwrap();
         let money_root = faucet.merkle_tree.root(0).unwrap();

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

@@ -29,7 +29,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};
@@ -42,8 +42,8 @@ async fn consensus_contract_stake_unstake() -> Result<()> {
     const ALICE_AIRDROP: u64 = 1000;
 
     // Slot to verify against
-    let current_slot = 0;
-    let current_epoch = 0;
+    let mut current_slot = 11;
+    let mut current_epoch = 1;
 
     // Initialize harness
     let mut th = ConsensusTestHarness::new().await?;
@@ -95,8 +95,30 @@ async fn consensus_contract_stake_unstake() -> Result<()> {
     // Verify values match
     assert!(alice_oc.note.value == alice_staked_oc.note.value);
 
-    // We simulate the proposal of genesis slot
-    let slot_checkpoint = th.get_slot_checkpoints_by_slot(current_slot).await?;
+    // 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, proposal_params, proposal_secret_key) =
+        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] =====================================");
+    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
+    let slot_checkpoint = th.generate_slot_checkpoint(current_slot).await?;
 
     // With alice's current coin value she can become the slot proposer,
     // so she creates a proposal transaction to burn her staked coin,