diff --git a/mina-p2p-messages/src/v2/manual.rs b/mina-p2p-messages/src/v2/manual.rs index cd12a08a20..2eb2e4b172 100644 --- a/mina-p2p-messages/src/v2/manual.rs +++ b/mina-p2p-messages/src/v2/manual.rs @@ -1532,6 +1532,12 @@ impl From for BlockTimeTimeStableV1 { } } +impl MinaBlockHeaderStableV2 { + pub fn genesis_state_hash(&self) -> &StateHash { + &self.protocol_state.body.genesis_state_hash + } +} + impl StagedLedgerDiffBodyStableV1 { pub fn diff(&self) -> &StagedLedgerDiffDiffDiffStableV2 { &self.staged_ledger_diff.diff diff --git a/node/src/action_kind.rs b/node/src/action_kind.rs index 1009e04c8b..a74fbaebf2 100644 --- a/node/src/action_kind.rs +++ b/node/src/action_kind.rs @@ -162,6 +162,8 @@ pub enum ActionKind { CheckTimeouts, ConsensusBestTipUpdate, ConsensusBlockChainProofUpdate, + ConsensusBlockPrevalidateError, + ConsensusBlockPrevalidateSuccess, ConsensusBlockReceived, ConsensusBlockSnarkVerifyError, ConsensusBlockSnarkVerifyPending, @@ -716,7 +718,7 @@ pub enum ActionKind { } impl ActionKind { - pub const COUNT: u16 = 599; + pub const COUNT: u16 = 601; } impl std::fmt::Display for ActionKind { @@ -857,6 +859,8 @@ impl ActionKindGet for ConsensusAction { fn kind(&self) -> ActionKind { match self { Self::BlockReceived { .. } => ActionKind::ConsensusBlockReceived, + Self::BlockPrevalidateSuccess { .. } => ActionKind::ConsensusBlockPrevalidateSuccess, + Self::BlockPrevalidateError { .. } => ActionKind::ConsensusBlockPrevalidateError, Self::BlockChainProofUpdate { .. } => ActionKind::ConsensusBlockChainProofUpdate, Self::BlockSnarkVerifyPending { .. } => ActionKind::ConsensusBlockSnarkVerifyPending, Self::BlockSnarkVerifySuccess { .. } => ActionKind::ConsensusBlockSnarkVerifySuccess, diff --git a/node/src/consensus/consensus_actions.rs b/node/src/consensus/consensus_actions.rs index a35ab7d6ff..0aeb10923a 100644 --- a/node/src/consensus/consensus_actions.rs +++ b/node/src/consensus/consensus_actions.rs @@ -9,6 +9,7 @@ use snark::block_verify::SnarkBlockVerifyError; use crate::consensus::ConsensusBlockStatus; use crate::snark::block_verify::SnarkBlockVerifyId; +use crate::state::BlockPrevalidationError; pub type ConsensusActionWithMeta = redux::ActionWithMeta; pub type ConsensusActionWithMetaRef<'a> = redux::ActionWithMeta<&'a ConsensusAction>; @@ -24,6 +25,13 @@ pub enum ConsensusAction { block: Arc, chain_proof: Option<(Vec, ArcBlockWithHash)>, }, + BlockPrevalidateSuccess { + hash: StateHash, + }, + BlockPrevalidateError { + hash: StateHash, + error: BlockPrevalidationError, + }, BlockChainProofUpdate { hash: StateHash, chain_proof: (Vec, ArcBlockWithHash), @@ -71,6 +79,12 @@ impl redux::EnablingCondition for ConsensusAction { }; !block.is_genesis() && !state.consensus.blocks.contains_key(hash) }, + ConsensusAction::BlockPrevalidateSuccess { hash } + | ConsensusAction::BlockPrevalidateError { hash, .. } => state + .consensus + .blocks + .get(hash) + .map_or(false, |block| block.status.is_received()), ConsensusAction::BlockChainProofUpdate { hash, .. } => { (state.consensus.best_tip.as_ref() == Some(hash) && state.consensus.best_tip_chain_proof.is_none()) @@ -85,7 +99,7 @@ impl redux::EnablingCondition for ConsensusAction { .consensus .blocks .get(hash) - .map_or(false, |block| block.status.is_received()) + .map_or(false, |block| block.status.is_prevalidated()) && state.snark.block_verify.jobs.contains(*req_id) }, ConsensusAction::BlockSnarkVerifySuccess { hash } => { diff --git a/node/src/consensus/consensus_reducer.rs b/node/src/consensus/consensus_reducer.rs index 2db1931633..780741ab3d 100644 --- a/node/src/consensus/consensus_reducer.rs +++ b/node/src/consensus/consensus_reducer.rs @@ -1,5 +1,5 @@ use openmina_core::{ - block::BlockHash, + block::{ArcBlockWithHash, BlockHash}, bug_condition, consensus::{is_short_range_fork, long_range_fork_take, short_range_fork_take}, }; @@ -48,9 +48,33 @@ impl ConsensusState { ); // Dispatch + let (dispatcher, state) = state_context.into_dispatcher_and_state(); + + let hash = hash.clone(); + let block = ArcBlockWithHash { + hash: hash.clone(), + block: block.clone(), + }; + match state.prevalidate_block(&block) { + Ok(()) => { + dispatcher.push(ConsensusAction::BlockPrevalidateSuccess { hash }); + } + Err(error) => { + dispatcher.push(ConsensusAction::BlockPrevalidateError { hash, error }); + } + } + } + ConsensusAction::BlockPrevalidateSuccess { hash } => { + let Some(block) = state.blocks.get_mut(hash) else { + return; + }; + block.status = ConsensusBlockStatus::Prevalidated; + + // Dispatch + let block = (hash.clone(), block.block.clone()).into(); let dispatcher = state_context.into_dispatcher(); dispatcher.push(SnarkBlockVerifyAction::Init { - block: (hash.clone(), block.clone()).into(), + block, on_init: redux::callback!( on_received_block_snark_verify_init((hash: BlockHash, req_id: SnarkBlockVerifyId)) -> crate::Action { ConsensusAction::BlockSnarkVerifyPending { hash, req_id } @@ -65,6 +89,9 @@ impl ConsensusState { }), }); } + ConsensusAction::BlockPrevalidateError { hash, .. } => { + state.blocks.remove(hash); + } ConsensusAction::BlockChainProofUpdate { hash, chain_proof } => { if state.best_tip.as_ref() == Some(hash) { state.best_tip_chain_proof = Some(chain_proof.clone()); diff --git a/node/src/consensus/consensus_state.rs b/node/src/consensus/consensus_state.rs index bfa5abc1f4..39a8f2a61b 100644 --- a/node/src/consensus/consensus_state.rs +++ b/node/src/consensus/consensus_state.rs @@ -44,6 +44,7 @@ pub enum ConsensusBlockStatus { Received { time: redux::Timestamp, }, + Prevalidated, SnarkVerifyPending { time: redux::Timestamp, req_id: SnarkBlockVerifyId, @@ -73,6 +74,10 @@ impl ConsensusBlockStatus { matches!(self, Self::Received { .. }) } + pub fn is_prevalidated(&self) -> bool { + matches!(self, Self::Prevalidated) + } + pub fn is_snark_verify_pending(&self) -> bool { matches!(self, Self::SnarkVerifyPending { .. }) } @@ -167,6 +172,7 @@ impl ConsensusState { }; match &candidate.status { ConsensusBlockStatus::Received { .. } => false, + ConsensusBlockStatus::Prevalidated => false, ConsensusBlockStatus::SnarkVerifyPending { .. } => false, ConsensusBlockStatus::SnarkVerifySuccess { .. } => false, ConsensusBlockStatus::ForkRangeDetected { .. } => false, diff --git a/node/src/state.rs b/node/src/state.rs index 73fea89b61..c607487e91 100644 --- a/node/src/state.rs +++ b/node/src/state.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use std::time::Duration; use mina_p2p_messages::v2::{MinaBaseUserCommandStableV2, MinaBlockBlockStableV2}; +use openmina_core::constants::PROTOCOL_VERSION; use rand::prelude::*; use openmina_core::block::BlockWithHash; @@ -73,8 +74,27 @@ pub struct State { applied_actions_count: u64, } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum BlockPrevalidationError { + GenesisNotReady, + ReceivedTooEarly { + current_global_slot: u32, + block_global_slot: u32, + }, + ReceivedTooLate { + current_global_slot: u32, + block_global_slot: u32, + delta: u32, + }, + InvalidGenesisProtocolState, + InvalidProtocolVersion, + MismatchedProtocolVersion, + ConsantsMismatch, + InvalidDeltaBlockChainProof, +} + // Substate accessors that will be used in reducers -use openmina_core::{impl_substate_access, SubstateAccess}; +use openmina_core::{bug_condition, impl_substate_access, SubstateAccess}; impl_substate_access!(State, SnarkState, snark); impl_substate_access!(State, SnarkBlockVerifyState, snark.block_verify); @@ -356,6 +376,85 @@ impl State { }) } + pub fn prevalidate_block( + &self, + block: &ArcBlockWithHash, + ) -> Result<(), BlockPrevalidationError> { + let Some((genesis, cur_global_slot)) = + None.or_else(|| Some((self.genesis_block()?, self.cur_global_slot()?))) + else { + // we don't have genesis block. This should be impossible + // because we don't even know chain_id before we have genesis + // block, so we can't be connected to any peers from which + // we would receive a block. + bug_condition!("Tried to prevalidate a block before the genesis block was ready"); + return Err(BlockPrevalidationError::GenesisNotReady); + }; + + // received_at_valid_time + // https://github.com/minaprotocol/mina/blob/6af211ad58e9356f00ea4a636cea70aa8267c072/src/lib/consensus/proof_of_stake.ml#L2746 + { + let block_global_slot = block.global_slot(); + + let delta = genesis.constants().delta.as_u32(); + if cur_global_slot < block_global_slot { + // Too_early + return Err(BlockPrevalidationError::ReceivedTooEarly { + current_global_slot: cur_global_slot, + block_global_slot, + }); + } else if cur_global_slot.saturating_sub(block_global_slot) > delta { + // Too_late + return Err(BlockPrevalidationError::ReceivedTooLate { + current_global_slot: cur_global_slot, + block_global_slot, + delta, + }); + } + } + + if block.header().genesis_state_hash() != genesis.hash() { + return Err(BlockPrevalidationError::InvalidGenesisProtocolState); + } + + let (protocol_versions_are_valid, protocol_version_matches_daemon) = { + let min_transaction_version = 1.into(); + let v = &block.header().current_protocol_version; + let nv = block + .header() + .proposed_protocol_version_opt + .as_ref() + .unwrap_or(v); + + // Our version values are unsigned, so there is no need to check that the + // other parts are not negative. + let valid = v.transaction >= min_transaction_version + && nv.transaction >= min_transaction_version; + let compatible = v.transaction == PROTOCOL_VERSION.transaction + && v.network == PROTOCOL_VERSION.network; + + (valid, compatible) + }; + + if !protocol_versions_are_valid { + return Err(BlockPrevalidationError::InvalidProtocolVersion); + } else if !protocol_version_matches_daemon { + return Err(BlockPrevalidationError::MismatchedProtocolVersion); + } + + // NOTE: currently these cannot change between blocks, but that + // may not always be true? + if block.constants() != genesis.constants() { + return Err(BlockPrevalidationError::ConsantsMismatch); + } + + // TODO(tizoc): check for InvalidDeltaBlockChainProof + // https://github.com/MinaProtocol/mina/blob/d800da86a764d8d37ffb8964dd8d54d9f522b358/src/lib/mina_block/validation.ml#L369 + // https://github.com/MinaProtocol/mina/blob/d800da86a764d8d37ffb8964dd8d54d9f522b358/src/lib/transition_chain_verifier/transition_chain_verifier.ml + + Ok(()) + } + pub fn should_log_node_id(&self) -> bool { self.config.testing_run } diff --git a/node/testing/src/scenarios/solo_node/bootstrap.rs b/node/testing/src/scenarios/solo_node/bootstrap.rs index 5293592b93..9ce1101e73 100644 --- a/node/testing/src/scenarios/solo_node/bootstrap.rs +++ b/node/testing/src/scenarios/solo_node/bootstrap.rs @@ -1,6 +1,7 @@ use std::time::Duration; use node::transition_frontier::sync::TransitionFrontierSyncState; +use openmina_core::constants::constraint_constants; use redux::Instant; use crate::{ @@ -19,6 +20,24 @@ use crate::{ #[derive(documented::Documented, Default, Clone, Copy)] pub struct SoloNodeBootstrap; +// TODO(tizoc): this is ugly, do a cleaner conversion or figure out a better way. +// This test will fail if we don't start with this as the initial time because +// the time validation for the first block will reject it. +fn first_block_slot_timestamp_nanos(config: &RustNodeTestingConfig) -> u64 { + let first_block_global_slot = 46891; // Update if replay changes + let protocol_constants = config.genesis.protocol_constants().unwrap(); + let genesis_timestamp_ms = protocol_constants.genesis_state_timestamp.0.as_u64(); + let milliseconds_per_slot = constraint_constants().block_window_duration_ms; + let first_block_global_slot_delta_ms = first_block_global_slot * milliseconds_per_slot; + + // Convert to nanos + genesis_timestamp_ms + .checked_add(first_block_global_slot_delta_ms) + .unwrap() + .checked_mul(1_000_000) + .unwrap() +} + impl SoloNodeBootstrap { pub async fn run(self, mut runner: ClusterRunner<'_>) { use self::TransitionFrontierSyncState::*; @@ -27,10 +46,12 @@ impl SoloNodeBootstrap { let replayer = hosts::replayer(); - let node_id = runner.add_rust_node( - RustNodeTestingConfig::devnet_default() - .initial_peers(vec![ListenerNode::Custom(replayer)]), - ); + let mut config = RustNodeTestingConfig::devnet_default(); + + config.initial_time = redux::Timestamp::new(first_block_slot_timestamp_nanos(&config)); + + let node_id = + runner.add_rust_node(config.initial_peers(vec![ListenerNode::Custom(replayer)])); eprintln!("launch Openmina node with default configuration, id: {node_id}"); let mut timeout = TIMEOUT;