From e2ad94dfffc384c77d2cb83fa2d2ce9021b12631 Mon Sep 17 00:00:00 2001 From: aggstam Date: Tue, 6 Jun 2023 17:45:28 +0300 Subject: [PATCH] contract/consensus: introduce stake timelock With this check added, a staker is only able to propose after grace(lock) period has passed --- .../consensus/src/entrypoint/proposal_v1.rs | 14 +++++-- src/contract/consensus/src/error.rs | 4 ++ src/contract/consensus/src/model.rs | 33 ++++++++++----- .../consensus/tests/genesis_stake_unstake.rs | 2 +- src/contract/consensus/tests/harness.rs | 40 ++++++++++++++++++- src/contract/consensus/tests/stake_unstake.rs | 32 ++++++++++++--- 6 files changed, 105 insertions(+), 20 deletions(-) diff --git a/src/contract/consensus/src/entrypoint/proposal_v1.rs b/src/contract/consensus/src/entrypoint/proposal_v1.rs index ca7c52e2e..f4d144b0d 100644 --- a/src/contract/consensus/src/entrypoint/proposal_v1.rs +++ b/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 = ¶ms.input; let output = ¶ms.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))? { diff --git a/src/contract/consensus/src/error.rs b/src/contract/consensus/src/error.rs index d95299a00..434993d36 100644 --- a/src/contract/consensus/src/error.rs +++ b/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 for ContractError { @@ -32,6 +35,7 @@ impl From for ContractError { match e { ConsensusError::ProposalMissingSlotCheckpoint => Self::Custom(1), ConsensusError::ProposalErroneousVrfProof => Self::Custom(2), + ConsensusError::CoinStillInGracePeriod => Self::Custom(3), } } } diff --git a/src/contract/consensus/src/model.rs b/src/contract/consensus/src/model.rs index 5d86aca3a..7295dd632 100644 --- a/src/contract/consensus/src/model.rs +++ b/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) +} diff --git a/src/contract/consensus/tests/genesis_stake_unstake.rs b/src/contract/consensus/tests/genesis_stake_unstake.rs index 54165b830..102e46a47 100644 --- a/src/contract/consensus/tests/genesis_stake_unstake.rs +++ b/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, diff --git a/src/contract/consensus/tests/harness.rs b/src/contract/consensus/tests/harness.rs index 18fb2e931..daf9cffb4 100644 --- a/src/contract/consensus/tests/harness.rs +++ b/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, + 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 { + pub async fn get_slot_checkpoint_by_slot(&self, slot: u64) -> Result { 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 { + // 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(); diff --git a/src/contract/consensus/tests/stake_unstake.rs b/src/contract/consensus/tests/stake_unstake.rs index 54cf736c5..c162a4c31 100644 --- a/src/contract/consensus/tests/stake_unstake.rs +++ b/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,