diff --git a/ethexe/cli/src/commands/check.rs b/ethexe/cli/src/commands/check.rs index 58fc4e73857..85b5b7010e4 100644 --- a/ethexe/cli/src/commands/check.rs +++ b/ethexe/cli/src/commands/check.rs @@ -19,7 +19,6 @@ use ethexe_db::{ }; use ethexe_processor::{Processor, ProcessorConfig}; use ethexe_runtime_common::FinalizedBlockTransitions; -use gprimitives::H256; use indicatif::{ProgressBar, ProgressStyle}; use std::{collections::HashSet, path::PathBuf}; @@ -337,7 +336,7 @@ impl Checker { pb.inc(1); }; - if current_compact_mb.parent == H256::zero() { + if current_compact_mb.parent.is_zero() { break; } current_mb = current_compact_mb.parent; diff --git a/ethexe/cli/src/commands/dump.rs b/ethexe/cli/src/commands/dump.rs index eb68bf06c5d..506294ee731 100644 --- a/ethexe/cli/src/commands/dump.rs +++ b/ethexe/cli/src/commands/dump.rs @@ -4,7 +4,7 @@ use crate::params::{MergeParams, Params}; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; -use ethexe_common::db::GlobalsStorageRO; +use ethexe_common::{EB, HashOf, db::GlobalsStorageRO}; use ethexe_db::{Database, RawDatabase, RocksDatabase, dump::StateDump}; use gprimitives::H256; use std::path::{Path, PathBuf}; @@ -75,13 +75,18 @@ impl DumpCommand { let raw_db = RawDatabase::from_one(&rocks_db); let db = Database::try_from_raw(raw_db)?; - let block_hash = block_hash.unwrap_or_else(|| { - let latest_prepared_block = db.globals().latest_prepared_eb_hash; - log::info!( - "No block hash provided, using latest committed block: {latest_prepared_block:?}" - ); - latest_prepared_block - }); + let block_hash = block_hash + .map(|raw| { + // SAFETY: CLI-supplied EB hash; treat verbatim. + unsafe { HashOf::::new(raw) } + }) + .unwrap_or_else(|| { + let latest_prepared_block = db.globals().latest_prepared_eb_hash; + log::info!( + "No block hash provided, using latest committed block: {latest_prepared_block:?}" + ); + latest_prepared_block + }); log::info!("Collecting state dump for block {block_hash:?}..."); let dump = StateDump::collect_from_storage(&db, block_hash)?; diff --git a/ethexe/cli/src/commands/tx.rs b/ethexe/cli/src/commands/tx.rs index 8350e454170..14611214271 100644 --- a/ethexe/cli/src/commands/tx.rs +++ b/ethexe/cli/src/commands/tx.rs @@ -1158,7 +1158,7 @@ impl TxCommand { Ok(SendMessageResult::Injected { tx_hash, reference_block_number, - reference_block_hash, + reference_block_hash: reference_block_hash.inner(), payload: SendMessagePayload { message_id, actor_id, diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index 390af381b92..7c127ce33f8 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -4,13 +4,17 @@ //! Common db types and traits. use crate::{ - Address, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, ProtocolTimelines, Schedule, - SimpleBlockData, ValidatorsVec, + Address, BlockHeader, CodeBlobInfo, Digest, EB, HashOf, ProgramStates, ProtocolTimelines, + Schedule, ValidatorsVec, events::BlockEvent, gear::StateTransition, injected::{InjectedTransaction, Promise, SignedInjectedTransaction, SignedTxReceipt}, - malachite::Operations, + malachite::{MB, Operations}, }; + +// Re-export so existing `ethexe_common::db::CompactMb` imports keep working +// — the type itself now lives in `ethexe_common::malachite` alongside `MB`. +pub use crate::malachite::CompactMb; use alloc::{ collections::{BTreeSet, VecDeque}, vec::Vec, @@ -34,9 +38,9 @@ pub struct BlockMeta { /// Last committed on-chain batch hash (digest). pub last_committed_batch: Option, /// Last committed MB hash. - pub last_committed_mb: Option, + pub last_committed_mb: Option>, /// Last committed EB hash. - pub last_committed_eb: Option, + pub last_committed_eb: Option>, /// Latest era with committed validators. pub latest_era_validators_committed: Option, } @@ -49,14 +53,14 @@ pub trait HashStorageRO { #[auto_impl::auto_impl(&, Box)] pub trait BlockMetaStorageRO { /// NOTE: if `BlockMeta` doesn't exist in the database, it will return the default value. - fn block_meta(&self, block_hash: H256) -> BlockMeta; + fn block_meta(&self, block_hash: HashOf) -> BlockMeta; } #[auto_impl::auto_impl(&)] pub trait BlockMetaStorageRW: BlockMetaStorageRO { /// NOTE: if `BlockMeta` doesn't exist in the database, /// it will be created with default values and then will be mutated. - fn mutate_block_meta(&self, block_hash: H256, f: impl FnOnce(&mut BlockMeta)); + fn mutate_block_meta(&self, block_hash: HashOf, f: impl FnOnce(&mut BlockMeta)); } #[auto_impl::auto_impl(&, Box)] @@ -84,14 +88,14 @@ pub trait CodesStorageRW: CodesStorageRO { #[auto_impl::auto_impl(&, Box)] pub trait OnChainStorageRO { - fn block_header(&self, block_hash: H256) -> Option; - fn block_events(&self, block_hash: H256) -> Option>; + fn block_header(&self, block_hash: HashOf) -> Option; + fn block_events(&self, block_hash: HashOf) -> Option>; fn code_blob_info(&self, code_id: CodeId) -> Option; - fn block_synced(&self, block_hash: H256) -> bool; + fn block_synced(&self, block_hash: HashOf) -> bool; fn validators(&self, era_index: u64) -> Option; - fn block_simple_data(&self, block_hash: H256) -> Option { - self.block_header(block_hash).map(|header| SimpleBlockData { + fn block_simple_data(&self, block_hash: HashOf) -> Option { + self.block_header(block_hash).map(|header| EB { hash: block_hash, header, }) @@ -100,11 +104,11 @@ pub trait OnChainStorageRO { #[auto_impl::auto_impl(&)] pub trait OnChainStorageRW: OnChainStorageRO { - fn set_block_header(&self, block_hash: H256, header: BlockHeader); - fn set_block_events(&self, block_hash: H256, events: &[BlockEvent]); + fn set_block_header(&self, block_hash: HashOf, header: BlockHeader); + fn set_block_events(&self, block_hash: HashOf, events: &[BlockEvent]); fn set_code_blob_info(&self, code_id: CodeId, code_info: CodeBlobInfo); fn set_validators(&self, era_index: u64, validator_set: ValidatorsVec); - fn set_block_synced(&self, block_hash: H256); + fn set_block_synced(&self, block_hash: HashOf); } #[auto_impl::auto_impl(&)] @@ -131,25 +135,12 @@ pub trait InjectedStorageRW: InjectedStorageRO { fn set_receipt(&self, receipt: &SignedTxReceipt); } -/// MB static identity. Keyed by the Blake2b envelope hash; existence implies -/// the matching `Operations` blob is in CAS at `operations_hash`. -#[derive( - Debug, Clone, Copy, Default, Encode, Decode, TypeInfo, PartialEq, Eq, Hash, derive_more::Display, -)] -#[display("MB(height {height}, parent {parent}, operations_hash {operations_hash})")] -pub struct CompactMb { - pub parent: H256, - pub height: u64, - pub operations_hash: H256, -} - /// MB dynamic state. `last_advanced_eb` is propagated forward at save time -/// (resets on `AdvanceTillEthereumBlock`); `synced` requires this MB and every -/// ancestor to be persisted. +/// (resets on `AdvanceTillEthereumBlock`). #[derive(Debug, Clone, Default, Encode, Decode, TypeInfo, PartialEq, Eq, Hash)] pub struct MbMeta { pub computed: bool, - pub last_advanced_eb: H256, + pub last_advanced_eb: HashOf, } #[auto_impl::auto_impl(&, Box)] @@ -157,25 +148,25 @@ pub trait MbStorageRO { /// Static identity (parent + height + `operations_hash`). /// Existence implies the matching [`Operations`] blob is in the /// CAS at `operations_hash`. - fn mb_compact_block(&self, mb_hash: H256) -> Option; + fn mb_compact_block(&self, mb_hash: HashOf) -> Option; /// Read the [`Operations`] blob from CAS by its content hash. fn operations(&self, operations_hash: H256) -> Option; - fn mb_program_states(&self, mb_hash: H256) -> Option; - fn mb_outcome(&self, mb_hash: H256) -> Option>; - fn mb_schedule(&self, mb_hash: H256) -> Option; - fn mb_meta(&self, mb_hash: H256) -> MbMeta; + fn mb_program_states(&self, mb_hash: HashOf) -> Option; + fn mb_outcome(&self, mb_hash: HashOf) -> Option>; + fn mb_schedule(&self, mb_hash: HashOf) -> Option; + fn mb_meta(&self, mb_hash: HashOf) -> MbMeta; } #[auto_impl::auto_impl(&)] pub trait MbStorageRW: MbStorageRO { - fn set_mb_compact_block(&self, mb_hash: H256, compact: CompactMb); + fn set_mb_compact_block(&self, mb_hash: HashOf, compact: CompactMb); /// Write an [`Operations`] blob into the CAS and return its hash /// (the value stored in [`CompactMb::operations_hash`]). fn set_operations(&self, operations: Operations) -> H256; - fn set_mb_program_states(&self, mb_hash: H256, program_states: ProgramStates); - fn set_mb_outcome(&self, mb_hash: H256, outcome: Vec); - fn set_mb_schedule(&self, mb_hash: H256, schedule: Schedule); - fn mutate_mb_meta(&self, mb_hash: H256, f: impl FnOnce(&mut MbMeta)); + fn set_mb_program_states(&self, mb_hash: HashOf, program_states: ProgramStates); + fn set_mb_outcome(&self, mb_hash: HashOf, outcome: Vec); + fn set_mb_schedule(&self, mb_hash: HashOf, schedule: Schedule); + fn mutate_mb_meta(&self, mb_hash: HashOf, f: impl FnOnce(&mut MbMeta)); } pub struct PreparedBlockData { @@ -184,8 +175,8 @@ pub struct PreparedBlockData { pub latest_era_with_committed_validators: u64, pub codes_queue: VecDeque, pub last_committed_batch: Digest, - pub last_committed_mb: H256, - pub last_committed_eb: H256, + pub last_committed_mb: HashOf, + pub last_committed_eb: HashOf, } #[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] @@ -194,24 +185,24 @@ pub struct DBConfig { pub chain_id: u64, pub router_address: Address, pub timelines: ProtocolTimelines, - pub genesis_block_hash: H256, + pub genesis_block_hash: HashOf, pub max_validators: u16, } #[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] pub struct DBGlobals { - pub start_block_hash: H256, - pub latest_synced_eb: SimpleBlockData, - pub latest_prepared_eb_hash: H256, + pub start_block_hash: HashOf, + pub latest_synced_eb: EB, + pub latest_prepared_eb_hash: HashOf, /// Latest MB BFT-finalized by Malachite. Rows /// (`mb_program_states`/`mb_outcome`/`mb_schedule`) may not yet /// be persisted — use [`Self::latest_computed_mb_hash`] for any /// read that depends on those rows existing. - pub latest_finalized_mb_hash: H256, + pub latest_finalized_mb_hash: HashOf, /// Latest MB whose per-row state has been written by the compute /// pipeline. Trails `latest_finalized_mb_hash` until compute /// catches up. - pub latest_computed_mb_hash: H256, + pub latest_computed_mb_hash: HashOf, } #[cfg(feature = "std")] @@ -259,15 +250,16 @@ pub use mock_interfaces::{SetConfig, SetGlobals}; #[cfg(test)] mod tests { use super::*; - use crate::malachite::Operations; + use crate::malachite::{BlockPayload, MB, Operations}; use indoc::formatdoc; use scale_info::{PortableRegistry, Registry, meta_type}; use sha3::{Digest, Sha3_256}; #[test] fn ensure_types_unchanged() { + // Recomputed after the typed-hash + MB move refactor (typed MB/EB hashes). const EXPECTED_TYPE_INFO_HASH: &str = - "600c7b8ccc11ab8c87a94170473bad7cf7c1c87973f5f56f3734ff4ad7473a2a"; + "40c73820b88e8c60899d573ec47cb02b332898a418205206c2b6597517ba3ea5"; let types = [ meta_type::(), @@ -284,6 +276,8 @@ mod tests { meta_type::(), meta_type::(), meta_type::(), + meta_type::(), + meta_type::(), meta_type::(), // NOTE: `Operation` hand-rolls its `Encode`/`Decode` (fixed-width // u32 tag), so this TypeInfo hash does NOT cover its wire format — diff --git a/ethexe/common/src/gear.rs b/ethexe/common/src/gear.rs index 3f987ebbcb1..e82bd587a62 100644 --- a/ethexe/common/src/gear.rs +++ b/ethexe/common/src/gear.rs @@ -3,7 +3,7 @@ //! This is supposed to be an exact copy of Gear.sol library. -use crate::{Address, Digest, ToDigest, ValidatorsVec}; +use crate::{Address, Digest, EB, HashOf, ToDigest, ValidatorsVec}; use alloc::vec::Vec; use alloy_primitives::U256 as AlloyU256; use gear_core::message::{ReplyCode, ReplyDetails, StoredMessage, SuccessReplyReason}; @@ -53,7 +53,7 @@ pub struct AddressBook { pub struct ChainCommitment { pub transitions: Vec, pub head: H256, - pub last_advanced_eth_block: H256, + pub last_advanced_eth_block: HashOf, } impl ToDigest for ChainCommitment { @@ -66,7 +66,7 @@ impl ToDigest for ChainCommitment { hasher.update(transitions.to_digest()); hasher.update(head.0); - hasher.update(last_advanced_eth_block.0); + hasher.update(last_advanced_eth_block.inner().0); } } @@ -162,7 +162,7 @@ pub struct BatchCommitment { // Hash of ethereum block for which this batch has been created // This is used to identify whether router have to apply this batch, // it can be a batch from another branch and after reorg it's not actual anymore (currently we have predecessorBlock for this) - pub block_hash: H256, + pub block_hash: HashOf, /// Timestamp of ethereum block for which this batch has been created /// This timestamp is used to identify validator set to verify commitment (current or previous era) @@ -198,7 +198,7 @@ impl ToDigest for BatchCommitment { rewards_commitment, } = self; - hasher.update(block_hash); + hasher.update(block_hash.inner()); hasher.update(crate::u64_into_uint48_be_bytes_lossy(*timestamp)); hasher.update(previous_batch); hasher.update(expiry.to_be_bytes()); diff --git a/ethexe/common/src/hash.rs b/ethexe/common/src/hash.rs index 7ef4d7db52c..c7353ce1bab 100644 --- a/ethexe/common/src/hash.rs +++ b/ethexe/common/src/hash.rs @@ -11,7 +11,7 @@ use core::{ marker::PhantomData, }; use gprimitives::H256; -use parity_scale_codec::{Decode, Encode}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; fn option_string(value: &Option) -> String { @@ -28,7 +28,7 @@ fn shortname() -> &'static str { .expect("name is empty") } -#[derive(Encode, Decode, TypeInfo, derive_more::Into, derive_more::Display)] +#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, derive_more::Into, derive_more::Display)] #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", serde(transparent))] #[display("{hash}")] @@ -102,13 +102,20 @@ impl HashOf { self.hash } - pub fn zero() -> Self { + pub const fn zero() -> Self { Self { hash: H256::zero(), _phantom: PhantomData, } } + /// True iff this is the zero-byte sentinel hash. Useful where a + /// caller treats `H256::zero()` as "no value yet" (genesis pre-MB, + /// uninitialised parent links, etc.). + pub fn is_zero(&self) -> bool { + self.hash.is_zero() + } + #[cfg(feature = "mock")] pub fn random() -> Self { Self { diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index c571ced9707..d4a29825975 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -1,7 +1,7 @@ // Copyright (C) Gear Technologies Inc. // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 -use crate::{Address, HashOf, ToDigest, ecdsa::SignedMessage}; +use crate::{Address, EB, HashOf, ToDigest, ecdsa::SignedMessage}; use alloc::string::{String, ToString}; use core::hash::Hash; use gear_core::{limited::LimitedVec, rpc::ReplyInfo}; @@ -64,7 +64,7 @@ pub struct InjectedTransaction { /// NOTE: at this moment will be zero. pub value: u128, /// Reference block number. - pub reference_block: H256, + pub reference_block: HashOf, /// Arbitrary bytes to allow multiple synonymous /// transactions to be sent simultaneously. /// NOTE: this is also a salt for MessageId generation. @@ -103,7 +103,7 @@ impl InjectedTransaction { append(destination.as_ref()); append(gear_core::utils::hash(payload).as_ref()); append(value.to_be_bytes().as_ref()); - append(reference_block.0.as_ref()); + append(reference_block.inner().0.as_ref()); append(gear_core::utils::hash(salt).as_ref()); hashable_bytes @@ -456,7 +456,8 @@ mod tests { destination: ActorId::zero(), payload: vec![1u8, 2u8, 3u8, 4u8].try_into().unwrap(), value: 100, - reference_block: H256::random(), + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + reference_block: unsafe { HashOf::::new(H256::random()) }, salt: vec![1u8, 2u8].try_into().unwrap(), }; @@ -473,13 +474,14 @@ mod tests { value_be[0] = payload_last_byte; shifted_tx.value = u128::from_be_bytes(value_be); - let mut ref_block_data = shifted_tx.reference_block.0; + let mut ref_block_data = shifted_tx.reference_block.inner().0; let last_ref_block = ref_block_data[31]; ref_block_data.copy_within(0..31, 1); ref_block_data[0] = value_last_byte; - shifted_tx.reference_block = H256(ref_block_data); + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + shifted_tx.reference_block = unsafe { HashOf::::new(H256(ref_block_data)) }; let mut salt = shifted_tx.salt.clone().into_vec(); salt.insert(0, last_ref_block); @@ -493,7 +495,7 @@ mod tests { tx.destination.as_ref(), tx.payload.as_ref(), tx.value.to_be_bytes().as_ref(), - tx.reference_block.0.as_ref(), + tx.reference_block.inner().0.as_ref(), tx.salt.as_ref(), ] .concat() diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index d62cc3866a7..a9512b610b5 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -1,13 +1,21 @@ // Copyright (C) Gear Technologies Inc. // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 -//! Application-level block shape produced by the Malachite sequencer -//! and consumed by the ethexe executor. +//! Malachite block model shared by the consensus service +//! (`ethexe-malachite-core`), the consensus glue layer +//! (`ethexe-malachite`), and the executor (`ethexe-compute`). //! -//! [`Operations`] is the application schema: an ordered list of -//! [`Operation`]s. The consensus engine ships it SCALE-encoded as an -//! opaque, size-capped byte string (the malachite `Block` payload); -//! the encoding/decoding lives behind the consensus boundary. +//! - [`MB`] is the consensus block envelope: `(parent_hash, height, +//! payload, reserved)`. Its hash is [`HashOf`]. +//! - [`BlockPayload`] is the opaque, versioned, size-capped wire +//! payload carried by an MB. The application schema +//! ([`Operations`]) lives inside [`BlockPayload`] as SCALE-encoded +//! bytes. +//! - [`CompactMb`] is `MB` with the payload bytes replaced by +//! `operations_hash` — what gets indexed in the ethexe DB once the +//! matching [`Operations`] blob is in CAS. +//! - [`Operations`] is the application-level ordered list of +//! [`Operation`]s that the executor consumes. //! //! Protocol evolution is additive: a new behaviour gets a new //! [`Operation`] variant with the next free `#[repr(u32)]` discriminant @@ -17,19 +25,15 @@ //! validator side — older operations can be retired from new blocks //! without ever losing the ability to decode and replay them. //! -//! Block-level identity (parent linkage, height) lives in -//! [`crate::db::CompactMb`], indexed by the consensus block envelope -//! hash. The matching [`Operations`] blob is stored in the -//! content-addressed half of the ethexe db and referenced by -//! `CompactMb::operations_hash`. -//! //! These types live in `ethexe-common` (rather than inside //! `ethexe-malachite`) so `ethexe-processor` can accept them without //! depending on the consensus layer. -use crate::injected::SignedInjectedTransaction; +use crate::{EB, HashOf, injected::SignedInjectedTransaction}; use alloc::vec::Vec; +use anyhow::{Result, anyhow}; use derive_more::{Deref, DerefMut, IntoIterator}; +use gear_core::limited::LimitedVec; use gprimitives::H256; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; @@ -37,13 +41,150 @@ use scale_info::TypeInfo; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; +/// Per-block payload size cap. +/// +/// The whole [`MB`] ships as a single gossipsub message: the proposer +/// streams it as one `Data` proposal part, and the value-sync path fetches +/// a finalized block in one request-response round. Malachite's +/// `pubsub_max_size` (the gossipsub `max_transmit_size`) defaults to +/// 4 MiB, so the encoded MB must stay well under that. The 1 MiB cap +/// leaves ~4x headroom under the transport ceiling. +pub const MAX_BLOCK_PAYLOAD_BYTES: usize = 1024 * 1024; + +/// Current `BlockPayload::version` written by this code path. +/// +/// Bump in lockstep with a wire-format change in how the application +/// interprets [`BlockPayload::bytes`]; decoders MUST tolerate seeing +/// versions strictly less than the current one but MAY reject newer +/// ones. +pub const BLOCK_PAYLOAD_VERSION: u16 = 0; + +/// Versioned, size-capped block payload carried by an [`MB`]. +/// +/// The consensus service treats `bytes` as opaque. The ethexe +/// application schema lives inside as a SCALE-encoded [`Operations`] +/// — `version` exists so a future protocol bump can change that +/// encoding without breaking the [`MB`] wire shape. +#[derive(Clone, Debug, Default, PartialEq, Eq, Encode, Decode, TypeInfo)] +pub struct BlockPayload { + pub version: u16, + pub bytes: LimitedVec, +} + +impl BlockPayload { + /// Wrap raw application bytes at the current + /// [`BLOCK_PAYLOAD_VERSION`]. Returns `Err` if `bytes` exceeds + /// [`MAX_BLOCK_PAYLOAD_BYTES`]. + pub fn new(bytes: Vec) -> Result { + let len = bytes.len(); + let bytes = LimitedVec::try_from(bytes).map_err(|_| { + anyhow!("block payload exceeds {MAX_BLOCK_PAYLOAD_BYTES}-byte cap (got {len})") + })?; + Ok(Self { + version: BLOCK_PAYLOAD_VERSION, + bytes, + }) + } + + /// Content-addressed hash of the application bytes (the value + /// stored in [`CompactMb::operations_hash`]). The `version` prefix + /// deliberately does NOT contribute to the digest: at v0 the + /// bytes are SCALE-encoded [`Operations`], so this hash matches + /// the legacy `Operations`-keyed CAS slot byte-for-byte. + pub fn hash(&self) -> H256 { + gear_core::utils::hash(self.bytes.as_ref()).into() + } +} + +/// Malachite block envelope: opaque versioned payload plus +/// chain-position fields (parent hash + height) and a [`Self::reserved`] +/// tail for future protocol extensions. +/// +/// The block hash ([`Self::hash`]) is [`gear_core::utils::hash`] +/// (Blake2b-256) over a SCALE-encoded +/// `(parent_hash, height, payload_hash, reserved)` tuple, where +/// `payload_hash = BlockPayload::hash()`. Two nodes with the same +/// envelope content produce the same hash. +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo)] +pub struct MB { + pub parent_hash: HashOf, + pub height: u64, + pub payload: BlockPayload, + pub reserved: [u8; 64], +} + +impl MB { + /// Construct an MB with `reserved` zeroed out. + pub fn new(parent_hash: HashOf, height: u64, payload: BlockPayload) -> Self { + Self { + parent_hash, + height, + payload, + reserved: [0u8; 64], + } + } + + /// Compute the canonical [`HashOf`] for this envelope. + pub fn hash(&self) -> HashOf { + let payload_hash = self.payload.hash(); + let inner = (self.parent_hash, self.height, payload_hash, self.reserved).encode(); + let raw: H256 = gear_core::utils::hash(&inner).into(); + // SAFETY: `raw` is the canonical MB envelope digest. Wrapping + // it in `HashOf` is exactly what the constructor exists for. + unsafe { HashOf::new(raw) } + } +} + +/// MB static identity. Same shape as [`MB`] but with the opaque +/// payload bytes replaced by `operations_hash`. Existence implies the +/// matching application-level [`Operations`] blob is in the +/// content-addressed half of the ethexe DB at `operations_hash`. +#[derive( + Debug, Clone, Copy, Encode, Decode, TypeInfo, PartialEq, Eq, Hash, derive_more::Display, +)] +#[display("MB(height {height}, parent {parent}, operations_hash {operations_hash})")] +pub struct CompactMb { + pub parent: HashOf, + pub height: u64, + pub operations_hash: H256, + pub reserved: [u8; 64], +} + +impl Default for CompactMb { + fn default() -> Self { + Self { + parent: HashOf::zero(), + height: 0, + operations_hash: H256::zero(), + reserved: [0u8; 64], + } + } +} + +impl CompactMb { + /// Recompute the [`HashOf`] from this compact record. Matches + /// [`MB::hash`] byte-for-byte by construction (same SCALE tuple). + pub fn mb_hash(&self) -> HashOf { + let inner = ( + self.parent, + self.height, + self.operations_hash, + self.reserved, + ) + .encode(); + let raw: H256 = gear_core::utils::hash(&inner).into(); + // SAFETY: identical derivation to `MB::hash`. + unsafe { HashOf::new(raw) } + } +} + /// A single operation in the malachite block. #[derive(Clone, Debug, PartialEq, Eq, TypeInfo, derive_more::IsVariant)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[repr(u32)] pub enum Operation { /// Pin executor's view to a quarantine-passed Ethereum block. - AdvanceTillEthereumBlock { block_hash: H256 } = 0, + AdvanceTillEthereumBlock { block_hash: HashOf } = 0, /// Progress scheduled tasks (mailbox/waitlist/reservation cleanup). ProgressTasks = 1, @@ -86,7 +227,7 @@ impl Decode for Operation { let tag = u32::decode(input)?; match tag { 0 => Ok(Operation::AdvanceTillEthereumBlock { - block_hash: H256::decode(input)?, + block_hash: HashOf::::decode(input)?, }), 1 => Ok(Operation::ProgressTasks), 2 => Ok(Operation::ProcessQueues { @@ -155,7 +296,8 @@ mod tests { let mut a = empty_txs(); let b = empty_txs(); a.push(Operation::AdvanceTillEthereumBlock { - block_hash: H256::from_low_u64_be(0xEB), + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + block_hash: unsafe { HashOf::::new(H256::from_low_u64_be(0xEB)) }, }); assert_ne!(a.hash(), b.hash()); } @@ -163,7 +305,7 @@ mod tests { #[test] fn operation_tag_distinguishes_variants() { let advance = Operation::AdvanceTillEthereumBlock { - block_hash: H256::zero(), + block_hash: HashOf::::zero(), }; let progress = Operation::ProgressTasks; let queues = Operation::ProcessQueues { @@ -183,7 +325,7 @@ mod tests { // consensus wire format and must stay frozen forever. assert_eq!( Operation::AdvanceTillEthereumBlock { - block_hash: H256::zero() + block_hash: HashOf::::zero() } .tag(), 0 @@ -193,7 +335,7 @@ mod tests { assert_eq!( &Operation::AdvanceTillEthereumBlock { - block_hash: H256::zero() + block_hash: HashOf::::zero() } .encode()[..4], &[0, 0, 0, 0], @@ -218,11 +360,46 @@ mod tests { use parity_scale_codec::Decode; let original = Operations::new(alloc::vec![Operation::AdvanceTillEthereumBlock { - block_hash: H256::from_low_u64_be(0xEB) + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + block_hash: unsafe { HashOf::::new(H256::from_low_u64_be(0xEB)) } }]); let encoded = original.encode(); let decoded = Operations::decode(&mut encoded.as_slice()).expect("decode"); assert_eq!(original, decoded); assert_eq!(original.hash(), decoded.hash()); } + + #[test] + fn block_payload_new_accepts_at_or_below_cap() { + BlockPayload::new(alloc::vec![]).expect("empty payload"); + BlockPayload::new(alloc::vec![0u8; MAX_BLOCK_PAYLOAD_BYTES]).expect("payload at cap"); + } + + #[test] + fn block_payload_new_rejects_above_cap() { + let err = BlockPayload::new(alloc::vec![0u8; MAX_BLOCK_PAYLOAD_BYTES + 1]) + .expect_err("over-cap must reject"); + assert!( + err.to_string() + .contains(&MAX_BLOCK_PAYLOAD_BYTES.to_string()), + "expected cap-sized error mention, got: {err}", + ); + } + + #[test] + fn block_payload_decode_rejects_oversized_bytes_field() { + // Hand-roll an encoded `BlockPayload` whose `bytes` length + // exceeds the cap. SCALE prefixes `Vec` with a `Compact` + // length; we use the 4-byte mode for clarity. Decode must reject + // before allocating the over-cap buffer. + use parity_scale_codec::DecodeAll; + + let oversize = (MAX_BLOCK_PAYLOAD_BYTES + 1) as u32; + let mut encoded = alloc::vec::Vec::new(); + encoded.extend_from_slice(&BLOCK_PAYLOAD_VERSION.encode()); + encoded.extend_from_slice(&parity_scale_codec::Compact(oversize).encode()); + encoded.extend(core::iter::repeat_n(0u8, oversize as usize)); + BlockPayload::decode_all(&mut encoded.as_slice()) + .expect_err("decode must reject over-cap payload"); + } } diff --git a/ethexe/common/src/mock.rs b/ethexe/common/src/mock.rs index 183ed86010c..f447c931016 100644 --- a/ethexe/common/src/mock.rs +++ b/ethexe/common/src/mock.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 use crate::{ - Address, BlockData, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, + Address, BlockData, BlockHeader, CodeBlobInfo, Digest, EB, HashOf, ProgramStates, ProtocolTimelines, Rfm, Schedule, ScheduledTask, Sd, SimpleBlockData, StateHashWithQueueSize, Sum, ValidatorsVec, consensus::BatchCommitmentValidationRequest, @@ -12,7 +12,7 @@ use crate::{ BatchCommitment, ChainCommitment, CodeCommitment, Message, MessageType, StateTransition, }, injected::{InjectedTransaction, Promise}, - malachite::Operations, + malachite::{MB, Operations}, }; use alloc::{collections::BTreeMap, vec}; use gear_core::{ @@ -61,7 +61,7 @@ where #[derive(Debug, Clone, Copy, Default)] pub struct BlockHeaderParams { - parent_hash: Option, + parent_hash: Option>, } impl From<()> for BlockHeaderParams { @@ -72,6 +72,15 @@ impl From<()> for BlockHeaderParams { impl From for BlockHeaderParams { fn from(parent_hash: H256) -> Self { + Self { + // SAFETY: synthetic chain hash for mocks — same invariant as a real EB hash. + parent_hash: Some(unsafe { HashOf::::new(parent_hash) }), + } + } +} + +impl From> for BlockHeaderParams { + fn from(parent_hash: HashOf) -> Self { Self { parent_hash: Some(parent_hash), } @@ -206,7 +215,11 @@ impl Arbitrary for SimpleBlockData { fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { (h256_strategy(), BlockHeader::arbitrary_with(args)) - .prop_map(|(hash, header)| Self { hash, header }) + .prop_map(|(hash, header)| Self { + // SAFETY: HashOf wraps the raw chain hash verbatim. + hash: unsafe { HashOf::::new(hash) }, + header, + }) .boxed() } } @@ -218,7 +231,10 @@ impl Arbitrary for BlockHeader { fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { let parent_hash = match args.parent_hash { Some(parent_hash) => Just(parent_hash).boxed(), - None => h256_strategy(), + // SAFETY: synthetic chain hash for mocks — same invariant as a real EB hash. + None => h256_strategy() + .prop_map(|h| unsafe { HashOf::::new(h) }) + .boxed(), }; parent_hash @@ -299,7 +315,7 @@ impl Arbitrary for ChainCommitment { .prop_map(|(first, second, head)| Self { transitions: vec![first, second], head, - last_advanced_eth_block: H256::zero(), + last_advanced_eth_block: HashOf::::zero(), }) .boxed() } @@ -325,7 +341,8 @@ impl Arbitrary for BatchCommitment { code_commitment_1, code_commitment_2, )| Self { - block_hash, + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + block_hash: unsafe { HashOf::::new(block_hash) }, timestamp: 42, previous_batch, expiry: 10, @@ -442,12 +459,12 @@ pub struct SyncedBlockData { pub struct PreparedBlockData { pub codes_queue: VecDeque, pub last_committed_batch: Digest, - pub last_committed_mb: H256, + pub last_committed_mb: HashOf, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct BlockFullData { - pub hash: H256, + pub hash: HashOf, pub synced: Option, pub prepared: Option, } @@ -520,9 +537,9 @@ pub struct MockComputedMbData { /// written to the DB. #[derive(Debug, Clone, PartialEq, Eq)] pub struct MbFullData { - pub hash: H256, - /// Parent MB hash. `H256::zero()` for the very first real MB. - pub parent: H256, + pub hash: HashOf, + /// Parent MB hash. Zero for the very first real MB. + pub parent: HashOf, /// MB height. Set to the block index `i` so it monotonically /// matches [`BlockChain::blocks`]. pub height: u64, @@ -577,7 +594,7 @@ impl BlockChain { /// Convenience for the common `mbs[idx].hash` pattern. #[track_caller] - pub fn mb_hash_at(&self, idx: usize) -> H256 { + pub fn mb_hash_at(&self, idx: usize) -> HashOf { self.mbs[idx].hash } } @@ -593,20 +610,22 @@ where DB: MbStorageRW, { let operations_hash = db.set_operations(Operations::new(vec![])); + let zero_mb = HashOf::::zero(); db.set_mb_compact_block( - H256::zero(), + zero_mb, CompactMb { - parent: H256::zero(), + parent: HashOf::::zero(), height: 0, operations_hash, + reserved: [0u8; 64], }, ); - db.set_mb_program_states(H256::zero(), Default::default()); - db.set_mb_schedule(H256::zero(), Default::default()); - db.set_mb_outcome(H256::zero(), Vec::new()); - db.mutate_mb_meta(H256::zero(), |m| { + db.set_mb_program_states(zero_mb, Default::default()); + db.set_mb_schedule(zero_mb, Default::default()); + db.set_mb_outcome(zero_mb, Vec::new()); + db.mutate_mb_meta(zero_mb, |m| { m.computed = true; - m.last_advanced_eb = H256::zero(); + m.last_advanced_eb = HashOf::::zero(); }); } @@ -641,7 +660,7 @@ impl BlockChain { // sentinel (zero hash). Empty-operations MBs share one CAS // entry naturally — `set_operations` is content-addressed. for mb in &mbs { - if mb.hash == H256::zero() { + if mb.hash.is_zero() { continue; } let operations_hash = db.set_operations(mb.operations.clone()); @@ -651,6 +670,7 @@ impl BlockChain { parent: mb.parent, height: mb.height, operations_hash, + reserved: [0u8; 64], }, ); if let Some(computed) = &mb.computed { @@ -662,19 +682,19 @@ impl BlockChain { } for BlockFullData { - hash, + hash: block_hash, synced, prepared, } in blocks { if let Some(SyncedBlockData { header, events }) = synced { - db.set_block_header(hash, header); - db.set_block_events(hash, &events); - db.set_block_synced(hash); + db.set_block_header(block_hash, header); + db.set_block_events(block_hash, &events); + db.set_block_synced(block_hash); let block_era = config.timelines.era_from_ts(header.timestamp).unwrap(); db.set_validators(block_era, validators.clone()); - db.mutate_block_meta(hash, |meta| { + db.mutate_block_meta(block_hash, |meta| { meta.latest_era_validators_committed = Some(block_era) }); } @@ -685,7 +705,7 @@ impl BlockChain { last_committed_mb, }) = prepared { - db.mutate_block_meta(hash, |meta| { + db.mutate_block_meta(block_hash, |meta| { *meta = BlockMeta { prepared: true, codes_queue: Some(codes_queue), @@ -746,19 +766,21 @@ impl BlockChain { .map( |((parent_hash, _, _), (block_hash, block_height, block_timestamp))| { BlockFullData { - hash: block_hash, + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + hash: unsafe { HashOf::::new(block_hash) }, synced: Some(SyncedBlockData { header: BlockHeader { height: block_height, timestamp: block_timestamp as u64, - parent_hash, + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + parent_hash: unsafe { HashOf::::new(parent_hash) }, }, events: Default::default(), }), prepared: Some(PreparedBlockData { codes_queue: Default::default(), last_committed_batch: Digest::zero(), - last_committed_mb: H256::zero(), + last_committed_mb: HashOf::zero(), }), } }, @@ -783,12 +805,12 @@ impl BlockChain { // the `blocks[0]` genesis-parent placeholder; subsequent MBs // link parent-to-parent in chronological order. let mut mbs: VecDeque = VecDeque::with_capacity(blocks.len()); - let mut prev_mb_hash = H256::zero(); + let mut prev_mb_hash = HashOf::::zero(); for i in 0..blocks.len() { if i == 0 { mbs.push_back(MbFullData { - hash: H256::zero(), - parent: H256::zero(), + hash: HashOf::::zero(), + parent: HashOf::::zero(), height: 0, computed: None, operations: Operations::new(vec![]), @@ -799,7 +821,8 @@ impl BlockChain { let mut hb = [0u8; 32]; hb[0] = 0xCD; hb[1..9].copy_from_slice(&(i as u64).to_be_bytes()); - let hash = H256::from(hb); + // SAFETY: synthetic MB hash for tests — same invariant as a real MB envelope hash. + let hash = unsafe { HashOf::::new(H256::from(hb)) }; mbs.push_back(MbFullData { hash, parent: prev_mb_hash, @@ -819,8 +842,8 @@ impl BlockChain { start_block_hash: blocks[0].hash, latest_synced_eb: blocks.back().unwrap().to_simple(), latest_prepared_eb_hash: blocks.back().unwrap().hash, - latest_finalized_mb_hash: H256::zero(), - latest_computed_mb_hash: H256::zero(), + latest_finalized_mb_hash: HashOf::zero(), + latest_computed_mb_hash: HashOf::zero(), }; Self { @@ -857,8 +880,11 @@ impl SimpleBlockData { } pub fn next_block(self) -> Self { + let raw_hash = H256::from_low_u64_be(self.hash.inner().to_low_u64_be() + 1); Self { - hash: H256::from_low_u64_be(self.hash.to_low_u64_be() + 1), + // SAFETY: synthetic chain hash for tests — same invariant as the + // real Ethereum block hash. `HashOf` carries it verbatim. + hash: unsafe { HashOf::::new(raw_hash) }, header: BlockHeader { height: self.header.height + 1, parent_hash: self.hash, @@ -891,7 +917,8 @@ impl Arbitrary for DBConfig { chain_id: 0, router_address: Address::default(), timelines, - genesis_block_hash, + // SAFETY: synthetic chain hash for mocks — same invariant as a real EB hash. + genesis_block_hash: unsafe { HashOf::::new(genesis_block_hash) }, max_validators: 0, }) .boxed() @@ -918,11 +945,17 @@ impl Arbitrary for DBGlobals { latest_finalized_mb_hash, latest_computed_mb_hash, )| Self { - start_block_hash, + // SAFETY: synthetic chain hash for mocks — same invariant as a real EB hash. + start_block_hash: unsafe { HashOf::::new(start_block_hash) }, latest_synced_eb, - latest_prepared_eb_hash, - latest_finalized_mb_hash, - latest_computed_mb_hash, + // SAFETY: synthetic chain hash for mocks — same invariant as a real EB hash. + latest_prepared_eb_hash: unsafe { HashOf::::new(latest_prepared_eb_hash) }, + // SAFETY: synthetic MB envelope hash for mocks. + latest_finalized_mb_hash: unsafe { + HashOf::::new(latest_finalized_mb_hash) + }, + // SAFETY: synthetic MB envelope hash for mocks. + latest_computed_mb_hash: unsafe { HashOf::::new(latest_computed_mb_hash) }, }, ) .boxed() diff --git a/ethexe/common/src/primitives.rs b/ethexe/common/src/primitives.rs index 13cc16ef0ea..39419c00211 100644 --- a/ethexe/common/src/primitives.rs +++ b/ethexe/common/src/primitives.rs @@ -1,7 +1,7 @@ // Copyright (C) Gear Technologies Inc. // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 -use crate::events::BlockEvent; +use crate::{events::BlockEvent, hash::HashOf}; use alloc::{ collections::{btree_map::BTreeMap, btree_set::BTreeSet}, vec::Vec, @@ -19,25 +19,26 @@ pub type ProgramStates = BTreeMap; pub struct BlockHeader { pub height: u32, pub timestamp: u64, - pub parent_hash: H256, + pub parent_hash: HashOf, } impl BlockHeader { pub fn dummy(height: u32) -> Self { - let mut parent_hash = [0; 32]; - parent_hash[..4].copy_from_slice(&height.to_le_bytes()); + let mut parent_hash_bytes = [0u8; 32]; + parent_hash_bytes[..4].copy_from_slice(&height.to_le_bytes()); Self { height, timestamp: height as u64 * 12, - parent_hash: parent_hash.into(), + // SAFETY: synthetic deterministic dummy hash for tests/fixtures. + parent_hash: unsafe { HashOf::::new(parent_hash_bytes.into()) }, } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct BlockData { - pub hash: H256, + pub hash: HashOf, pub header: BlockHeader, pub events: Vec, } @@ -56,10 +57,15 @@ impl BlockData { )] #[display("Block(hash: {hash}, height: {}, parent: {}, ts: {})", header.height, header.parent_hash, header.timestamp)] pub struct SimpleBlockData { - pub hash: H256, + pub hash: HashOf, pub header: BlockHeader, } +/// Ethereum block alias. `HashOf` carries the raw Ethereum block hash +/// verbatim — no Blake2b recomputation — because the canonical EB identity +/// already lives on the chain side. +pub type EB = SimpleBlockData; + /// [`PromisePolicy`] tells processor whether should it emits promises or not. #[derive(Clone, Debug, Copy, Default, PartialEq, Eq, Encode, Decode, derive_more::IsVariant)] pub enum PromisePolicy { diff --git a/ethexe/common/src/utils.rs b/ethexe/common/src/utils.rs index caecae4bc50..8350f1b22b6 100644 --- a/ethexe/common/src/utils.rs +++ b/ethexe/common/src/utils.rs @@ -1,8 +1,10 @@ // Copyright (C) Gear Technologies Inc. // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 -use crate::db::{BlockMeta, BlockMetaStorageRW, OnChainStorageRW, PreparedBlockData}; -use gprimitives::H256; +use crate::{ + EB, HashOf, + db::{BlockMeta, BlockMetaStorageRW, OnChainStorageRW, PreparedBlockData}, +}; /// Decodes hexed string to a byte array. pub fn decode_to_array(s: &str) -> Result<[u8; N], hex::FromHexError> { @@ -25,7 +27,7 @@ pub const fn u64_into_uint48_be_bytes_lossy(val: u64) -> [u8; 6] { pub fn setup_block_in_db( db: &DB, - block_hash: H256, + block_hash: HashOf, block_data: PreparedBlockData, ) { db.set_block_header(block_hash, block_data.header); diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 970f7d30da9..8c742057ab4 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -11,16 +11,17 @@ use crate::{ComputeError, ComputeEvent, ProcessorExt, Result, service::SubService}; use ethexe_common::{ - PromiseEmissionMode, PromisePolicy, + EB, HashOf, PromiseEmissionMode, PromisePolicy, db::{CodesStorageRW, CompactMb, ConfigStorageRO, MbStorageRO, MbStorageRW, OnChainStorageRO}, events::BlockRequestEvent, injected::Promise, - malachite::{Operation, Operations}, + malachite::{MB, Operation, Operations}, }; use ethexe_db::Database; use ethexe_processor::{BoundPromiseSink, ExecutableData}; use ethexe_runtime_common::FinalizedBlockTransitions; use futures::{FutureExt, Stream, StreamExt, future::BoxFuture}; +#[cfg(test)] use gprimitives::H256; use std::{ collections::VecDeque, @@ -37,11 +38,11 @@ use tokio::sync::mpsc; /// instead — `AlwaysEmit` re-emits, `ConsensusDriven` stays silent. #[derive(Debug)] pub(crate) struct MbComputeRequest { - pub mb_hash: H256, + pub mb_hash: HashOf, pub promise_policy: PromisePolicy, } -type ComputationFuture = future_timing::Timed>>; +type ComputationFuture = future_timing::Timed>>>; /// Metrics for the [`ComputeSubService`]. #[derive(Clone, metrics_derive::Metrics)] @@ -56,7 +57,7 @@ struct Metrics { /// /// The MB hash arrives on the channel pre-tagged by [`BoundPromiseSink`]. struct MbPromisesStream { - receiver: mpsc::UnboundedReceiver<(H256, Promise)>, + receiver: mpsc::UnboundedReceiver<(HashOf, Promise)>, } impl Stream for MbPromisesStream { @@ -84,7 +85,7 @@ pub struct ComputeSubService { input: VecDeque, /// Head of the in-flight computation, kept so [`Self::receive_mb`] can /// skip duplicates that would otherwise re-emit `MbComputed`. - in_flight_mb: Option, + in_flight_mb: Option>, computation: Option, /// Per-MB promise channel; polled before `computation` so promises stream out live. promises_stream: Option, @@ -115,7 +116,7 @@ impl ComputeSubService

{ } } - pub fn receive_mb(&mut self, mb_hash: H256, promise_policy: PromisePolicy) { + pub fn receive_mb(&mut self, mb_hash: HashOf, promise_policy: PromisePolicy) { // Idempotent: skip if already computed, in flight, or queued — // otherwise BlockProposal+BlockFinalized for the same head emit // `MbComputed` twice. @@ -136,8 +137,8 @@ impl ComputeSubService

{ mut processor: P, req: MbComputeRequest, promise_emission_mode: PromiseEmissionMode, - promise_tx: mpsc::UnboundedSender<(H256, Promise)>, - ) -> Result { + promise_tx: mpsc::UnboundedSender<(HashOf, Promise)>, + ) -> Result> { let MbComputeRequest { mb_hash: head_mb_hash, promise_policy, @@ -179,7 +180,7 @@ impl ComputeSubService

{ async fn compute_one( db: &Database, processor: &mut P, - mb_hash: H256, + mb_hash: HashOf, compact_mb: CompactMb, promise_sink: Option, ) -> Result<()> { @@ -215,7 +216,7 @@ impl ComputeSubService

{ /// Builds executable data for a single MB, parent MB must be computed. pub fn prepare_executable_for_mb( db: &Database, - mb_hash: H256, + mb_hash: HashOf, compact_mb: CompactMb, ) -> Result { let CompactMb { @@ -262,7 +263,7 @@ fn build_executable_data( operations: Operations, program_states: ethexe_common::ProgramStates, schedule: ethexe_common::Schedule, - initial_advanced_block: H256, + initial_advanced_block: HashOf, ) -> Result { let mut events: Vec = Vec::new(); let mut injected_transactions = Vec::new(); @@ -319,7 +320,11 @@ fn build_executable_data( } /// EBs in `(last_advanced, target]`, oldest-first; capped at 1024. -fn collect_advance_chain(db: &Database, target: H256, last_advanced: H256) -> Result> { +fn collect_advance_chain( + db: &Database, + target: HashOf, + last_advanced: HashOf, +) -> Result>> { const MAX_ADVANCE_STEPS: usize = 1024; if target == last_advanced { @@ -328,7 +333,7 @@ fn collect_advance_chain(db: &Database, target: H256, last_advanced: H256) -> Re let mut chain = Vec::new(); let mut current = target; - while current != last_advanced && current != H256::zero() { + while current != last_advanced && !current.is_zero() { if chain.len() >= MAX_ADVANCE_STEPS { return Err(ComputeError::AdvanceWalkTooDeep { target, @@ -354,8 +359,8 @@ fn collect_advance_chain(db: &Database, target: H256, last_advanced: H256) -> Re /// Returns an error if any MB in the chain is missing from the DB. fn collect_uncomputed_chain( db: &Database, - head_mb_hash: H256, -) -> Result> { + head_mb_hash: HashOf, +) -> Result, CompactMb)>> { let mut chain = VecDeque::new(); let mut mb_hash = head_mb_hash; while !mb_hash.is_zero() && !db.mb_meta(mb_hash).computed { @@ -463,18 +468,28 @@ mod tests { use gprimitives::{ActorId, CodeId, MessageId}; use proptest::prelude::*; + fn eb_hash_of(raw: u64) -> HashOf { + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + unsafe { HashOf::::new(H256::from_low_u64_be(raw)) } + } + + fn mb_hash_of(raw: u64) -> HashOf { + // SAFETY: synthetic MB hash for tests — same invariant as a real MB envelope hash. + unsafe { HashOf::::new(H256::from_low_u64_be(raw)) } + } + fn dummy_ops(db: &Database, tag: u8) -> Operations { // Tag-derived AdvanceTillEthereumBlock makes each block's // operations list (and thus its CAS hash) unique across heights. // The referenced EB also needs a header in the DB so the // compute-side advance walk picks it up. - let eth_block_hash = H256::from_low_u64_be(0xEB00 + tag as u64); + let eth_block_hash = eb_hash_of(0xEB00 + tag as u64); db.set_block_header( eth_block_hash, BlockHeader { height: tag as u32, timestamp: tag as u64, - parent_hash: H256::zero(), + parent_hash: HashOf::::zero(), }, ); db.set_block_events(eth_block_hash, &[]); @@ -490,7 +505,13 @@ mod tests { } /// Mimics malachite `process_mb_proposal`: CAS write + `CompactMb`. - fn seed_mb(db: &Database, mb_hash: H256, parent: H256, height: u64, ops: Operations) { + fn seed_mb( + db: &Database, + mb_hash: HashOf, + parent: HashOf, + height: u64, + ops: Operations, + ) { let operations_hash = db.set_operations(ops); db.set_mb_compact_block( mb_hash, @@ -498,6 +519,7 @@ mod tests { parent, height, operations_hash, + reserved: [0u8; 64], }, ); } @@ -516,9 +538,9 @@ mod tests { // 5-block chain; mb_hash = 0x1000 + i. const N: u64 = 5; let mut hashes = Vec::with_capacity(N as usize); - let mut parent = H256::zero(); + let mut parent = HashOf::::zero(); for i in 1..=N { - let mb_hash = H256::from_low_u64_be(0x1000 + i); + let mb_hash = mb_hash_of(0x1000 + i); seed_mb(&db, mb_hash, parent, i, dummy_ops(&db, i as u8)); hashes.push((i, mb_hash)); parent = mb_hash; @@ -555,10 +577,10 @@ mod tests { fn collect_uncomputed_chain_returns_oldest_first(chain_len in 2u64..=16) { let db = Database::memory(); let mut hashes = Vec::with_capacity(chain_len as usize); - let mut parent = H256::zero(); + let mut parent = HashOf::::zero(); for i in 1..=chain_len { - let mb_hash = H256::from_low_u64_be(0xB000 + i); + let mb_hash = mb_hash_of(0xB000 + i); seed_mb(&db, mb_hash, parent, i, dummy_ops(&db, i as u8)); hashes.push(mb_hash); parent = mb_hash; @@ -585,10 +607,10 @@ mod tests { #[test] fn collect_advance_chain_errors_on_missing_intermediate_header() { let db = Database::memory(); - let last_advanced = H256::from_low_u64_be(0xA0); - let parent_b = H256::from_low_u64_be(0xA1); - let parent_a = H256::from_low_u64_be(0xA2); - let target = H256::from_low_u64_be(0xA3); + let last_advanced = eb_hash_of(0xA0); + let parent_b = eb_hash_of(0xA1); + let parent_a = eb_hash_of(0xA2); + let target = eb_hash_of(0xA3); // target -> parent_a -> parent_b -> last_advanced // parent_b's header is intentionally missing. @@ -631,8 +653,8 @@ mod tests { let processor = MockProcessor::default(); let mut sub = ComputeSubService::new(db.clone(), processor); - let mb_hash = H256::from_low_u64_be(0xCAFE); - seed_mb(&db, mb_hash, H256::zero(), 1, dummy_ops(&db, 0)); + let mb_hash = mb_hash_of(0xCAFE); + seed_mb(&db, mb_hash, HashOf::::zero(), 1, dummy_ops(&db, 0)); db.mutate_mb_meta(mb_hash, |meta| { meta.computed = true; }); @@ -681,14 +703,14 @@ mod tests { /// Synthetic Ethereum block with a zeroed parent, so the compute-side /// advance walk collects exactly this single block. - fn synthetic_eb(db: &Database, tag: u8, events: Vec) -> H256 { - let hash = H256::from_low_u64_be(0xEB00 + tag as u64); + fn synthetic_eb(db: &Database, tag: u8, events: Vec) -> HashOf { + let hash = eb_hash_of(0xEB00 + tag as u64); db.set_block_header( hash, BlockHeader { height: tag as u32, timestamp: tag as u64, - parent_hash: H256::zero(), + parent_hash: HashOf::::zero(), }, ); db.set_block_events(hash, &events); @@ -700,7 +722,8 @@ mod tests { destination, payload: b"PING".to_vec().try_into().unwrap(), value: 0, - reference_block: H256::random(), + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + reference_block: unsafe { HashOf::::new(H256::random()) }, salt: H256::random().0.to_vec().try_into().unwrap(), }; SignedMessage::create(PrivateKey::random(), tx).expect("failed to sign injected tx") @@ -721,7 +744,7 @@ mod tests { db: &Database, processor: &mut Processor, pinger_count: u64, - ) -> Vec { + ) -> Vec> { let ping_code_id = upload_ping_code(processor, db).await; let ping_id = ActorId::from(0x10000); @@ -758,18 +781,18 @@ mod tests { }, ], ); - let creator = H256::from_low_u64_be(0x1000); + let creator = mb_hash_of(0x1000); let mut ops = vec![Operation::AdvanceTillEthereumBlock { block_hash: create_eb, }]; ops.extend(mb_bookend()); - seed_mb(db, creator, H256::zero(), 0, Operations::new(ops)); + seed_mb(db, creator, HashOf::::zero(), 0, Operations::new(ops)); mb_hashes.push(creator); // MB #1.. — each injects a single PING into the ping program. for i in 1..=pinger_count { let eb = synthetic_eb(db, i as u8, vec![]); - let mb_hash = H256::from_low_u64_be(0x1000 + i); + let mb_hash = mb_hash_of(0x1000 + i); let mut ops = vec![ Operation::AdvanceTillEthereumBlock { block_hash: eb }, Operation::Injected(ping_injected(ping_id)), @@ -794,7 +817,7 @@ mod tests { mode: PromiseEmissionMode, policy: PromisePolicy, pinger_count: u64, - ) -> (Vec, Vec<(H256, Promise)>) { + ) -> (Vec>, Vec<(HashOf, Promise)>) { let db = Database::memory(); seed_genesis_zero_mb(&db); let mut processor = Processor::new(db.clone()).expect("failed to create processor"); @@ -834,7 +857,7 @@ mod tests { .await; let head = *mb_hashes.last().unwrap(); - let emitting: Vec = promises.iter().map(|(mb, _)| *mb).collect(); + let emitting: Vec> = promises.iter().map(|(mb, _)| *mb).collect(); assert_eq!( emitting, vec![head], @@ -855,8 +878,8 @@ mod tests { // mb_hashes[0] creates the program (no injected tx); the three // pingers each produce one promise, in oldest-first order. - let expected: Vec = mb_hashes[1..].to_vec(); - let emitting: Vec = promises.iter().map(|(mb, _)| *mb).collect(); + let expected: Vec> = mb_hashes[1..].to_vec(); + let emitting: Vec> = promises.iter().map(|(mb, _)| *mb).collect(); assert_eq!( emitting, expected, "AlwaysEmit must surface a promise for every MB in the walked chain" diff --git a/ethexe/compute/src/lib.rs b/ethexe/compute/src/lib.rs index 03a6b3059a7..653695744ef 100644 --- a/ethexe/compute/src/lib.rs +++ b/ethexe/compute/src/lib.rs @@ -93,7 +93,7 @@ //! service layer enforces this by gating event emission inside //! `MalachiteService::receive_new_chain_head` (in `ethexe-malachite`). -use ethexe_common::{CodeAndIdUnchecked, injected::Promise}; +use ethexe_common::{CodeAndIdUnchecked, EB, HashOf, injected::Promise, malachite::MB}; use ethexe_processor::{ BoundPromiseSink, ExecutableData, ProcessedCodeInfo, Processor, ProcessorError, }; @@ -114,35 +114,35 @@ mod tests; #[derive(Debug, Clone, Eq, PartialEq)] pub struct BlockProcessed { - pub block_hash: H256, + pub block_hash: HashOf, } #[derive(Debug, Clone, Eq, PartialEq, derive_more::Unwrap, derive_more::From)] pub enum ComputeEvent { RequestLoadCodes(HashSet), CodeProcessed(CodeId), - BlockPrepared(H256), + BlockPrepared(HashOf), #[from(skip)] - MbComputed(H256), - Promise(Promise, H256), + MbComputed(HashOf), + Promise(Promise, HashOf), } #[derive(thiserror::Error, Debug)] pub enum ComputeError { #[error("block({0}) is not synced")] - BlockNotSynced(H256), + BlockNotSynced(HashOf), #[error("block({0}) is not prepared")] - BlockNotPrepared(H256), + BlockNotPrepared(HashOf), #[error("not found events for block({0})")] - BlockEventsNotFound(H256), + BlockEventsNotFound(HashOf), #[error("block header not found for synced block({0})")] - BlockHeaderNotFound(H256), + BlockHeaderNotFound(HashOf), #[error("block validators committed for era not found for block({0})")] - CommittedEraNotFound(H256), + CommittedEraNotFound(HashOf), #[error("codes queue not found for computed block({0})")] - CodesQueueNotFound(H256), + CodesQueueNotFound(HashOf), #[error("last committed batch not found for computed block({0})")] - LastCommittedBatchNotFound(H256), + LastCommittedBatchNotFound(HashOf), #[error( "Received validators commitment for an earlier era {commitment_era_index}, previous was {previous_commitment_era_index}" )] @@ -151,23 +151,29 @@ pub enum ComputeError { commitment_era_index: u64, }, #[error("MB payload {payload_hash} not found for mb {mb_hash}")] - MbPayloadNotFound { mb_hash: H256, payload_hash: H256 }, + MbPayloadNotFound { + mb_hash: HashOf, + payload_hash: H256, + }, #[error("MB {0} CompactMb is missing")] - MbCompactNotFound(H256), + MbCompactNotFound(HashOf), #[error("parent MB {0} marked computed but program_states row missing")] - ParentMbStatesMissing(H256), + ParentMbStatesMissing(HashOf), #[error("parent MB {0} marked computed but schedule row missing")] - ParentMbScheduleMissing(H256), + ParentMbScheduleMissing(HashOf), #[error("block events row missing for advance-chain block({0})")] - AdvanceBlockEventsMissing(H256), + AdvanceBlockEventsMissing(HashOf), #[error("anchor Eth block header missing for {0}")] - AnchorBlockHeaderMissing(H256), + AnchorBlockHeaderMissing(HashOf), #[error("AdvanceTillEthereumBlock walk hit a missing parent header at {hash}")] - AdvanceMissingHeader { hash: H256 }, + AdvanceMissingHeader { hash: HashOf }, #[error( "AdvanceTillEthereumBlock walk from {target} to {last_advanced} exceeded the safety cap" )] - AdvanceWalkTooDeep { target: H256, last_advanced: H256 }, + AdvanceWalkTooDeep { + target: HashOf, + last_advanced: HashOf, + }, #[error(transparent)] Processor(#[from] ProcessorError), diff --git a/ethexe/compute/src/prepare.rs b/ethexe/compute/src/prepare.rs index 0140b75dfe7..64e83c34fce 100644 --- a/ethexe/compute/src/prepare.rs +++ b/ethexe/compute/src/prepare.rs @@ -3,7 +3,7 @@ use crate::{ComputeError, ComputeEvent, Result, service::SubService}; use ethexe_common::{ - BlockData, + BlockData, EB, HashOf, db::{ BlockMetaStorageRO, BlockMetaStorageRW, CodesStorageRO, GlobalsStorageRW, OnChainStorageRO, OnChainStorageRW, @@ -15,9 +15,10 @@ use ethexe_common::{ EBCommittedEvent, MBCommittedEvent, ValidatorsCommittedForEraEvent, }, }, + malachite::MB, }; use ethexe_db::Database; -use gprimitives::{CodeId, H256}; +use gprimitives::CodeId; use metrics::Gauge; use std::{ collections::{HashSet, VecDeque}, @@ -26,7 +27,7 @@ use std::{ #[derive(Debug, Clone, PartialEq, Eq)] pub enum Event { - BlockPrepared(H256), + BlockPrepared(HashOf), RequestCodes(HashSet), } @@ -61,7 +62,7 @@ struct Metrics { pub struct PrepareSubService { db: Database, state: State, - input: VecDeque, + input: VecDeque>, metrics: Metrics, } @@ -75,7 +76,7 @@ impl PrepareSubService { } } - pub fn receive_block_to_prepare(&mut self, block: H256) { + pub fn receive_block_to_prepare(&mut self, block: HashOf) { self.input.push_back(block); self.metrics.blocks_queue_len.set(self.input.len() as f64); } @@ -169,7 +170,7 @@ impl SubService for PrepareSubService { /// Returns the collected blocks in a `VecDeque`, ordered from oldest to newest. fn collect_not_prepared_blocks_chain( db: &Database, - mut block_hash: H256, + mut block_hash: HashOf, ) -> Result> { let mut chain = VecDeque::new(); @@ -284,7 +285,7 @@ fn prepare_one_block = None; + let mut last_committed_eb: Option> = None; for event in block.events { match event { @@ -306,7 +307,8 @@ fn prepare_one_block { - last_committed_eb = Some(eth_block_hash); + // SAFETY: real on-chain EB hash from Router event. + last_committed_eb = Some(unsafe { HashOf::::new(eth_block_hash) }); } BlockEvent::Router(RouterEvent::ValidatorsCommittedForEra( @@ -330,7 +332,8 @@ fn prepare_one_block::new(hash) }) } else { parent_meta.last_committed_mb }; @@ -446,8 +449,10 @@ mod tests { prop_assert!(meta.prepared); prop_assert_eq!(meta.codes_queue, Some(vec![code2_id].into())); prop_assert_eq!(meta.last_committed_batch, Some(batch_committed)); - prop_assert_eq!(meta.last_committed_mb, Some(block1_mb_hash)); - prop_assert_eq!(meta.last_committed_eb, Some(block1_eb_hash)); + // SAFETY: on-chain MB hash mirror — same invariant as the chain event payload. + prop_assert_eq!(meta.last_committed_mb, Some(unsafe { HashOf::::new(block1_mb_hash) })); + // SAFETY: on-chain EB hash mirror — same invariant as the chain event payload. + prop_assert_eq!(meta.last_committed_eb, Some(unsafe { HashOf::::new(block1_eb_hash) })); prop_assert_eq!(meta.latest_era_validators_committed, Some(expected_era)); } } diff --git a/ethexe/compute/src/service.rs b/ethexe/compute/src/service.rs index 30083f8e299..f3e941bd4dc 100644 --- a/ethexe/compute/src/service.rs +++ b/ethexe/compute/src/service.rs @@ -7,11 +7,12 @@ use crate::{ ComputeEvent, ProcessorExt, Result, codes::CodesSubService, compute::ComputeSubService, prepare::PrepareSubService, }; -use ethexe_common::{CodeAndIdUnchecked, PromiseEmissionMode, PromisePolicy}; +use ethexe_common::{ + CodeAndIdUnchecked, EB, HashOf, PromiseEmissionMode, PromisePolicy, malachite::MB, +}; use ethexe_db::Database; use ethexe_processor::Processor; use futures::{Stream, stream::FusedStream}; -use gprimitives::H256; use std::{ pin::Pin, task::{Context, Poll}, @@ -72,11 +73,11 @@ impl ComputeService

{ self.codes_sub_service.receive_code_to_process(code_and_id); } - pub fn prepare_block(&mut self, block: H256) { + pub fn prepare_block(&mut self, block: HashOf) { self.prepare_sub_service.receive_block_to_prepare(block); } - pub fn compute_mb(&mut self, mb_hash: H256, policy: PromisePolicy) { + pub fn compute_mb(&mut self, mb_hash: HashOf, policy: PromisePolicy) { self.mb_compute_sub_service.receive_mb(mb_hash, policy); } } @@ -146,14 +147,15 @@ mod tests { use gprimitives::{CodeId, H256}; use proptest::{collection, prelude::*}; - fn seed_mb(db: &DB, mb_hash: H256, gas_allowance: u64) { - let eth_block_hash = H256::from_low_u64_be(0xEB00); + fn seed_mb(db: &DB, mb_hash: HashOf, gas_allowance: u64) { + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + let eth_block_hash = unsafe { HashOf::::new(H256::from_low_u64_be(0xEB00)) }; db.set_block_header( eth_block_hash, BlockHeader { height: 1, timestamp: 1, - parent_hash: H256::zero(), + parent_hash: HashOf::::zero(), }, ); db.set_block_events(eth_block_hash, &[]); @@ -169,9 +171,10 @@ mod tests { db.set_mb_compact_block( mb_hash, CompactMb { - parent: H256::zero(), + parent: HashOf::::zero(), height: 1, operations_hash, + reserved: [0u8; 64], }, ); } @@ -206,7 +209,8 @@ mod tests { let db = DB::memory(); seed_genesis_zero_mb(&db); let mut service = ComputeService::new_mock_processor(db.clone()); - let mb_hash = H256::from_low_u64_be(0xCAFE); + // SAFETY: synthetic MB hash for tests — same invariant as a real MB envelope hash. + let mb_hash = unsafe { HashOf::::new(H256::from_low_u64_be(0xCAFE)) }; seed_mb(&db, mb_hash, gas_allowance); service.compute_mb(mb_hash, PromisePolicy::Disabled); diff --git a/ethexe/compute/src/tests.rs b/ethexe/compute/src/tests.rs index 93dbf7e6a35..648d8e3dca0 100644 --- a/ethexe/compute/src/tests.rs +++ b/ethexe/compute/src/tests.rs @@ -4,7 +4,7 @@ use super::*; use crate::service::SubService; use ethexe_common::{ - CodeBlobInfo, + CodeBlobInfo, EB, HashOf, db::*, events::{ BlockEvent, RouterEvent, @@ -216,7 +216,7 @@ impl TestEnv { TestEnv { db, compute, chain } } - async fn prepare_and_assert_block(&mut self, block: H256) { + async fn prepare_and_assert_block(&mut self, block: HashOf) { self.compute.prepare_block(block); match next_compute_event(&mut self.compute).await { diff --git a/ethexe/consensus/src/lib.rs b/ethexe/consensus/src/lib.rs index 3eb4b9ad4a1..2df518b9c77 100644 --- a/ethexe/consensus/src/lib.rs +++ b/ethexe/consensus/src/lib.rs @@ -47,7 +47,7 @@ use anyhow::Result; use ethexe_common::{ - Digest, SimpleBlockData, + Digest, EB, HashOf, SimpleBlockData, consensus::{BatchCommitmentValidationReply, VerifiedValidationRequest}, network::SignedValidatorMessage, }; @@ -69,10 +69,10 @@ pub trait ConsensusService: fn receive_new_chain_head(&mut self, block: SimpleBlockData) -> Result<()>; /// Process a synced block info - fn receive_synced_block(&mut self, block: H256) -> Result<()>; + fn receive_synced_block(&mut self, block: HashOf) -> Result<()>; /// Process a prepared block received - fn receive_prepared_block(&mut self, block: H256) -> Result<()>; + fn receive_prepared_block(&mut self, block: HashOf) -> Result<()>; /// Process a received validation request fn receive_validation_request(&mut self, request: VerifiedValidationRequest) -> Result<()>; diff --git a/ethexe/consensus/src/utils.rs b/ethexe/consensus/src/utils.rs index e52c678aca9..1f719a1bf4f 100644 --- a/ethexe/consensus/src/utils.rs +++ b/ethexe/consensus/src/utils.rs @@ -8,13 +8,12 @@ use anyhow::{Result, anyhow}; use ethexe_common::{ - Address, Digest, ToDigest, + Address, Digest, EB, HashOf, ToDigest, consensus::BatchCommitmentValidationReply, db::OnChainStorageRO, ecdsa::{ContractSignature, PublicKey}, gear::BatchCommitment, }; -use gprimitives::H256; use gsigner::secp256k1::{Secp256k1SignerExt, Signer}; use parity_scale_codec::{Decode, Encode}; use std::collections::{BTreeMap, HashSet}; @@ -112,11 +111,11 @@ pub fn has_duplicates(data: &[T]) -> bool { /// `target` lies on the canonical eth chain ending at `head` — i.e., `head` /// is `target` itself or one of its descendants reachable via parent links. -/// `target == H256::zero()` is the genesis sentinel and returns `Ok(true)`. +/// Zero `target` is the genesis sentinel and returns `Ok(true)`. pub fn is_eth_block_canonical_to( db: &DB, - target: H256, - head: H256, + target: HashOf, + head: HashOf, ) -> Result { if target.is_zero() { return Ok(true); diff --git a/ethexe/consensus/src/validator/batch/filler.rs b/ethexe/consensus/src/validator/batch/filler.rs index 11dad0c47b6..6602be657d2 100644 --- a/ethexe/consensus/src/validator/batch/filler.rs +++ b/ethexe/consensus/src/validator/batch/filler.rs @@ -131,6 +131,7 @@ impl BatchFiller { mod tests { use super::*; use alloy::sol_types::SolValue; + use ethexe_common::{EB, HashOf}; use ethexe_ethereum::abi::Gear; use gprimitives::{CodeId, H256}; @@ -144,7 +145,8 @@ mod tests { let checkpoint = ChainCommitment { head: H256::from_low_u64_be(0xC0DE), transitions: Vec::new(), - last_advanced_eth_block: H256::from_low_u64_be(0xEB), + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + last_advanced_eth_block: unsafe { HashOf::::new(H256::from_low_u64_be(0xEB)) }, }; filler.include_chain_commitment(checkpoint).unwrap(); diff --git a/ethexe/consensus/src/validator/batch/manager.rs b/ethexe/consensus/src/validator/batch/manager.rs index 28a07a410b4..9e4b28e866a 100644 --- a/ethexe/consensus/src/validator/batch/manager.rs +++ b/ethexe/consensus/src/validator/batch/manager.rs @@ -10,7 +10,7 @@ use crate::validator::{ use alloy::sol_types::SolValue; use anyhow::{Context as _, Result, anyhow, bail}; use ethexe_common::{ - SimpleBlockData, ToDigest, + HashOf, SimpleBlockData, ToDigest, consensus::BatchCommitmentValidationRequest, db::{ BlockMetaStorageRO, CodesStorageRO, ConfigStorageRO, GlobalsStorageRO, MbStorageRO, @@ -19,10 +19,10 @@ use ethexe_common::{ gear::{ BatchCommitment, ChainCommitment, CodeCommitment, RewardsCommitment, ValidatorsCommitment, }, + malachite::MB, }; use ethexe_db::Database; use ethexe_ethereum::abi::Gear; -use gprimitives::H256; use hashbrown::HashSet; #[derive(derive_more::Debug, Clone)] @@ -195,7 +195,11 @@ impl BatchCommitmentManager { .push(CodeCommitment { id, valid }); } - if let Some(head_mb) = head { + if let Some(head_mb_raw) = head { + // SAFETY: head_mb travels on-chain as H256; typing it here only + // gates DB lookups that the rest of this branch treats as MB. + let head_mb = unsafe { HashOf::::new(head_mb_raw) }; + // Mirror the coordinator-side guard: refuse to sign anything if our // own `latest_finalized_mb` advanced to a non-canonical Eth block // (deep Eth reorg past quarantine). The coordinator's advance must @@ -208,7 +212,7 @@ impl BatchCommitmentManager { return Ok(ValidationStatus::Rejected { request, reason: ValidationRejectReason::LatestFinalizedAdvanceNotCanonical( - latest_advanced, + latest_advanced.inner(), ), }); } @@ -227,7 +231,7 @@ impl BatchCommitmentManager { ); return Ok(ValidationStatus::Rejected { request, - reason: ValidationRejectReason::HeadMbNotFinalized(head_mb), + reason: ValidationRejectReason::HeadMbNotFinalized(head_mb_raw), }); } @@ -239,7 +243,7 @@ impl BatchCommitmentManager { ); return Ok(ValidationStatus::Rejected { request, - reason: ValidationRejectReason::HeadMbNotComputed(head_mb), + reason: ValidationRejectReason::HeadMbNotComputed(head_mb_raw), }); } @@ -247,7 +251,7 @@ impl BatchCommitmentManager { .db .block_meta(block.hash) .last_committed_mb - .unwrap_or(H256::zero()); + .unwrap_or(HashOf::::zero()); // Head must strictly advance past last-committed; genesis = height 0. let head_height = self @@ -278,7 +282,7 @@ impl BatchCommitmentManager { ); return Ok(ValidationStatus::Rejected { request, - reason: ValidationRejectReason::HeadMbAlreadyCommitted(head_mb), + reason: ValidationRejectReason::HeadMbAlreadyCommitted(head_mb_raw), }); } @@ -291,7 +295,7 @@ impl BatchCommitmentManager { let mut chain_commitment = ChainCommitment { transitions: Vec::new(), - head: head_mb, + head: head_mb_raw, last_advanced_eth_block: self.db.mb_meta(head_mb).last_advanced_eb, }; for mb_hash in pending.into_iter() { @@ -440,7 +444,7 @@ impl BatchCommitmentManager { }; let request = ElectionRequest { - at_block_hash: election_block.hash, + at_block_hash: election_block.hash.inner(), at_timestamp: election_ts, max_validators, }; diff --git a/ethexe/consensus/src/validator/batch/tests.rs b/ethexe/consensus/src/validator/batch/tests.rs index 008e856b361..0a6dd9c3b35 100644 --- a/ethexe/consensus/src/validator/batch/tests.rs +++ b/ethexe/consensus/src/validator/batch/tests.rs @@ -12,11 +12,11 @@ use super::{BatchCommitmentManager, BatchLimits, ValidationStatus, types::ValidationRejectReason}; use crate::validator::core::MiddlewareWrapper; use ethexe_common::{ - Address, Digest, ProgramStates, Schedule, SimpleBlockData, ToDigest, ValidatorsVec, + Address, Digest, EB, HashOf, ProgramStates, Schedule, SimpleBlockData, ToDigest, ValidatorsVec, consensus::BatchCommitmentValidationRequest, db::{BlockMetaStorageRW, CompactMb, GlobalsStorageRW, MbStorageRW, SetConfig}, gear::StateTransition, - malachite::{Operation, Operations}, + malachite::{MB, Operation, Operations}, mock::*, }; use ethexe_db::Database; @@ -58,22 +58,30 @@ fn mock_batch_manager(db: Database) -> BatchCommitmentManager { /// Append a single MB to the chain. Sets the meta as `computed=true` /// so the manager treats it as finalized state available for batching. -fn append_mb(db: &Database, parent: H256, height: u64, outcome: Vec) -> H256 { +fn append_mb( + db: &Database, + parent: HashOf, + height: u64, + outcome: Vec, +) -> HashOf { let ops = Operations::new(vec![ Operation::AdvanceTillEthereumBlock { - block_hash: H256::from_low_u64_be(0xEB00 + height), + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + block_hash: unsafe { HashOf::::new(H256::from_low_u64_be(0xEB00 + height)) }, }, Operation::ProcessQueues { gas_allowance: 0 }, ]); let operations_hash = db.set_operations(ops); // Synthetic mb_hash — uniqueness is what matters here. - let mb_hash = H256::from_low_u64_be(0x1000 + height); + // SAFETY: synthetic MB hash for tests — same invariant as a real MB envelope hash. + let mb_hash = unsafe { HashOf::::new(H256::from_low_u64_be(0x1000 + height)) }; db.set_mb_compact_block( mb_hash, CompactMb { parent, height, operations_hash, + reserved: [0u8; 64], }, ); db.set_mb_outcome(mb_hash, outcome); @@ -81,7 +89,7 @@ fn append_mb(db: &Database, parent: H256, height: u64, outcome: Vec::zero(); }); mb_hash } @@ -89,8 +97,8 @@ fn append_mb(db: &Database, parent: H256, height: u64, outcome: Vec>) -> Vec { - let mut parent = H256::zero(); +fn setup_mb_chain(db: &Database, outcomes: Vec>) -> Vec> { + let mut parent = HashOf::::zero(); let mut hashes = Vec::with_capacity(outcomes.len()); for (i, outcome) in outcomes.into_iter().enumerate() { let h = append_mb(db, parent, (i + 1) as u64, outcome); @@ -339,7 +347,7 @@ async fn rejects_head_mb_at_or_below_last_committed_mb() { .unwrap(); assert_eq!( unwrap_rejected(status), - ValidationRejectReason::HeadMbAlreadyCommitted(head) + ValidationRejectReason::HeadMbAlreadyCommitted(head.inner()) ); } @@ -377,7 +385,7 @@ async fn rejects_head_mb_not_computed() { .unwrap(); assert_eq!( unwrap_rejected(status), - ValidationRejectReason::HeadMbNotComputed(head) + ValidationRejectReason::HeadMbNotComputed(head.inner()) ); } @@ -637,7 +645,8 @@ async fn idle_chain_above_threshold_emits_checkpoint_batch_commitment() { "checkpoint must pin the head MB's last_advanced_eb on-chain", ); assert_eq!( - chain_commitment.head, head_mb, + chain_commitment.head, + head_mb.inner(), "checkpoint must reference the latest finalized MB", ); assert!( diff --git a/ethexe/consensus/src/validator/batch/utils.rs b/ethexe/consensus/src/validator/batch/utils.rs index d3481c55e27..d3b638de695 100644 --- a/ethexe/consensus/src/validator/batch/utils.rs +++ b/ethexe/consensus/src/validator/batch/utils.rs @@ -6,13 +6,14 @@ use crate::validator::batch::{filler::BatchFiller, types::BatchParts}; use anyhow::{Result, anyhow, bail}; use core::num::NonZero; use ethexe_common::{ - SimpleBlockData, + EB, HashOf, SimpleBlockData, db::{BlockMetaStorageRO, CodesStorageRO, MbStorageRO, OnChainStorageRO}, gear::{ BatchCommitment, ChainCommitment, CodeCommitment, Message, StateTransition, ValueClaim, }, + malachite::MB, }; -use gprimitives::{ActorId, H256}; +use gprimitives::ActorId; use std::collections::{HashMap, hash_map::Entry}; /// MBs in `(last_committed_mb, mb_hash]`, chronological order. Strict: errors @@ -21,14 +22,14 @@ use std::collections::{HashMap, hash_map::Entry}; /// [`collect_computed_uncommitted_predecessors`]. pub fn collect_not_committed_mb_predecessors( db: &DB, - last_committed_mb: H256, - mb_hash: H256, -) -> Result> { + last_committed_mb: HashOf, + mb_hash: HashOf, +) -> Result>> { let mut mbs = Vec::new(); let mut current = mb_hash; while current != last_committed_mb { - if current == H256::zero() { + if current.is_zero() { bail!( "MB chain walk reached genesis without finding last_committed_mb {last_committed_mb}" ); @@ -54,20 +55,20 @@ pub fn collect_not_committed_mb_predecessors( /// computed or the parent walk doesn't reach the anchor (e.g. fresh restart). pub fn collect_computed_uncommitted_predecessors( db: &DB, - last_committed_mb: H256, - mb_head: H256, -) -> Vec { + last_committed_mb: HashOf, + mb_head: HashOf, +) -> Vec> { // Walk the parent chain backward from `mb_head` until we either // reach `last_committed_mb` or run off the local chain. let mut chain = Vec::new(); // newest-first let mut current = mb_head; - while current != last_committed_mb && current != H256::zero() { + while current != last_committed_mb && !current.is_zero() { let meta = db.mb_meta(current); chain.push((current, meta.computed)); current = db .mb_compact_block(current) .map(|c| c.parent) - .unwrap_or(H256::zero()); + .unwrap_or(HashOf::::zero()); } if current != last_committed_mb { // Walk didn't reach the anchor (fast-restart / sync-lag); caller retries. @@ -95,27 +96,27 @@ pub fn collect_computed_uncommitted_predecessors( /// `true` iff `candidate` is reachable from `latest_finalized_mb` by walking /// `parent_mb_hash`. Sound by BFT linear-order; bounded by the height gap. -/// `H256::zero()` is the genesis sentinel. +/// Zero is the genesis sentinel. pub fn is_finalized_locally( db: &DB, - candidate: H256, - latest_finalized_mb: H256, + candidate: HashOf, + latest_finalized_mb: HashOf, ) -> bool { - if candidate == H256::zero() || candidate == latest_finalized_mb { + if candidate.is_zero() || candidate == latest_finalized_mb { return true; } - if latest_finalized_mb == H256::zero() { + if latest_finalized_mb.is_zero() { return false; } let mut current = latest_finalized_mb; - while current != H256::zero() { + while !current.is_zero() { if current == candidate { return true; } current = db .mb_compact_block(current) .map(|c| c.parent) - .unwrap_or(H256::zero()); + .unwrap_or(HashOf::::zero()); } false } @@ -172,7 +173,7 @@ pub fn create_batch_commitment( /// once the filler rejects further additions (e.g. size limit). pub fn aggregate_code_commitments_for_block( db: &DB, - block_hash: H256, + block_hash: HashOf, batch_filler: &mut BatchFiller, ) -> Result<()> { let queue = db @@ -200,14 +201,14 @@ pub fn aggregate_code_commitments_for_block( db: &DB, - at_block: H256, - mb_head: H256, + at_block: HashOf, + mb_head: HashOf, batch_filler: &mut BatchFiller, -) -> Result { +) -> Result> { let last_committed_mb = db .block_meta(at_block) .last_committed_mb - .unwrap_or(H256::zero()); + .unwrap_or(HashOf::::zero()); let pending = collect_computed_uncommitted_predecessors(db, last_committed_mb, mb_head); @@ -228,7 +229,7 @@ pub fn try_include_chain_commitment( let len_before = transitions.len(); transitions.extend(mb_transitions); let trial_commitment = ChainCommitment { - head: *mb_hash, + head: mb_hash.inner(), transitions, last_advanced_eth_block: db.mb_meta(*mb_hash).last_advanced_eb, }; @@ -254,7 +255,7 @@ pub fn try_include_chain_commitment( } let commitment = ChainCommitment { - head: last_included, + head: last_included.inner(), transitions, last_advanced_eth_block: db.mb_meta(last_included).last_advanced_eb, }; @@ -276,8 +277,8 @@ pub fn try_include_checkpoint_chain_commitment< DB: BlockMetaStorageRO + MbStorageRO + OnChainStorageRO, >( db: &DB, - at_block: H256, - mb_head: H256, + at_block: HashOf, + mb_head: HashOf, threshold: NonZero, batch_filler: &mut BatchFiller, ) -> Result<()> { @@ -312,7 +313,7 @@ pub fn try_include_checkpoint_chain_commitment< } let commitment = ChainCommitment { - head: mb_head, + head: mb_head.inner(), transitions: Vec::new(), last_advanced_eth_block: advanced, }; @@ -475,41 +476,54 @@ mod tests { malachite::{Operation, Operations}, }; use ethexe_db::Database; + use gprimitives::H256; /// Per-height unique CAS via `AdvanceTillEthereumBlock` salt. fn empty_ops(height: u64) -> Operations { Operations::new(vec![ Operation::AdvanceTillEthereumBlock { - block_hash: H256::from_low_u64_be(0xEB00 + height), + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + block_hash: unsafe { HashOf::::new(H256::from_low_u64_be(0xEB00 + height)) }, }, Operation::ProcessQueues { gas_allowance: 0 }, ]) } + fn mb_hash_of(raw: u64) -> HashOf { + // SAFETY: synthetic MB hash for tests — same invariant as a real MB envelope hash. + unsafe { HashOf::::new(H256::from_low_u64_be(raw)) } + } + + fn eb_hash_of(raw: u64) -> HashOf { + // SAFETY: synthetic EB hash for tests — same invariant as a real chain hash. + unsafe { HashOf::::new(H256::from_low_u64_be(raw)) } + } + /// Mimics malachite `process_mb_proposal` + executor's `meta.computed` flip. fn write_mb( db: &Database, - parent_mb: H256, + parent_mb: HashOf, height: u64, outcome: Vec, - ) -> H256 { + ) -> HashOf { let ops = empty_ops(height); let operations_hash = db.set_operations(ops); // Synthetic mb_hash; only uniqueness matters here. - let mb_hash = H256::from_low_u64_be(0x1000 + height); + let mb_hash = mb_hash_of(0x1000 + height); db.set_mb_compact_block( mb_hash, CompactMb { parent: parent_mb, height, operations_hash, + reserved: [0u8; 64], }, ); db.set_mb_outcome(mb_hash, outcome); db.set_mb_schedule(mb_hash, Schedule::default()); db.mutate_mb_meta(mb_hash, |meta| { meta.computed = true; - meta.last_advanced_eb = H256::zero(); + meta.last_advanced_eb = HashOf::::zero(); }); mb_hash } @@ -517,11 +531,11 @@ mod tests { #[test] fn collect_predecessors_walks_chain() { let db = Database::memory(); - let mb1 = write_mb(&db, H256::zero(), 1, vec![]); + let mb1 = write_mb(&db, HashOf::::zero(), 1, vec![]); let mb2 = write_mb(&db, mb1, 2, vec![]); let mb3 = write_mb(&db, mb2, 3, vec![]); - let walked = collect_not_committed_mb_predecessors(&db, H256::zero(), mb3).unwrap(); + let walked = collect_not_committed_mb_predecessors(&db, HashOf::::zero(), mb3).unwrap(); assert_eq!(walked, vec![mb1, mb2, mb3]); let from_mb1 = collect_not_committed_mb_predecessors(&db, mb1, mb3).unwrap(); @@ -531,7 +545,7 @@ mod tests { #[test] fn collect_predecessors_returns_empty_when_at_target() { let db = Database::memory(); - let mb1 = write_mb(&db, H256::zero(), 1, vec![]); + let mb1 = write_mb(&db, HashOf::::zero(), 1, vec![]); let walked = collect_not_committed_mb_predecessors(&db, mb1, mb1).unwrap(); assert!(walked.is_empty()); @@ -540,11 +554,11 @@ mod tests { #[test] fn collect_predecessors_errors_when_target_not_in_chain() { let db = Database::memory(); - let mb1 = write_mb(&db, H256::zero(), 1, vec![]); + let mb1 = write_mb(&db, HashOf::::zero(), 1, vec![]); let mb2 = write_mb(&db, mb1, 2, vec![]); // mb2 cannot trace back to a hash that's not on the chain. - let bogus = H256::from_low_u64_be(0xDEAD); + let bogus = mb_hash_of(0xDEAD); let err = collect_not_committed_mb_predecessors(&db, bogus, mb2).unwrap_err(); let msg = format!("{err:#}"); assert!(msg.contains("genesis"), "got: {msg}"); @@ -553,12 +567,13 @@ mod tests { #[test] fn collect_predecessors_errors_on_uncomputed_mb() { let db = Database::memory(); - let mb1 = write_mb(&db, H256::zero(), 1, vec![]); + let mb1 = write_mb(&db, HashOf::::zero(), 1, vec![]); let mb2 = write_mb(&db, mb1, 2, vec![]); // Force mb2 to look uncomputed. db.mutate_mb_meta(mb2, |meta| meta.computed = false); - let err = collect_not_committed_mb_predecessors(&db, H256::zero(), mb2).unwrap_err(); + let err = + collect_not_committed_mb_predecessors(&db, HashOf::::zero(), mb2).unwrap_err(); let msg = format!("{err:#}"); assert!(msg.contains("not computed"), "got: {msg}"); } @@ -566,11 +581,11 @@ mod tests { #[test] fn lenient_collect_returns_full_range_when_all_computed() { let db = Database::memory(); - let mb1 = write_mb(&db, H256::zero(), 1, vec![]); + let mb1 = write_mb(&db, HashOf::::zero(), 1, vec![]); let mb2 = write_mb(&db, mb1, 2, vec![]); let mb3 = write_mb(&db, mb2, 3, vec![]); - let walked = collect_computed_uncommitted_predecessors(&db, H256::zero(), mb3); + let walked = collect_computed_uncommitted_predecessors(&db, HashOf::::zero(), mb3); assert_eq!(walked, vec![mb1, mb2, mb3]); let from_mb1 = collect_computed_uncommitted_predecessors(&db, mb1, mb3); @@ -580,33 +595,33 @@ mod tests { #[test] fn lenient_collect_truncates_at_first_uncomputed() { let db = Database::memory(); - let mb1 = write_mb(&db, H256::zero(), 1, vec![]); + let mb1 = write_mb(&db, HashOf::::zero(), 1, vec![]); let mb2 = write_mb(&db, mb1, 2, vec![]); let mb3 = write_mb(&db, mb2, 3, vec![]); // Compute is lagging: mb2 hasn't finished yet. db.mutate_mb_meta(mb2, |meta| meta.computed = false); // Only mb1 is contiguous-computed from anchor; mb2 gap blocks the rest. - let walked = collect_computed_uncommitted_predecessors(&db, H256::zero(), mb3); + let walked = collect_computed_uncommitted_predecessors(&db, HashOf::::zero(), mb3); assert_eq!(walked, vec![mb1]); } #[test] fn lenient_collect_returns_empty_when_first_successor_uncomputed() { let db = Database::memory(); - let mb1 = write_mb(&db, H256::zero(), 1, vec![]); + let mb1 = write_mb(&db, HashOf::::zero(), 1, vec![]); db.mutate_mb_meta(mb1, |meta| meta.computed = false); - let walked = collect_computed_uncommitted_predecessors(&db, H256::zero(), mb1); + let walked = collect_computed_uncommitted_predecessors(&db, HashOf::::zero(), mb1); assert!(walked.is_empty()); } #[test] fn lenient_collect_returns_empty_when_chain_does_not_reach_anchor() { let db = Database::memory(); - let mb1 = write_mb(&db, H256::zero(), 1, vec![]); + let mb1 = write_mb(&db, HashOf::::zero(), 1, vec![]); - let bogus = H256::from_low_u64_be(0xDEAD); + let bogus = mb_hash_of(0xDEAD); // Walk doesn't hit `bogus`; producer skips silently instead of erroring. let walked = collect_computed_uncommitted_predecessors(&db, bogus, mb1); assert!(walked.is_empty()); @@ -615,7 +630,7 @@ mod tests { #[test] fn lenient_collect_returns_empty_when_at_target() { let db = Database::memory(); - let mb1 = write_mb(&db, H256::zero(), 1, vec![]); + let mb1 = write_mb(&db, HashOf::::zero(), 1, vec![]); let walked = collect_computed_uncommitted_predecessors(&db, mb1, mb1); assert!(walked.is_empty()); @@ -624,23 +639,27 @@ mod tests { #[test] fn is_finalized_zero_candidate_is_universally_finalized() { let db = Database::memory(); - let mb1 = write_mb(&db, H256::zero(), 1, vec![]); - assert!(is_finalized_locally(&db, H256::zero(), mb1)); + let mb1 = write_mb(&db, HashOf::::zero(), 1, vec![]); + assert!(is_finalized_locally(&db, HashOf::::zero(), mb1)); // Even with no local finalization yet, zero is the genesis sentinel. - assert!(is_finalized_locally(&db, H256::zero(), H256::zero())); + assert!(is_finalized_locally( + &db, + HashOf::::zero(), + HashOf::::zero() + )); } #[test] fn is_finalized_self_is_finalized() { let db = Database::memory(); - let mb1 = write_mb(&db, H256::zero(), 1, vec![]); + let mb1 = write_mb(&db, HashOf::::zero(), 1, vec![]); assert!(is_finalized_locally(&db, mb1, mb1)); } #[test] fn is_finalized_resolves_proper_ancestor_of_finalized_head() { let db = Database::memory(); - let mb1 = write_mb(&db, H256::zero(), 1, vec![]); + let mb1 = write_mb(&db, HashOf::::zero(), 1, vec![]); let mb2 = write_mb(&db, mb1, 2, vec![]); let mb3 = write_mb(&db, mb2, 3, vec![]); // Latest finalized is mb3 → mb1 and mb2 are also finalized. @@ -652,7 +671,7 @@ mod tests { fn is_finalized_returns_false_for_descendant_of_finalized_head() { // Speculative-but-not-yet-finalized candidate must fail strict check. let db = Database::memory(); - let mb1 = write_mb(&db, H256::zero(), 1, vec![]); + let mb1 = write_mb(&db, HashOf::::zero(), 1, vec![]); let mb2 = write_mb(&db, mb1, 2, vec![]); let mb3 = write_mb(&db, mb2, 3, vec![]); assert!(!is_finalized_locally(&db, mb3, mb1)); @@ -662,8 +681,8 @@ mod tests { #[test] fn is_finalized_returns_false_when_no_local_finalization() { let db = Database::memory(); - let mb1 = write_mb(&db, H256::zero(), 1, vec![]); - assert!(!is_finalized_locally(&db, mb1, H256::zero())); + let mb1 = write_mb(&db, HashOf::::zero(), 1, vec![]); + assert!(!is_finalized_locally(&db, mb1, HashOf::::zero())); } #[test] @@ -676,12 +695,12 @@ mod tests { use std::num::NonZero; let db = Database::memory(); - let block_hash = H256::from_low_u64_be(0xB10C); + let block_hash = eb_hash_of(0xB10C); db.set_block_header( block_hash, BlockHeader { height: 7, - parent_hash: H256::zero(), + parent_hash: HashOf::::zero(), timestamp: 1234, }, ); @@ -706,8 +725,8 @@ mod tests { value_claims: vec![], messages: vec![], }], - head: block_hash, - last_advanced_eth_block: H256::zero(), + head: block_hash.inner(), + last_advanced_eth_block: HashOf::::zero(), }), code_commitments: vec![], validators_commitment: None, @@ -736,14 +755,15 @@ mod tests { #[test] fn is_finalized_returns_false_on_disjoint_chain() { let db = Database::memory(); - let chain_a = write_mb(&db, H256::zero(), 1, vec![]); - let chain_b_root = H256::from_low_u64_be(0xB001); + let chain_a = write_mb(&db, HashOf::::zero(), 1, vec![]); + let chain_b_root = mb_hash_of(0xB001); db.set_mb_compact_block( chain_b_root, CompactMb { - parent: H256::from_low_u64_be(0xB000), // unknown parent + parent: mb_hash_of(0xB000), // unknown parent height: 1, operations_hash: db.set_operations(empty_ops(99)), + reserved: [0u8; 64], }, ); assert!(!is_finalized_locally(&db, chain_b_root, chain_a)); diff --git a/ethexe/consensus/src/validator/coordinator.rs b/ethexe/consensus/src/validator/coordinator.rs index 5267661985e..225e6d2b36d 100644 --- a/ethexe/consensus/src/validator/coordinator.rs +++ b/ethexe/consensus/src/validator/coordinator.rs @@ -237,7 +237,7 @@ impl Coordinator { let batch_digest = batch.to_digest(); let event = match cloned_committer.commit(batch, signatures).await { Ok(tx) => CommitmentSubmitted { - block_hash, + block_hash: block_hash.inner(), batch_digest, tx, }.into(), diff --git a/ethexe/consensus/src/validator/idle.rs b/ethexe/consensus/src/validator/idle.rs index b87f49dd09a..2b97f89dccb 100644 --- a/ethexe/consensus/src/validator/idle.rs +++ b/ethexe/consensus/src/validator/idle.rs @@ -22,10 +22,9 @@ use super::{ use anyhow::{Context as _, Result, anyhow}; use derive_more::{Debug, Display}; use ethexe_common::{ - SimpleBlockData, + EB, HashOf, SimpleBlockData, db::{BlockMetaStorageRO, OnChainStorageRO}, }; -use gprimitives::H256; /// Idle state — waits for the next Ethereum chain head and then routes to /// either [`Coordinator`] or [`Participant`] for that block. @@ -64,7 +63,7 @@ impl StateHandler for Idle { Self::create_with_chain_head(self.ctx, block) } - fn process_synced_block(mut self, block: H256) -> Result { + fn process_synced_block(mut self, block: HashOf) -> Result { match &self.state { SubState::WaitingForSynced { block: pending } if pending.hash == block => { let pending = *pending; @@ -81,7 +80,7 @@ impl StateHandler for Idle { } } - fn process_prepared_block(self, block: H256) -> Result { + fn process_prepared_block(self, block: HashOf) -> Result { match &self.state { SubState::WaitingForPrepared { block: pending } if pending.hash == block => { self.maybe_advance_to_role() diff --git a/ethexe/consensus/src/validator/mod.rs b/ethexe/consensus/src/validator/mod.rs index dc17d079a6d..b1f58a92f4f 100644 --- a/ethexe/consensus/src/validator/mod.rs +++ b/ethexe/consensus/src/validator/mod.rs @@ -33,8 +33,8 @@ use anyhow::Result; pub use core::BatchCommitter; use derive_more::{Debug, From}; use ethexe_common::{ - Address, SimpleBlockData, consensus::VerifiedValidationRequest, db::ConfigStorageRO, - ecdsa::PublicKey, + Address, EB, HashOf, SimpleBlockData, consensus::VerifiedValidationRequest, + db::ConfigStorageRO, ecdsa::PublicKey, }; use ethexe_db::Database; use ethexe_ethereum::middleware::ElectionProvider; @@ -43,7 +43,6 @@ use futures::{ future::BoxFuture, stream::{FusedStream, FuturesUnordered}, }; -use gprimitives::H256; use gsigner::secp256k1::Signer; use std::{ collections::VecDeque, @@ -170,11 +169,11 @@ impl ConsensusService for ValidatorService { self.update_inner(|inner| inner.process_new_head(block)) } - fn receive_synced_block(&mut self, block: H256) -> Result<()> { + fn receive_synced_block(&mut self, block: HashOf) -> Result<()> { self.update_inner(|inner| inner.process_synced_block(block)) } - fn receive_prepared_block(&mut self, block: H256) -> Result<()> { + fn receive_prepared_block(&mut self, block: HashOf) -> Result<()> { self.update_inner(|inner| inner.process_prepared_block(block)) } @@ -256,11 +255,11 @@ where DefaultProcessing::new_head(self.into(), block) } - fn process_synced_block(self, block: H256) -> Result { + fn process_synced_block(self, block: HashOf) -> Result { DefaultProcessing::synced_block(self.into(), block) } - fn process_prepared_block(self, block: H256) -> Result { + fn process_prepared_block(self, block: HashOf) -> Result { DefaultProcessing::prepared_block(self.into(), block) } @@ -326,11 +325,11 @@ impl StateHandler for ValidatorState { delegate_call!(self => process_new_head(block)) } - fn process_synced_block(self, block: H256) -> Result { + fn process_synced_block(self, block: HashOf) -> Result { delegate_call!(self => process_synced_block(block)) } - fn process_prepared_block(self, block: H256) -> Result { + fn process_prepared_block(self, block: HashOf) -> Result { delegate_call!(self => process_prepared_block(block)) } @@ -360,13 +359,13 @@ impl DefaultProcessing { Idle::create_with_chain_head(s.into().into_context(), block) } - fn synced_block(s: impl Into, block: H256) -> Result { + fn synced_block(s: impl Into, block: HashOf) -> Result { let mut s = s.into(); s.warning(format!("unexpected synced block: {block}")); Ok(s) } - fn prepared_block(s: impl Into, block: H256) -> Result { + fn prepared_block(s: impl Into, block: HashOf) -> Result { let mut s = s.into(); s.warning(format!("unexpected processed block: {block}")); Ok(s) diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index e1dc2b4e1f1..ee0ed42135b 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -10,7 +10,7 @@ use crate::{ use anyhow::{Context, Result}; use delegate::delegate; use ethexe_common::{ - BlockHeader, CodeBlobInfo, HashOf, ProgramStates, Schedule, ValidatorsVec, + BlockHeader, CodeBlobInfo, EB, HashOf, ProgramStates, Schedule, ValidatorsVec, db::{ BlockMeta, BlockMetaStorageRO, BlockMetaStorageRW, CodesStorageRO, CodesStorageRW, CompactMb, ConfigStorageRO, DBConfig, DBGlobals, GlobalsStorageRO, GlobalsStorageRW, @@ -20,7 +20,7 @@ use ethexe_common::{ events::BlockEvent, gear::StateTransition, injected::{InjectedTransaction, Promise, SignedInjectedTransaction, SignedTxReceipt}, - malachite::Operations, + malachite::{MB, Operations}, }; use ethexe_runtime_common::state::{ Allocations, DispatchStash, Mailbox, MemoryPages, MemoryPagesRegion, MessageQueue, @@ -362,9 +362,9 @@ impl RawDatabase { } impl MbStorageRO for RawDatabase { - fn mb_compact_block(&self, mb_hash: H256) -> Option { + fn mb_compact_block(&self, mb_hash: HashOf) -> Option { self.kv - .get(&Key::MbCompactBlock(mb_hash).to_bytes()) + .get(&Key::MbCompactBlock(mb_hash.inner()).to_bytes()) .map(|data| { CompactMb::decode(&mut data.as_slice()) .expect("Failed to decode data into `CompactMb`") @@ -378,36 +378,36 @@ impl MbStorageRO for RawDatabase { }) } - fn mb_program_states(&self, mb_hash: H256) -> Option { + fn mb_program_states(&self, mb_hash: HashOf) -> Option { self.kv - .get(&Key::MbProgramStates(mb_hash).to_bytes()) + .get(&Key::MbProgramStates(mb_hash.inner()).to_bytes()) .map(|data| { ProgramStates::decode(&mut data.as_slice()) .expect("Failed to decode data into `ProgramStates`") }) } - fn mb_outcome(&self, mb_hash: H256) -> Option> { + fn mb_outcome(&self, mb_hash: HashOf) -> Option> { self.kv - .get(&Key::MbOutcome(mb_hash).to_bytes()) + .get(&Key::MbOutcome(mb_hash.inner()).to_bytes()) .map(|data| { Vec::::decode(&mut data.as_slice()) .expect("Failed to decode data into `Vec`") }) } - fn mb_schedule(&self, mb_hash: H256) -> Option { + fn mb_schedule(&self, mb_hash: HashOf) -> Option { self.kv - .get(&Key::MbSchedule(mb_hash).to_bytes()) + .get(&Key::MbSchedule(mb_hash.inner()).to_bytes()) .map(|data| { Schedule::decode(&mut data.as_slice()) .expect("Failed to decode data into `Schedule`") }) } - fn mb_meta(&self, mb_hash: H256) -> MbMeta { + fn mb_meta(&self, mb_hash: HashOf) -> MbMeta { self.kv - .get(&Key::MbMeta(mb_hash).to_bytes()) + .get(&Key::MbMeta(mb_hash.inner()).to_bytes()) .map(|data| { MbMeta::decode(&mut data.as_slice()).expect("Failed to decode data into `MbMeta`") }) @@ -416,53 +416,60 @@ impl MbStorageRO for RawDatabase { } impl MbStorageRW for RawDatabase { - fn set_mb_compact_block(&self, mb_hash: H256, compact: CompactMb) { + fn set_mb_compact_block(&self, mb_hash: HashOf, compact: CompactMb) { tracing::trace!(mb_hash = %mb_hash, "Set MB compact block"); - self.kv - .put(&Key::MbCompactBlock(mb_hash).to_bytes(), compact.encode()); + self.kv.put( + &Key::MbCompactBlock(mb_hash.inner()).to_bytes(), + compact.encode(), + ); } fn set_operations(&self, operations: Operations) -> H256 { self.cas.write(&operations.encode()) } - fn set_mb_program_states(&self, mb_hash: H256, program_states: ProgramStates) { + fn set_mb_program_states(&self, mb_hash: HashOf, program_states: ProgramStates) { tracing::trace!(mb_hash = %mb_hash, "Set MB program states"); self.kv.put( - &Key::MbProgramStates(mb_hash).to_bytes(), + &Key::MbProgramStates(mb_hash.inner()).to_bytes(), program_states.encode(), ); } - fn set_mb_outcome(&self, mb_hash: H256, outcome: Vec) { + fn set_mb_outcome(&self, mb_hash: HashOf, outcome: Vec) { tracing::trace!(mb_hash = %mb_hash, "Set MB outcome"); - self.kv - .put(&Key::MbOutcome(mb_hash).to_bytes(), outcome.encode()); + self.kv.put( + &Key::MbOutcome(mb_hash.inner()).to_bytes(), + outcome.encode(), + ); } - fn set_mb_schedule(&self, mb_hash: H256, schedule: Schedule) { + fn set_mb_schedule(&self, mb_hash: HashOf, schedule: Schedule) { tracing::trace!(mb_hash = %mb_hash, "Set MB schedule"); - self.kv - .put(&Key::MbSchedule(mb_hash).to_bytes(), schedule.encode()); + self.kv.put( + &Key::MbSchedule(mb_hash.inner()).to_bytes(), + schedule.encode(), + ); } - fn mutate_mb_meta(&self, mb_hash: H256, f: impl FnOnce(&mut MbMeta)) { + fn mutate_mb_meta(&self, mb_hash: HashOf, f: impl FnOnce(&mut MbMeta)) { tracing::trace!(mb_hash = %mb_hash, "Mutate MB meta"); let mut meta = self.mb_meta(mb_hash); f(&mut meta); - self.kv.put(&Key::MbMeta(mb_hash).to_bytes(), meta.encode()); + self.kv + .put(&Key::MbMeta(mb_hash.inner()).to_bytes(), meta.encode()); } } impl OnChainStorageRO for RawDatabase { - fn block_header(&self, block_hash: H256) -> Option { + fn block_header(&self, block_hash: HashOf) -> Option { self.kv - .with_small_data(block_hash, |data| data.block_header)? + .with_small_data(block_hash.inner(), |data| data.block_header)? } - fn block_events(&self, block_hash: H256) -> Option> { + fn block_events(&self, block_hash: HashOf) -> Option> { self.kv - .get(&Key::BlockEvents(block_hash).to_bytes()) + .get(&Key::BlockEvents(block_hash.inner()).to_bytes()) .map(|data| { Vec::::decode(&mut data.as_slice()) .expect("Failed to decode data into `Vec`") @@ -478,9 +485,9 @@ impl OnChainStorageRO for RawDatabase { }) } - fn block_synced(&self, block_hash: H256) -> bool { + fn block_synced(&self, block_hash: HashOf) -> bool { self.kv - .with_small_data(block_hash, |data| data.block_is_synced) + .with_small_data(block_hash.inner(), |data| data.block_is_synced) .unwrap_or_default() } @@ -495,16 +502,18 @@ impl OnChainStorageRO for RawDatabase { } impl OnChainStorageRW for RawDatabase { - fn set_block_header(&self, block_hash: H256, header: BlockHeader) { + fn set_block_header(&self, block_hash: HashOf, header: BlockHeader) { tracing::trace!("Set block header for {block_hash}"); self.kv - .mutate_small_data(block_hash, |data| data.block_header = Some(header)); + .mutate_small_data(block_hash.inner(), |data| data.block_header = Some(header)); } - fn set_block_events(&self, block_hash: H256, events: &[BlockEvent]) { + fn set_block_events(&self, block_hash: HashOf, events: &[BlockEvent]) { tracing::trace!("Set block events for {block_hash}"); - self.kv - .put(&Key::BlockEvents(block_hash).to_bytes(), events.encode()); + self.kv.put( + &Key::BlockEvents(block_hash.inner()).to_bytes(), + events.encode(), + ); } fn set_code_blob_info(&self, code_id: CodeId, code_info: CodeBlobInfo) { @@ -513,9 +522,9 @@ impl OnChainStorageRW for RawDatabase { .put(&Key::CodeUploadInfo(code_id).to_bytes(), code_info.encode()); } - fn set_block_synced(&self, block_hash: H256) { + fn set_block_synced(&self, block_hash: HashOf) { tracing::trace!("For block {block_hash} set synced"); - self.kv.mutate_small_data(block_hash, |data| { + self.kv.mutate_small_data(block_hash.inner(), |data| { data.block_is_synced = true; }); } @@ -529,17 +538,17 @@ impl OnChainStorageRW for RawDatabase { } impl BlockMetaStorageRO for RawDatabase { - fn block_meta(&self, block_hash: H256) -> BlockMeta { + fn block_meta(&self, block_hash: HashOf) -> BlockMeta { self.kv - .with_small_data(block_hash, |data| data.meta) + .with_small_data(block_hash.inner(), |data| data.meta) .unwrap_or_default() } } impl BlockMetaStorageRW for RawDatabase { - fn mutate_block_meta(&self, block_hash: H256, f: impl FnOnce(&mut BlockMeta)) { + fn mutate_block_meta(&self, block_hash: HashOf, f: impl FnOnce(&mut BlockMeta)) { tracing::trace!("For block {block_hash} mutate meta"); - self.kv.mutate_small_data(block_hash, |data| { + self.kv.mutate_small_data(block_hash.inner(), |data| { f(&mut data.meta); }); } @@ -774,16 +783,16 @@ impl Database { election: 0, slot: 1.try_into().unwrap(), }, - genesis_block_hash: H256::zero(), + genesis_block_hash: HashOf::zero(), max_validators: 10, }; let globals = DBGlobals { - start_block_hash: H256::zero(), + start_block_hash: HashOf::zero(), latest_synced_eb: SimpleBlockData::default(), - latest_prepared_eb_hash: H256::zero(), - latest_finalized_mb_hash: H256::zero(), - latest_computed_mb_hash: H256::zero(), + latest_prepared_eb_hash: HashOf::zero(), + latest_finalized_mb_hash: HashOf::zero(), + latest_computed_mb_hash: HashOf::zero(), }; ::set_config(&mem_db, config); @@ -826,7 +835,7 @@ pub(crate) struct BlockSmallData { impl BlockMetaStorageRO for Database { delegate::delegate! { to self.raw { - fn block_meta(&self, block_hash: H256) -> BlockMeta; + fn block_meta(&self, block_hash: HashOf) -> BlockMeta; } } } @@ -834,7 +843,7 @@ impl BlockMetaStorageRO for Database { impl BlockMetaStorageRW for Database { delegate::delegate! { to self.raw { - fn mutate_block_meta(&self, block_hash: H256, f: impl FnOnce(&mut BlockMeta)); + fn mutate_block_meta(&self, block_hash: HashOf, f: impl FnOnce(&mut BlockMeta)); } } } @@ -872,10 +881,10 @@ impl Storage for Database { impl OnChainStorageRO for Database { delegate::delegate! { to self.raw { - fn block_header(&self, block_hash: H256) -> Option; - fn block_events(&self, block_hash: H256) -> Option>; + fn block_header(&self, block_hash: HashOf) -> Option; + fn block_events(&self, block_hash: HashOf) -> Option>; fn code_blob_info(&self, code_id: CodeId) -> Option; - fn block_synced(&self, block_hash: H256) -> bool; + fn block_synced(&self, block_hash: HashOf) -> bool; fn validators(&self, era_index: u64) -> Option; } } @@ -884,10 +893,10 @@ impl OnChainStorageRO for Database { impl OnChainStorageRW for Database { delegate::delegate! { to self.raw { - fn set_block_header(&self, block_hash: H256, header: BlockHeader); - fn set_block_events(&self, block_hash: H256, events: &[BlockEvent]); + fn set_block_header(&self, block_hash: HashOf, header: BlockHeader); + fn set_block_events(&self, block_hash: HashOf, events: &[BlockEvent]); fn set_code_blob_info(&self, code_id: CodeId, code_info: CodeBlobInfo); - fn set_block_synced(&self, block_hash: H256); + fn set_block_synced(&self, block_hash: HashOf); fn set_validators(&self, era_index: u64, validator_set: ValidatorsVec); } } @@ -903,23 +912,23 @@ impl InjectedStorageRO for Database { impl MbStorageRO for Database { delegate!(to self.raw { - fn mb_compact_block(&self, mb_hash: H256) -> Option; + fn mb_compact_block(&self, mb_hash: HashOf) -> Option; fn operations(&self, operations_hash: H256) -> Option; - fn mb_program_states(&self, mb_hash: H256) -> Option; - fn mb_outcome(&self, mb_hash: H256) -> Option>; - fn mb_schedule(&self, mb_hash: H256) -> Option; - fn mb_meta(&self, mb_hash: H256) -> MbMeta; + fn mb_program_states(&self, mb_hash: HashOf) -> Option; + fn mb_outcome(&self, mb_hash: HashOf) -> Option>; + fn mb_schedule(&self, mb_hash: HashOf) -> Option; + fn mb_meta(&self, mb_hash: HashOf) -> MbMeta; }); } impl MbStorageRW for Database { delegate!(to self.raw { - fn set_mb_compact_block(&self, mb_hash: H256, compact: CompactMb); + fn set_mb_compact_block(&self, mb_hash: HashOf, compact: CompactMb); fn set_operations(&self, operations: Operations) -> H256; - fn set_mb_program_states(&self, mb_hash: H256, program_states: ProgramStates); - fn set_mb_outcome(&self, mb_hash: H256, outcome: Vec); - fn set_mb_schedule(&self, mb_hash: H256, schedule: Schedule); - fn mutate_mb_meta(&self, mb_hash: H256, f: impl FnOnce(&mut MbMeta)); + fn set_mb_program_states(&self, mb_hash: HashOf, program_states: ProgramStates); + fn set_mb_outcome(&self, mb_hash: HashOf, outcome: Vec); + fn set_mb_schedule(&self, mb_hash: HashOf, schedule: Schedule); + fn mutate_mb_meta(&self, mb_hash: HashOf, f: impl FnOnce(&mut MbMeta)); }); } @@ -1031,7 +1040,7 @@ mod tests { destination: ActorId::zero(), payload: LimitedVec::new(), value: 0, - reference_block: H256::random(), + reference_block: unsafe { HashOf::::new(H256::random()) }, salt: LimitedVec::new(), }, ) @@ -1045,7 +1054,7 @@ mod tests { fn test_block_events() { let db = Database::memory(); - let block_hash = H256::random(); + let block_hash = unsafe { HashOf::::new(H256::random()) }; let events = vec![BlockEvent::Router(RouterEvent::StorageSlotChanged( StorageSlotChangedEvent { slot: H256::random(), @@ -1069,7 +1078,7 @@ mod tests { fn test_block_is_synced() { let db = Database::memory(); - let block_hash = H256::random(); + let block_hash = unsafe { HashOf::::new(H256::random()) }; assert!(!db.block_synced(block_hash)); db.set_block_synced(block_hash); assert!(db.block_synced(block_hash)); @@ -1166,7 +1175,7 @@ mod tests { fn test_block_header() { let db = Database::memory(); - let block_hash = H256::random(); + let block_hash = unsafe { HashOf::::new(H256::random()) }; let block_header = BlockHeader::default(); db.set_block_header(block_hash, block_header); assert_eq!(db.block_header(block_hash), Some(block_header)); diff --git a/ethexe/db/src/dump/collect.rs b/ethexe/db/src/dump/collect.rs index 40b74db05b3..c0d9bda4314 100644 --- a/ethexe/db/src/dump/collect.rs +++ b/ethexe/db/src/dump/collect.rs @@ -6,8 +6,9 @@ use super::StateDump; use anyhow::{Context, Result}; use ethexe_common::{ - HashOf, MaybeHashOf, StateHashWithQueueSize, + EB, HashOf, MaybeHashOf, StateHashWithQueueSize, db::{BlockMetaStorageRO, CodesStorageRO, HashStorageRO, MbStorageRO}, + malachite::MB, }; use ethexe_runtime_common::state::{ Dispatch, DispatchStash, Expiring, Mailbox, MailboxMessage, MemoryPages, MemoryPagesInner, @@ -304,8 +305,8 @@ impl StateDump { /// a convenience that derives the MB from `BlockMeta::last_committed_mb`. pub fn collect_from_mb_storage( storage: &(impl MbStorageRO + CodesStorageRO + HashStorageRO), - mb_hash: H256, - eb_hash: H256, + mb_hash: HashOf, + eb_hash: HashOf, ) -> Result { let mut collector = BlobCollector { storage, @@ -348,8 +349,8 @@ impl StateDump { } Ok(StateDump { - metadata: mb_hash, - eb_hash, + metadata: mb_hash.inner(), + eb_hash: eb_hash.inner(), codes, programs, blobs: collector.blobs, @@ -361,7 +362,7 @@ impl StateDump { /// [`Self::collect_from_mb_storage`]. pub fn collect_from_storage( storage: &(impl MbStorageRO + CodesStorageRO + BlockMetaStorageRO + HashStorageRO), - eb_hash: H256, + eb_hash: HashOf, ) -> Result { let block_meta = storage.block_meta(eb_hash); diff --git a/ethexe/db/src/iterator.rs b/ethexe/db/src/iterator.rs index 4bddfc41874..d9dd2da0584 100644 --- a/ethexe/db/src/iterator.rs +++ b/ethexe/db/src/iterator.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 use ethexe_common::{ - BlockHeader, HashOf, MaybeHashOf, ProgramStates, Schedule, ScheduledTask, + BlockHeader, EB, HashOf, MaybeHashOf, ProgramStates, Schedule, ScheduledTask, StateHashWithQueueSize, db::{ BlockMeta, BlockMetaStorageRO, CodesStorageRO, CompactMb, MbMeta, MbStorageRO, @@ -10,6 +10,7 @@ use ethexe_common::{ }, events::BlockEvent, gear::StateTransition, + malachite::MB, }; use ethexe_runtime_common::state::{ ActiveProgram, Allocations, DispatchStash, Expiring, Mailbox, MemoryPages, MemoryPagesRegion, @@ -112,55 +113,55 @@ node! { Chain( #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct ChainNode { - pub head: H256, - pub bottom: H256, + pub head: HashOf, + pub bottom: HashOf, } ), Block( #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct BlockNode { - pub block: H256, + pub block: HashOf, } ), BlockMeta( #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct BlockMetaNode { - pub block: H256, + pub block: HashOf, pub meta: BlockMeta, } ), BlockHeader( #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct BlockHeaderNode { - pub block: H256, + pub block: HashOf, pub block_header: BlockHeader, } ), BlockEvents( #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct BlockEventsNode { - pub block: H256, + pub block: HashOf, pub block_events: Vec, } ), BlockSynced( #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct BlockSyncedNode { - pub block: H256, + pub block: HashOf, pub block_synced: bool, } ), Mb( #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct MbNode { - pub mb_hash: H256, + pub mb_hash: HashOf, pub mb: CompactMb, } ), MbMeta( #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct MbMetaNode { - pub mb_hash: H256, + pub mb_hash: HashOf, pub mb_meta: MbMeta, } ), @@ -206,7 +207,7 @@ node! { MbProgramStates( #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct MbProgramStatesNode { - pub mb_hash: H256, + pub mb_hash: HashOf, pub mb_program_states: ProgramStates, } ), @@ -219,14 +220,14 @@ node! { MbSchedule( #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct MbScheduleNode { - pub mb_hash: H256, + pub mb_hash: HashOf, pub mb_schedule: Schedule, } ), MbScheduleTasks( #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct MbScheduleTasksNode { - pub mb_hash: H256, + pub mb_hash: HashOf, pub height: u32, pub tasks: BTreeSet, } @@ -240,7 +241,7 @@ node! { MbOutcome( #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct MbOutcomeNode { - pub mb_hash: H256, + pub mb_hash: HashOf, pub mb_outcome: Vec, } ), @@ -343,15 +344,15 @@ impl From for Node { #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, derive_more::IsVariant)] pub enum DatabaseIteratorError { /* block */ - NoBlockHeader(H256), - NoBlockEvents(H256), - NoBlockCodesQueue(H256), + NoBlockHeader(HashOf), + NoBlockEvents(HashOf), + NoBlockCodesQueue(HashOf), /* MB */ - NoMb(H256), - NoMbProgramStates(H256), - NoMbSchedule(H256), - NoMbOutcome(H256), + NoMb(HashOf), + NoMbProgramStates(HashOf), + NoMbSchedule(HashOf), + NoMbOutcome(HashOf), /* memory */ NoMemoryPages(HashOf), @@ -514,10 +515,10 @@ where .. } = meta; - // `H256::zero()` is the genesis sentinel: no MB has been + // Zero hash is the genesis sentinel: no MB has been // committed on-chain yet, so there is nothing to walk. if let Some(mb_hash) = *last_committed_mb - && mb_hash != H256::zero() + && !mb_hash.is_zero() { if let Some(mb) = self.storage.mb_compact_block(mb_hash) { self.push_node(MbNode { mb_hash, mb }); @@ -877,8 +878,8 @@ pub(crate) mod tests { #[test] fn walk_chain_basic() { - let head = H256::from_low_u64_be(1); - let bottom = H256::from_low_u64_be(2); + let head = unsafe { HashOf::::new(H256::from_low_u64_be(1)) }; + let bottom = unsafe { HashOf::::new(H256::from_low_u64_be(2)) }; // This will fail because we don't have the block header in the database assert!( @@ -890,7 +891,7 @@ pub(crate) mod tests { #[test] fn walk_block_with_missing_data() { - let block = H256::from_low_u64_be(42); + let block = unsafe { HashOf::::new(H256::from_low_u64_be(42)) }; let errors: Vec<_> = DatabaseIterator::new(setup_db(), BlockNode { block }) .filter_map(Node::into_error) @@ -1036,7 +1037,7 @@ pub(crate) mod tests { use ethexe_common::StateHashWithQueueSize; use std::collections::BTreeMap; - let mb_hash = H256::random(); + let mb_hash = unsafe { HashOf::::new(H256::random()) }; let program_id = ActorId::from([3u8; 32]); let state_hash = H256::random(); @@ -1067,7 +1068,7 @@ pub(crate) mod tests { fn walk_mb_schedule_tasks() { use gear_core::ids::MessageId; - let mb_hash = H256::random(); + let mb_hash = unsafe { HashOf::::new(H256::random()) }; let program_id = ActorId::from([10u8; 32]); let mut tasks = BTreeSet::new(); @@ -1098,7 +1099,7 @@ pub(crate) mod tests { use gear_core::ids::MessageId; use std::collections::BTreeMap; - let mb_hash = H256::random(); + let mb_hash = unsafe { HashOf::::new(H256::random()) }; let program_id = ActorId::from([14u8; 32]); let mut mb_schedule = BTreeMap::new(); @@ -1122,7 +1123,7 @@ pub(crate) mod tests { #[test] fn walk_mb_outcome() { - let mb_hash = H256::random(); + let mb_hash = unsafe { HashOf::::new(H256::random()) }; let actor_id = ActorId::from([15u8; 32]); let new_state_hash = H256::random(); diff --git a/ethexe/db/src/migrations/init.rs b/ethexe/db/src/migrations/init.rs index b3466b6b0ae..4ae59e0583b 100644 --- a/ethexe/db/src/migrations/init.rs +++ b/ethexe/db/src/migrations/init.rs @@ -8,16 +8,16 @@ use crate::{Database, RawDatabase, dump::StateDump, migrations::GenesisInitializ use alloy::providers::{Provider as _, RootProvider}; use anyhow::{Context as _, Result, ensure}; use ethexe_common::{ - BlockHeader, ProgramStates, ProtocolTimelines, Schedule, SimpleBlockData, + BlockHeader, EB, HashOf, ProgramStates, ProtocolTimelines, Schedule, SimpleBlockData, StateHashWithQueueSize, db::{CodesStorageRO, CodesStorageRW, CompactMb, MbStorageRW, PreparedBlockData}, gear::{GenesisBlockInfo, Timelines}, - malachite::Operations, + malachite::{MB, Operations}, }; use ethexe_ethereum::router::RouterQuery; use ethexe_runtime_common::{RUNTIME_ID, ScheduleRestorer, state::Storage}; use futures::{TryStreamExt, stream::FuturesUnordered}; -use gprimitives::{CodeId, H256}; +use gprimitives::CodeId; pub async fn initialize_db(config: InitConfig, db: RawDatabase) -> Result { log::info!("Initializing database to version {LATEST_VERSION}..."); @@ -83,10 +83,11 @@ pub async fn initialize_empty_db(config: InitConfig, db: &RawDatabase) -> Result let genesis: GenesisBlockInfo = storage_view.genesisBlock.into(); let genesis_eb = SimpleBlockData { - hash: genesis.hash, + // SAFETY: genesis EB hash sourced from the Router contract. + hash: unsafe { HashOf::::new(genesis.hash) }, header: BlockHeader { // genesis block header is not important in any way for ethexe - parent_hash: H256::zero(), + parent_hash: HashOf::::zero(), height: genesis.number, timestamp: genesis.timestamp, }, @@ -110,14 +111,15 @@ pub async fn initialize_empty_db(config: InitConfig, db: &RawDatabase) -> Result // the zero ancestor that the service's height-1 block points to. // `last_advanced_eb` is zero (pre-genesis: nothing advanced yet), matching // the compute anchor fallback and the malachite-service zero handling. - let genesis_parent_mb_hash = H256::zero(); + let genesis_parent_mb_hash = HashOf::::zero(); let operations_hash = db.set_operations(Operations::default()); db.set_mb_compact_block( genesis_parent_mb_hash, CompactMb { - parent: H256::zero(), + parent: HashOf::::zero(), height: 0, operations_hash, + reserved: [0u8; 64], }, ); db.set_mb_program_states(genesis_parent_mb_hash, program_states); @@ -125,7 +127,7 @@ pub async fn initialize_empty_db(config: InitConfig, db: &RawDatabase) -> Result db.set_mb_outcome(genesis_parent_mb_hash, Vec::new()); db.mutate_mb_meta(genesis_parent_mb_hash, |m| { m.computed = true; - m.last_advanced_eb = H256::zero(); + m.last_advanced_eb = HashOf::::zero(); }); ethexe_common::setup_block_in_db( @@ -136,8 +138,8 @@ pub async fn initialize_empty_db(config: InitConfig, db: &RawDatabase) -> Result events: Default::default(), codes_queue: Default::default(), last_committed_batch: Default::default(), - last_committed_mb: H256::zero(), - last_committed_eb: H256::zero(), + last_committed_mb: HashOf::::zero(), + last_committed_eb: HashOf::::zero(), latest_era_with_committed_validators: 0, }, ); @@ -160,7 +162,8 @@ pub async fn initialize_empty_db(config: InitConfig, db: &RawDatabase) -> Result .try_into() .context("slot duration must be non-zero")?, }, - genesis_block_hash: genesis.hash, + // SAFETY: genesis EB hash sourced from the Router contract. + genesis_block_hash: unsafe { HashOf::::new(genesis.hash) }, max_validators: storage_view.maxValidators, }; @@ -196,7 +199,7 @@ async fn genesis_data_initialization( blobs, } = initializer.get_genesis_data()?; - if eb_hash != genesis_eb.hash { + if eb_hash != genesis_eb.hash.inner() { log::warn!( "Genesis data block hash {eb_hash} does not match the actual genesis block hash {}", genesis_eb.hash diff --git a/ethexe/db/src/verifier.rs b/ethexe/db/src/verifier.rs index 6e92bf2c19a..a8985ca928d 100644 --- a/ethexe/db/src/verifier.rs +++ b/ethexe/db/src/verifier.rs @@ -7,8 +7,9 @@ use crate::{ visitor::{DatabaseVisitor, walk}, }; use ethexe_common::{ - BlockHeader, HashOf, ScheduledTask, + BlockHeader, EB, HashOf, ScheduledTask, db::{BlockMeta, MbStorageRO}, + malachite::MB, }; use ethexe_runtime_common::state::{MessageQueue, MessageQueueHashWithSize}; use gear_core::code::CodeMetadata; @@ -24,15 +25,15 @@ pub enum IntegrityVerifierError { DatabaseIterator(DatabaseIteratorError), /* block */ - BlockIsNotSynced(H256), - BlockIsNotPrepared(H256), - NoBlockLastCommittedBatch(H256), - NoBlockLastCommittedMb(H256), - NoBlockLatestEraValidatorsCommitted(H256), - NoBlockHeader(H256), + BlockIsNotSynced(HashOf), + BlockIsNotPrepared(HashOf), + NoBlockLastCommittedBatch(HashOf), + NoBlockLastCommittedMb(HashOf), + NoBlockLatestEraValidatorsCommitted(HashOf), + NoBlockHeader(HashOf), /* block header */ - NoParentBlockHeader(H256), + NoParentBlockHeader(HashOf), InvalidBlockParentHeight { parent_height: u32, height: u32, @@ -51,9 +52,9 @@ pub enum IntegrityVerifierError { }, /* rest */ - MbNotFound(H256), + MbNotFound(HashOf), MbScheduleHasExpiredTasks { - mb_hash: H256, + mb_hash: HashOf, expiry: u32, tasks: usize, }, @@ -69,7 +70,7 @@ pub struct IntegrityVerifier { errors: Vec, cached_queue_sizes: HashMap, original_code: Option>, - bottom: Option, + bottom: Option>, } impl IntegrityVerifier { @@ -85,8 +86,8 @@ impl IntegrityVerifier { pub fn verify_chain( mut self, - head: H256, - bottom: H256, + head: HashOf, + bottom: HashOf, ) -> Result<(), Vec> { self.bottom = Some(bottom); walk(&mut self, ChainNode { head, bottom }); @@ -131,7 +132,7 @@ impl DatabaseVisitor for IntegrityVerifier { } #[tracing::instrument(level = "trace", skip(self))] - fn visit_block_meta(&mut self, block: H256, meta: BlockMeta) { + fn visit_block_meta(&mut self, block: HashOf, meta: BlockMeta) { if !meta.prepared { self.errors .push(IntegrityVerifierError::BlockIsNotPrepared(block)); @@ -153,7 +154,7 @@ impl DatabaseVisitor for IntegrityVerifier { } #[tracing::instrument(level = "trace", skip(self))] - fn visit_block_synced(&mut self, block: H256, block_synced: bool) { + fn visit_block_synced(&mut self, block: HashOf, block_synced: bool) { if !block_synced { self.errors .push(IntegrityVerifierError::BlockIsNotSynced(block)); @@ -161,7 +162,7 @@ impl DatabaseVisitor for IntegrityVerifier { } #[tracing::instrument(level = "trace", skip(self))] - fn visit_block_header(&mut self, block: H256, header: BlockHeader) { + fn visit_block_header(&mut self, block: HashOf, header: BlockHeader) { let Some(parent_header) = self.db().block_header(header.parent_hash) else { if self.bottom == Some(block) { // it's not guaranteed bottom parent block has header @@ -222,7 +223,7 @@ impl DatabaseVisitor for IntegrityVerifier { #[tracing::instrument(level = "trace", skip(self))] fn visit_mb_schedule_tasks( &mut self, - mb_hash: H256, + mb_hash: HashOf, height: u32, tasks: BTreeSet, ) { @@ -292,7 +293,7 @@ mod tests { #[test] fn test_block_meta_not_synced_error() { let db = setup_db(); - let block = H256::random(); + let block = unsafe { HashOf::::new(H256::random()) }; // Insert block with not synced meta db.mutate_block_meta(block, |meta| { @@ -311,7 +312,7 @@ mod tests { #[test] fn test_block_meta_not_prepared_error() { let db = setup_db(); - let block = H256::random(); + let block = unsafe { HashOf::::new(H256::random()) }; // Insert block with not prepared meta db.mutate_block_meta(block, |meta| { @@ -330,8 +331,8 @@ mod tests { #[test] fn test_no_parent_block_header_error() { let db = setup_db(); - let block = H256::random(); - let parent_hash = H256::random(); + let block = unsafe { HashOf::::new(H256::random()) }; + let parent_hash = unsafe { HashOf::::new(H256::random()) }; // Insert valid meta but header with non-existent parent db.mutate_block_meta(block, |meta| { @@ -357,15 +358,15 @@ mod tests { #[test] fn test_invalid_block_parent_height_error() { let db = setup_db(); - let block = H256::random(); - let parent_hash = H256::random(); + let block = unsafe { HashOf::::new(H256::random()) }; + let parent_hash = unsafe { HashOf::::new(H256::random()) }; // Setup parent block db.mutate_block_meta(parent_hash, |meta| { meta.prepared = true; }); - let parent_hash1 = H256::zero(); + let parent_hash1 = HashOf::::zero(); let parent_header = BlockHeader { height: 5, parent_hash: parent_hash1, @@ -400,15 +401,15 @@ mod tests { #[test] fn test_invalid_parent_timestamp_error() { let db = setup_db(); - let block = H256::random(); - let parent_hash = H256::random(); + let block = unsafe { HashOf::::new(H256::random()) }; + let parent_hash = unsafe { HashOf::::new(H256::random()) }; // Setup parent block db.mutate_block_meta(parent_hash, |meta| { meta.prepared = true; }); - let parent_hash1 = H256::zero(); + let parent_hash1 = HashOf::::zero(); let parent_header = BlockHeader { height: 5, parent_hash: parent_hash1, @@ -503,15 +504,16 @@ mod tests { use ethexe_common::db::{CompactMb, MbStorageRW}; let db = setup_db(); - let mb_hash = H256::random(); + let mb_hash = unsafe { HashOf::::new(H256::random()) }; // MB at height 100; a task scheduled for height 50 is expired. db.set_mb_compact_block( mb_hash, CompactMb { - parent: H256::zero(), + parent: HashOf::::zero(), height: 100, operations_hash: H256::zero(), + reserved: [0u8; 64], }, ); @@ -612,7 +614,7 @@ mod tests { #[test] fn test_multiple_errors_collected() { let db = setup_db(); - let block_hash = H256::random(); + let block_hash = unsafe { HashOf::::new(H256::random()) }; // Insert block with multiple issues db.mutate_block_meta(block_hash, |meta| { @@ -629,8 +631,8 @@ mod tests { #[test] fn test_successful_verification_with_valid_data() { let db = setup_db(); - let block_hash = H256::random(); - let parent_hash = H256::zero(); + let block_hash = unsafe { HashOf::::new(H256::random()) }; + let parent_hash = HashOf::::zero(); let block_header = BlockHeader { height: 100, parent_hash, @@ -642,7 +644,7 @@ mod tests { db.mutate_block_meta(block_hash, |meta| { meta.prepared = true; meta.last_committed_batch = Some(Digest::random()); - meta.last_committed_mb = Some(H256::zero()); + meta.last_committed_mb = Some(HashOf::::zero()); meta.codes_queue = Some(Default::default()); meta.latest_era_validators_committed = Some(10); }); @@ -658,7 +660,7 @@ mod tests { let verifier = IntegrityVerifier::new(db); // This should trigger DatabaseVisitorError due to missing block - let non_existent_block = H256::random(); + let non_existent_block = unsafe { HashOf::::new(H256::random()) }; let errors = verifier .verify_chain(non_existent_block, non_existent_block) .unwrap_err(); diff --git a/ethexe/db/src/visitor.rs b/ethexe/db/src/visitor.rs index de6cb2bfd5b..8ba795837a9 100644 --- a/ethexe/db/src/visitor.rs +++ b/ethexe/db/src/visitor.rs @@ -3,10 +3,11 @@ use crate::iterator::{DatabaseIterator, DatabaseIteratorError, DatabaseIteratorStorage, Node}; use ethexe_common::{ - BlockHeader, ProgramStates, Schedule, ScheduledTask, + BlockHeader, EB, HashOf, ProgramStates, Schedule, ScheduledTask, db::{BlockMeta, CompactMb, MbMeta}, events::BlockEvent, gear::StateTransition, + malachite::MB, }; use ethexe_runtime_common::state::{ Allocations, DispatchStash, Mailbox, MemoryPages, MemoryPagesRegion, MessageQueue, @@ -17,7 +18,7 @@ use gear_core::{ code::{CodeMetadata, InstrumentedCode}, memory::PageBuf, }; -use gprimitives::{ActorId, CodeId, H256}; +use gprimitives::{ActorId, CodeId}; use std::collections::BTreeSet; macro_rules! define_visitor { diff --git a/ethexe/ethereum/src/abi/gear.rs b/ethexe/ethereum/src/abi/gear.rs index 2fb76a90bc4..9f2f3bd7f46 100644 --- a/ethexe/ethereum/src/abi/gear.rs +++ b/ethexe/ethereum/src/abi/gear.rs @@ -24,7 +24,7 @@ impl From for Gear::ChainCommitment { Self { transitions: value.transitions.into_iter().map(Into::into).collect(), head: value.head.0.into(), - lastAdvancedEthBlock: value.last_advanced_eth_block.0.into(), + lastAdvancedEthBlock: value.last_advanced_eth_block.inner().0.into(), } } } @@ -93,7 +93,7 @@ impl From for Gear::RewardsCommitment { impl From for Gear::BatchCommitment { fn from(value: BatchCommitment) -> Self { Self { - blockHash: value.block_hash.0.into(), + blockHash: value.block_hash.inner().0.into(), blockTimestamp: u64_to_uint48_lossy(value.timestamp), previousCommittedBatchHash: value.previous_batch.0.into(), expiry: value.expiry, diff --git a/ethexe/ethereum/src/lib.rs b/ethexe/ethereum/src/lib.rs index cece30db34d..2b97277b92b 100644 --- a/ethexe/ethereum/src/lib.rs +++ b/ethexe/ethereum/src/lib.rs @@ -94,7 +94,7 @@ use alloy::{ }; use anyhow::{Context, Result, anyhow}; use async_trait::async_trait; -use ethexe_common::{BlockHeader, Digest, SimpleBlockData, ecdsa::PublicKey}; +use ethexe_common::{BlockHeader, Digest, EB, HashOf, SimpleBlockData, ecdsa::PublicKey}; use gprimitives::{ActorId, H256, MessageId}; use gsigner::secp256k1::{Address, Secp256k1SignerExt, Signer}; use middleware::Middleware; @@ -405,9 +405,11 @@ impl Ethereum { .number() .try_into() .with_context(|| "block number overflow")?; - let hash = block_resp.hash().0.into(); + // SAFETY: real Ethereum block hash from the chain — carried verbatim. + let hash = unsafe { HashOf::::new(block_resp.hash().0.into()) }; let header = block_resp.into_header(); - let parent_hash = header.parent_hash.0.into(); + // SAFETY: real Ethereum parent block hash from the chain — carried verbatim. + let parent_hash = unsafe { HashOf::::new(header.parent_hash.0.into()) }; let timestamp = header.timestamp; let header = BlockHeader { height, @@ -720,6 +722,12 @@ impl IntoBlockId for H256 { } } +impl IntoBlockId for HashOf { + fn into_block_id(self) -> BlockId { + BlockId::hash(self.inner().0.into()) + } +} + impl IntoBlockId for u32 { fn into_block_id(self) -> BlockId { BlockId::number(self.into()) diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 5464d65a967..67af8f32d08 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -45,10 +45,10 @@ use crate::{ use anyhow::{Result, anyhow}; use async_trait::async_trait; use ethexe_common::{ - MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, + EB, HashOf, MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, db::{CompactMb, GlobalsStorageRO, GlobalsStorageRW, MbStorageRO, MbStorageRW}, injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, SignedInjectedTransaction}, - malachite::{Operation, Operations}, + malachite::{MB, Operation, Operations}, }; use ethexe_db::Database; use ethexe_malachite_core::{Block, BlockPayload, Externalities, MAX_BLOCK_PAYLOAD_BYTES}; @@ -108,9 +108,9 @@ pub(crate) struct PendingEvent { pub event: MalachiteEvent, /// Eth-block hash whose `block_events` entry must be present /// before this event can fire — i.e. the MB's - /// `last_advanced_eb`. `H256::zero()` skips the gate (genesis + /// `last_advanced_eb`. Zero skips the gate (genesis /// or an MB that never advanced past the pre-genesis sentinel). - pub prerequisite: H256, + pub prerequisite: HashOf, } #[async_trait] @@ -127,13 +127,16 @@ impl Externalities for EthexeExternalities { let payload = Operations::decode_all(&mut mb.payload.as_ref()) .map_err(|e| anyhow!("decoding Operations from block payload bytes: {e}"))?; - let parent = mb.parent_hash; + // SAFETY: malachite-core hands us the freshly computed envelope hash. + let mb_hash = unsafe { HashOf::::new(mb_hash) }; + // SAFETY: parent_hash here is the parent MB envelope hash from malachite-core. + let parent = unsafe { HashOf::::new(mb.parent_hash) }; // Propagate `last_advanced_eb` forward — the latest // `AdvanceTillEthereumBlock` in this MB wins; otherwise we // inherit the parent's value (zero if pre-genesis). let parent_advanced = if parent.is_zero() { - H256::zero() + HashOf::::zero() } else { self.db.mb_meta(parent).last_advanced_eb }; @@ -156,6 +159,7 @@ impl Externalities for EthexeExternalities { parent, height: mb.height, operations_hash, + reserved: [0u8; 64], }, ); self.db.mutate_mb_meta(mb_hash, |meta| { @@ -177,6 +181,8 @@ impl Externalities for EthexeExternalities { mb_hash: H256, cert: ethexe_malachite_core::CommitCertificate, ) -> Result<()> { + // SAFETY: malachite-core hands us the BFT-finalized envelope hash. + let mb_hash = unsafe { HashOf::::new(mb_hash) }; let compact = self.db.mb_compact_block(mb_hash).ok_or_else(|| { anyhow!( "process_mb_finalized: no CompactMb for {mb_hash} \ @@ -234,8 +240,10 @@ impl Externalities for EthexeExternalities { // `parent_hash` is the consensus envelope hash of the parent // (zero for genesis). Use it directly to seed the producer's // `last_advanced_eb` lookup. + // SAFETY: malachite-core provides the parent MB envelope hash. + let parent_mb_hash = unsafe { HashOf::::new(parent_mb_hash) }; let parent_advanced = if parent_mb_hash.is_zero() { - H256::zero() + HashOf::::zero() } else { self.db.mb_meta(parent_mb_hash).last_advanced_eb }; @@ -371,6 +379,8 @@ impl Externalities for EthexeExternalities { } async fn validate_block_above(&self, parent_hash: H256, payload: BlockPayload) -> Result { + // SAFETY: malachite-core hands us the proposal's parent envelope hash. + let parent_hash = unsafe { HashOf::::new(parent_hash) }; // Validation only ever runs on a fresh proposal (never on the sync // path), so it enforces the operations *this* build accepts. Decode // rejects any operation whose discriminant this build doesn't know — @@ -395,7 +405,7 @@ impl Externalities for EthexeExternalities { let mut iter = payload.iter(); let mut next = iter.next(); - let advance: Option = + let advance: Option> = if let Some(Operation::AdvanceTillEthereumBlock { block_hash }) = next { let h = *block_hash; next = iter.next(); @@ -454,7 +464,7 @@ impl Externalities for EthexeExternalities { // `post_quarantine_delay` from observability rather than logs. if let Some(advance) = advance { let parent_advanced = if parent_hash.is_zero() { - H256::zero() + HashOf::::zero() } else { self.db.mb_meta(parent_hash).last_advanced_eb }; @@ -577,7 +587,7 @@ impl Externalities for EthexeExternalities { // hard cap on the encoded `Block` payload — anything larger // never reaches `validate_block_above` in the first place. let parent_advanced = if parent_hash.is_zero() { - H256::zero() + HashOf::::zero() } else { self.db.mb_meta(parent_hash).last_advanced_eb }; @@ -626,7 +636,7 @@ impl EthexeExternalities { /// code-validation pipeline and fail with `MissingCode` when an /// MB's advance chain contains a `ProgramCreated` event for a /// not-yet-validated code. - fn prerequisite_satisfied(&self, prerequisite: H256) -> bool { + fn prerequisite_satisfied(&self, prerequisite: HashOf) -> bool { use ethexe_common::db::BlockMetaStorageRO; prerequisite.is_zero() || self.db.block_meta(prerequisite).prepared } @@ -637,7 +647,7 @@ impl EthexeExternalities { /// pending buffer to keep ordering. Held entries are released /// from the front by [`Self::drain_pending_events`] once their /// prerequisite lands. - pub(crate) fn try_emit_or_queue(&self, event: MalachiteEvent, prerequisite: H256) { + pub(crate) fn try_emit_or_queue(&self, event: MalachiteEvent, prerequisite: HashOf) { let mut queue = self.pending_events.lock().expect("pending_events poisoned"); if queue.is_empty() && self.prerequisite_satisfied(prerequisite) { // Channel receiver dropped only on shutdown — best-effort. @@ -670,8 +680,8 @@ impl EthexeExternalities { // or any suitable injected tx to include in the next proposal. async fn wait_for_proposable_content( &self, - prev_advanced_eb_hash: H256, - ) -> (Option, Vec) { + prev_advanced_eb_hash: HashOf, + ) -> (Option>, Vec) { loop { let chain_head_notified = self.chain_head_notify.notified(); tokio::pin!(chain_head_notified); @@ -698,7 +708,10 @@ impl EthexeExternalities { } // Candidate EB must be anchored in the quarantine and a strict descendant of the previously advanced EB. - fn find_eb_candidate_for_advancing(&self, prev_advanced_eb_hash: H256) -> Option { + fn find_eb_candidate_for_advancing( + &self, + prev_advanced_eb_hash: HashOf, + ) -> Option> { let head = (*self.chain_head.read().expect("chain_head poisoned"))?; let start = self.db.globals().start_block_hash; // Producer-side total depth: protocol-required `canonical_quarantine` @@ -800,7 +813,7 @@ mod tests { /// hash without dragging an extraneous `AdvanceTillEthereumBlock` /// through the test (the `last_advanced_eb_propagates` case /// would otherwise see an unintended advance). - fn payload(advance: Option, salt: u8) -> Operations { + fn payload(advance: Option>, salt: u8) -> Operations { let mut txs = Vec::with_capacity(salt as usize + 3); if let Some(eth) = advance { txs.push(Operation::AdvanceTillEthereumBlock { block_hash: eth }); @@ -815,6 +828,18 @@ mod tests { Operations::new(txs) } + /// Convert a malachite-core `Block::hash()` value into our typed alias. + fn mb_h(raw: H256) -> HashOf { + // SAFETY: produced by Block::hash() — the canonical MB envelope digest. + unsafe { HashOf::::new(raw) } + } + + /// Convert an arbitrary H256 to HashOf for synthetic chain hashes. + fn eb_h(raw: H256) -> HashOf { + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + unsafe { HashOf::::new(raw) } + } + fn wrap(payload: Operations, height: u64, parent_hash: H256) -> Block { Block::new(parent_hash, height, to_payload(payload.encode())) } @@ -840,9 +865,10 @@ mod tests { let mb_hash = block.hash(); ext.process_mb_proposal(mb_hash, block).await.unwrap(); - let compact = db.mb_compact_block(mb_hash).expect("CompactMb saved"); + let typed_mb = mb_h(mb_hash); + let compact = db.mb_compact_block(typed_mb).expect("CompactMb saved"); assert_eq!(compact.height, 1); - assert_eq!(compact.parent, H256::zero()); + assert_eq!(compact.parent, HashOf::::zero()); let txs = db .operations(compact.operations_hash) .expect("operations in CAS"); @@ -854,7 +880,7 @@ mod tests { mb_hash: proposed, } => { assert_eq!(height, 1); - assert_eq!(proposed, mb_hash); + assert_eq!(proposed, typed_mb); let _ = p; } other => panic!("expected BlockProposal, got {other:?}"), @@ -881,7 +907,7 @@ mod tests { ext.process_mb_finalized(mb_hash, fake_cert(1)) .await .unwrap(); - assert_eq!(db.globals().latest_finalized_mb_hash, mb_hash); + assert_eq!(db.globals().latest_finalized_mb_hash, mb_h(mb_hash)); match rx.try_recv().expect("event").expect("ok") { MalachiteEvent::BlockFinalized { cert, @@ -889,9 +915,9 @@ mod tests { mb_hash: finalized, } => { assert_eq!(height, 1); - assert_eq!(mb_hash, finalized); + assert_eq!(mb_h(mb_hash), finalized); assert_eq!(cert.height, 1); - assert_eq!(cert.mb_hash, mb_hash); + assert_eq!(cert.mb_hash, mb_h(mb_hash)); let _ = p; } other => panic!("expected BlockFinalized, got {other:?}"), @@ -932,11 +958,15 @@ mod tests { // Pre-restart pointers must survive. let last_pre = chain.last().unwrap().0; - assert_eq!(db.globals().latest_finalized_mb_hash, last_pre); + assert_eq!(db.globals().latest_finalized_mb_hash, mb_h(last_pre)); for (i, (mb_hash, _)) in chain.iter().enumerate() { - let compact = db.mb_compact_block(*mb_hash).expect("compact"); + let compact = db.mb_compact_block(mb_h(*mb_hash)).expect("compact"); assert_eq!(compact.height, (i + 1) as u64); - let expected_parent = if i == 0 { H256::zero() } else { chain[i - 1].0 }; + let expected_parent = if i == 0 { + HashOf::::zero() + } else { + mb_h(chain[i - 1].0) + }; assert_eq!(compact.parent, expected_parent); } @@ -949,8 +979,11 @@ mod tests { ext_b.process_mb_proposal(mb4, block4).await.unwrap(); let _ = rx_b.recv().await; // proposal ext_b.process_mb_finalized(mb4, fake_cert(4)).await.unwrap(); - assert_eq!(db.mb_compact_block(mb4).unwrap().parent, last_pre); - assert_eq!(db.globals().latest_finalized_mb_hash, mb4); + assert_eq!( + db.mb_compact_block(mb_h(mb4)).unwrap().parent, + mb_h(last_pre) + ); + assert_eq!(db.globals().latest_finalized_mb_hash, mb_h(mb4)); } /// `last_advanced_eb` is propagated forward: an MB without an @@ -966,7 +999,7 @@ mod tests { let mut parent = H256::zero(); let payloads = [ payload(None, 1), - payload(Some(H256::repeat_byte(0xAB)), 2), + payload(Some(eb_h(H256::repeat_byte(0xAB))), 2), payload(None, 3), ]; for (i, p) in payloads.iter().enumerate() { @@ -982,14 +1015,14 @@ mod tests { } while rx.try_recv().is_ok() {} - assert!(db.mb_meta(chain[0]).last_advanced_eb.is_zero()); + assert!(db.mb_meta(mb_h(chain[0])).last_advanced_eb.is_zero()); assert_eq!( - db.mb_meta(chain[1]).last_advanced_eb, + db.mb_meta(mb_h(chain[1])).last_advanced_eb.inner(), H256::repeat_byte(0xAB), "h2 should anchor to its own AdvanceTillEthereumBlock" ); assert_eq!( - db.mb_meta(chain[2]).last_advanced_eb, + db.mb_meta(mb_h(chain[2])).last_advanced_eb.inner(), H256::repeat_byte(0xAB), "h3 inherits h2's anchor" ); @@ -1004,10 +1037,10 @@ mod tests { let (ext, _rx) = make_externalities(db.clone()); let payload = Operations::new(vec![ Operation::AdvanceTillEthereumBlock { - block_hash: H256::repeat_byte(0xAA), + block_hash: eb_h(H256::repeat_byte(0xAA)), }, Operation::AdvanceTillEthereumBlock { - block_hash: H256::repeat_byte(0xBB), + block_hash: eb_h(H256::repeat_byte(0xBB)), }, Operation::ProgressTasks, Operation::ProcessQueues { gas_allowance: 0 }, @@ -1054,7 +1087,7 @@ mod tests { let (ext, _rx) = make_externalities(db.clone()); let payload = Operations::new(vec![ Operation::AdvanceTillEthereumBlock { - block_hash: H256::repeat_byte(0xCC), + block_hash: eb_h(H256::repeat_byte(0xCC)), }, Operation::ProgressTasks, Operation::ProcessQueues { gas_allowance: 0 }, @@ -1076,11 +1109,11 @@ mod tests { ethexe_common::mock::seed_genesis_zero_mb(&db); let chain_hashes = { let mut hashes = Vec::with_capacity(3); - let mut parent = H256::zero(); + let mut parent = HashOf::::zero(); for i in 0..3 { let mut hb = [0u8; 32]; hb[0] = 0x10 + i as u8; - let hash = H256::from(hb); + let hash = eb_h(H256::from(hb)); let header = BlockHeader { height: i as u32, timestamp: i as u64, @@ -1162,11 +1195,11 @@ mod tests { // valid `reference_block` — even though the stub mempool's // `insert` is a no-op, the value still travels through the // committed block intact. - let ref_hash = H256::repeat_byte(0x42); + let ref_hash = eb_h(H256::repeat_byte(0x42)); let header = BlockHeader { height: 1, timestamp: 0, - parent_hash: H256::zero(), + parent_hash: HashOf::::zero(), }; db.set_block_header(ref_hash, header); @@ -1275,9 +1308,9 @@ mod tests { /// program with sufficient executable balance. fn setup_mb_with_destinations( db: &Database, - parent_mb: H256, + parent_mb: HashOf, destinations: &[gprimitives::ActorId], - ) -> H256 { + ) -> HashOf { use crate::tx_validity::MIN_EXECUTABLE_BALANCE_FOR_INJECTED_MESSAGES; use ethexe_common::{ MaybeHashOf, StateHashWithQueueSize, @@ -1288,13 +1321,14 @@ mod tests { }; let operations_hash = db.set_operations(Operations::new(vec![])); - let mb_hash = H256::random(); + let mb_hash = HashOf::::random(); db.set_mb_compact_block( mb_hash, CompactMb { parent: parent_mb, height: u64::MAX / 2, operations_hash, + reserved: [0u8; 64], }, ); @@ -1339,7 +1373,7 @@ mod tests { fn signed_injected_tx( pk: ðexe_common::PrivateKey, destination: gprimitives::ActorId, - reference_block: H256, + reference_block: HashOf, salt: u8, ) -> SignedInjectedTransaction { ethexe_common::SignedMessage::create( @@ -1403,7 +1437,7 @@ mod tests { let (ext, _rx) = make_externalities_with_pool(db, mempool); *ext.chain_head.write().unwrap() = Some(head); - let payload = ext.build_operations(parent_mb).await.unwrap(); + let payload = ext.build_operations(parent_mb.inner()).await.unwrap(); let injected: Vec<_> = payload .iter() .filter_map(|tx| match tx { @@ -1478,7 +1512,7 @@ mod tests { // The producer reads chain_head_notify to pick its advance candidate; // since canonical_quarantine = 0, head's parent (block 9) is a valid // advance. - let payload = ext.build_operations(parent_mb).await.unwrap(); + let payload = ext.build_operations(parent_mb.inner()).await.unwrap(); let advance_present = payload .iter() .any(|tx| matches!(tx, Operation::AdvanceTillEthereumBlock { .. })); @@ -1562,7 +1596,9 @@ mod tests { operations.push(Operation::ProcessQueues { gas_allowance: 0 }); let payload = Operations::new(operations); assert!( - !ext.validate_operations(parent_mb, payload).await.unwrap(), + !ext.validate_operations(parent_mb.inner(), payload) + .await + .unwrap(), "MB must be rejected when touched destinations + EB-touched > cap" ); } @@ -1619,7 +1655,7 @@ mod tests { let (ext, _rx) = make_externalities_with_pool(db.clone(), mempool); *ext.chain_head.write().unwrap() = Some(head); - let payload = ext.build_operations(parent_mb).await.unwrap(); + let payload = ext.build_operations(parent_mb.inner()).await.unwrap(); let injected: Vec<_> = payload .iter() .filter_map(|tx| match tx { @@ -1659,14 +1695,14 @@ mod tests { ) -> ( EthexeExternalities, mpsc::UnboundedReceiver>, - H256, + HashOf, ) { - let mut parent = H256::zero(); + let mut parent = HashOf::::zero(); let mut chain_hashes = Vec::new(); for i in 0..3u8 { let mut hb = [0u8; 32]; hb[0] = 0x10 + i; - let hash = H256::from(hb); + let hash = eb_h(H256::from(hb)); let header = BlockHeader { height: i as u32, timestamp: i as u64, @@ -1801,7 +1837,9 @@ mod tests { Operation::ProcessQueues { gas_allowance: 0 }, ]); assert!( - !ext.validate_operations(parent_mb, payload).await.unwrap(), + !ext.validate_operations(parent_mb.inner(), payload) + .await + .unwrap(), "MB where Advance is not the first tx must be rejected" ); } @@ -1834,12 +1872,12 @@ mod tests { let db = Database::memory(); // Build a 5-block chain. - let mut parent = H256::zero(); + let mut parent = HashOf::::zero(); let mut chain = Vec::new(); for i in 0..5u8 { let mut hb = [0u8; 32]; hb[0] = 0x10 + i; - let hash = H256::from(hb); + let hash = eb_h(H256::from(hb)); let header = BlockHeader { height: i as u32, timestamp: i as u64, @@ -1862,14 +1900,15 @@ mod tests { // `canonical_quarantine = 0`), but chain[1] is a strict ancestor // of chain[3], so the descendant check would reject — and that // is exactly what we want validators to do. - let parent_mb = H256::from([0xCD; 32]); + let parent_mb = mb_h(H256::from([0xCD; 32])); let operations_hash = db.set_operations(Operations::new(vec![])); db.set_mb_compact_block( parent_mb, ethexe_common::db::CompactMb { - parent: H256::zero(), + parent: HashOf::::zero(), height: 1, operations_hash, + reserved: [0u8; 64], }, ); db.set_mb_program_states(parent_mb, ethexe_common::ProgramStates::default()); @@ -1894,7 +1933,9 @@ mod tests { ]); assert!( - !ext.validate_operations(parent_mb, payload).await.unwrap(), + !ext.validate_operations(parent_mb.inner(), payload) + .await + .unwrap(), "MB whose AdvanceTillEthereumBlock regresses parent.last_advanced_eb \ must be rejected — currently passes because validate_block_above \ skips the strict-descendant check the producer enforces", @@ -1912,12 +1953,12 @@ mod tests { let db = Database::memory(); // Build a small canonical chain `[c0, c1, c2]`. - let mut parent = H256::zero(); + let mut parent = HashOf::::zero(); let mut chain = Vec::new(); for i in 0..3u8 { let mut hb = [0u8; 32]; hb[0] = 0x20 + i; - let hash = H256::from(hb); + let hash = eb_h(H256::from(hb)); let header = BlockHeader { height: i as u32, timestamp: i as u64, @@ -1939,7 +1980,7 @@ mod tests { // canonical-ancestor record for from `head` — a fully // unrelated hash. `verify_passed` will return Err and // validation must reject in one shot. - let stranger_advance = H256::from([0xEE; 32]); + let stranger_advance = eb_h(H256::from([0xEE; 32])); let (ext, _rx) = make_externalities(db.clone()); *ext.chain_head.write().unwrap() = Some(head); @@ -1978,12 +2019,12 @@ mod tests { // Long chain — `canonical_quarantine + post_quarantine_delay` // must walk back 5 blocks, so we need at least 7 to leave // headroom and confirm the walk stops at the right depth. - let mut parent = H256::zero(); + let mut parent = HashOf::::zero(); let mut chain = Vec::new(); for i in 0..8u8 { let mut hb = [0u8; 32]; hb[0] = 0x30 + i; - let hash = H256::from(hb); + let hash = eb_h(H256::from(hb)); let header = BlockHeader { height: i as u32, timestamp: i as u64, @@ -2019,7 +2060,7 @@ mod tests { }; let candidate = ext - .find_eb_candidate_for_advancing(H256::zero()) + .find_eb_candidate_for_advancing(HashOf::::zero()) .expect("must surface a candidate — chain is deep enough"); // Walk back `2 + 3 = 5` parents from head; that's the expected // anchor. diff --git a/ethexe/malachite/service/src/lib.rs b/ethexe/malachite/service/src/lib.rs index 7a71f916731..6ff5987a22b 100644 --- a/ethexe/malachite/service/src/lib.rs +++ b/ethexe/malachite/service/src/lib.rs @@ -61,7 +61,7 @@ pub use crate::{ service::MalachiteService, tx_validity::{MIN_EXECUTABLE_BALANCE_FOR_INJECTED_MESSAGES, TxValidity, TxValidityChecker}, }; -use ethexe_common::injected::PurgedTransaction; +use ethexe_common::{EB, HashOf, injected::PurgedTransaction, malachite::MB}; pub use ethexe_common::{ injected::SignedInjectedTransaction, malachite::{Operation, Operations}, @@ -75,7 +75,7 @@ pub use gprimitives::H256; #[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct CommitCertificate { pub height: u64, - pub mb_hash: H256, + pub mb_hash: HashOf, pub signatures: Vec>, } @@ -83,18 +83,18 @@ pub struct CommitCertificate { #[derive(Debug, Clone, PartialEq, Eq)] pub enum MalachiteEvent { /// New sequencer block persisted; `mb_hash` is the Blake2b envelope hash. - BlockProposal { height: u64, mb_hash: H256 }, + BlockProposal { height: u64, mb_hash: HashOf }, /// BFT-committed block; `globals.latest_finalized_mb_hash` now points at it. BlockFinalized { cert: CommitCertificate, height: u64, - mb_hash: H256, + mb_hash: HashOf, }, /// Transactions that were purged from the mempool. PurgedTransactions { - eb_hash: H256, + eb_hash: HashOf, transactions: Vec, }, } diff --git a/ethexe/malachite/service/src/mempool.rs b/ethexe/malachite/service/src/mempool.rs index ddefa48bb24..48de3dfb63a 100644 --- a/ethexe/malachite/service/src/mempool.rs +++ b/ethexe/malachite/service/src/mempool.rs @@ -37,7 +37,7 @@ use std::{ use async_trait::async_trait; use ethexe_common::{ - HashOf, SimpleBlockData, + EB, HashOf, SimpleBlockData, db::{GlobalsStorageRO, InjectedStorageRW, OnChainStorageRO}, injected::{ InjectedTransaction, InjectedTransactionAcceptance, PurgedTransaction, @@ -45,7 +45,6 @@ use ethexe_common::{ }, }; use ethexe_db::Database; -use gprimitives::H256; use tokio::sync::Notify; use tracing::{info, trace}; @@ -140,6 +139,7 @@ pub trait Mempool: Send + Sync + 'static { /// externalities without spinning up the real pool. Kept out of the /// public API so consumers can't reach for a no-op pool in production. #[cfg(test)] +#[allow(dead_code)] #[derive(Clone, Default)] pub(crate) struct EmptyMempool; @@ -178,7 +178,7 @@ pub const DEFAULT_POOL_CAPACITY: usize = 10_000; struct Inner { pool: HashMap, SignedInjectedTransaction>, /// Recently committed txs (tx_hash → ref_block) for dedup. Aged out with the validity window. - seen: HashMap, H256>, + seen: HashMap, HashOf>, /// Latest chain head height — drives age-out of pool/seen entries. latest_head_height: Option, } @@ -216,7 +216,7 @@ impl InjectedTxMempool { /// Resolve `reference_block` to its canonical height via the DB. /// Returns `None` if the block isn't in the DB yet. - fn ref_block_height(&self, reference_block: H256) -> Option { + fn ref_block_height(&self, reference_block: HashOf) -> Option { self.db.block_header(reference_block).map(|h| h.height) } @@ -226,12 +226,12 @@ impl InjectedTxMempool { } /// Oldest block the local DB has a header for; walks stop here. - fn start_block_hash(&self) -> H256 { + fn start_block_hash(&self) -> HashOf { self.db.globals().start_block_hash } /// Set of ancestors of `head` within `VALIDITY_WINDOW` steps. - fn recent_ancestors(&self, head: &SimpleBlockData) -> HashSet { + fn recent_ancestors(&self, head: &SimpleBlockData) -> HashSet> { let start_fence = self.start_block_hash(); let mut ancestors = HashSet::with_capacity(VALIDITY_WINDOW as usize + 1); @@ -240,7 +240,7 @@ impl InjectedTxMempool { let mut current = head.hash; let mut parent = head.header.parent_hash; for _ in 0..VALIDITY_WINDOW { - if current == start_fence || parent == H256::zero() { + if current == start_fence || parent.is_zero() { break; } if !ancestors.insert(parent) { @@ -464,7 +464,7 @@ mod tests { db::{BlockMetaStorageRW, GlobalsStorageRW, OnChainStorageRW}, injected::{InjectedTransaction, InjectedTransactionAcceptance}, }; - use gprimitives::ActorId; + use gprimitives::{ActorId, H256}; use std::time::Duration; /// Pins the `TxInsertionStatus -> InjectedTransactionAcceptance` split. @@ -599,13 +599,14 @@ mod tests { /// (genesis-like), later ones link to the previous hash. fn linear_chain(db: &Database, len: usize) -> Vec { let mut chain = Vec::with_capacity(len); - let mut parent = H256::zero(); + let mut parent = HashOf::::zero(); for i in 0..len { let mut hb = [0u8; 32]; hb[0] = 0x10 + (i as u8 % 0xF0); hb[1] = (i >> 8) as u8; hb[2] = i as u8; - let hash = H256::from(hb); + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + let hash = unsafe { HashOf::::new(H256::from(hb)) }; let header = BlockHeader { height: i as u32, timestamp: i as u64, @@ -622,7 +623,7 @@ mod tests { fn signed_tx( pk: &PrivateKey, destination: ActorId, - ref_block: H256, + ref_block: HashOf, salt: u8, ) -> SignedInjectedTransaction { SignedMessage::create( @@ -643,7 +644,13 @@ mod tests { let db = Database::memory(); let pool = InjectedTxMempool::new(db); let pk = PrivateKey::random(); - let tx = signed_tx(&pk, ActorId::zero(), H256::random(), 1); + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + let tx = signed_tx( + &pk, + ActorId::zero(), + unsafe { HashOf::::new(H256::random()) }, + 1, + ); pool.insert(tx); assert_eq!(pool.len(), 1); } @@ -696,7 +703,8 @@ mod tests { // 100 txs each anchored at a random ref_block NOT in our DB. for salt in 0..100u8 { - let bogus_ref_block = H256::random(); + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + let bogus_ref_block = unsafe { HashOf::::new(H256::random()) }; pool.insert(signed_tx(&pk, ActorId::zero(), bogus_ref_block, salt)); } assert_eq!(pool.len(), 100); @@ -769,7 +777,8 @@ mod tests { let db = Database::memory(); let chain = linear_chain(&db, 2); // alt block off the same parent as chain[1] - let alt_hash = H256::from([0xAA; 32]); + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + let alt_hash = unsafe { HashOf::::new(H256::from([0xAA; 32])) }; let alt_header = BlockHeader { height: 1, timestamp: 1, @@ -891,7 +900,7 @@ mod tests { /// proptest cases (same input → same chain). fn linear_chain_seeded(db: &Database, len: usize, seed: u32) -> Vec { let mut chain = Vec::with_capacity(len); - let mut parent = H256::zero(); + let mut parent = HashOf::::zero(); for i in 0..len { let mut hb = [0u8; 32]; // Spread across the high bytes so different `seed`s never @@ -902,7 +911,8 @@ mod tests { hb[3] = ((i >> 8) & 0xff) as u8; // Bias high so the hash is non-zero even if the seed is. hb[4] = 0x80; - let hash = H256::from(hb); + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + let hash = unsafe { HashOf::::new(H256::from(hb)) }; let header = BlockHeader { height: i as u32, timestamp: i as u64, @@ -994,7 +1004,8 @@ mod tests { let mut hb = [0u8; 32]; hb[0] = 0xAA; hb[1] = (seed & 0xff) as u8; - H256::from(hb) + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + unsafe { HashOf::::new(H256::from(hb)) } }; let alt_header = BlockHeader { height: 1, diff --git a/ethexe/malachite/service/src/quarantine.rs b/ethexe/malachite/service/src/quarantine.rs index da96eec6a3a..6d138dc18a0 100644 --- a/ethexe/malachite/service/src/quarantine.rs +++ b/ethexe/malachite/service/src/quarantine.rs @@ -24,9 +24,8 @@ //! canonical descendants on top. use anyhow::{Result, anyhow}; -use ethexe_common::{SimpleBlockData, db::OnChainStorageRO}; +use ethexe_common::{EB, HashOf, SimpleBlockData, db::OnChainStorageRO}; use ethexe_db::Database; -use gprimitives::H256; /// Cap on parent walks when verifying a peer's `AdvanceTillEthereumBlock`. const VERIFY_LOOKBACK_SLACK: u32 = 100_000; @@ -43,8 +42,8 @@ pub fn anchor( db: &Database, head: SimpleBlockData, depth: u32, - start_block_hash: H256, -) -> Result> { + start_block_hash: HashOf, +) -> Result>> { let mut current = head.hash; let mut header = head.header; @@ -67,9 +66,9 @@ pub fn anchor( pub fn verify_passed( db: &Database, head: SimpleBlockData, - candidate: H256, + candidate: HashOf, canonical_quarantine: u8, - start_block_hash: H256, + start_block_hash: HashOf, ) -> Result<()> { let canonical_quarantine = canonical_quarantine as u32; let max_steps = canonical_quarantine.saturating_add(VERIFY_LOOKBACK_SLACK); @@ -108,13 +107,13 @@ pub fn verify_passed( )) } -/// `candidate` strictly descends from `ancestor` (depth ≥ 1). `H256::zero()` = +/// `candidate` strictly descends from `ancestor` (depth ≥ 1). Zero = /// pre-genesis sentinel; equal hashes return `Ok(false)`. pub fn is_strict_descendant_of( db: &Database, - candidate: H256, - ancestor: H256, - start_block_hash: H256, + candidate: HashOf, + ancestor: HashOf, + start_block_hash: HashOf, ) -> Result { if ancestor.is_zero() { return Ok(true); @@ -134,7 +133,7 @@ pub fn is_strict_descendant_of( if parent == ancestor { return Ok(true); } - if parent == H256::zero() { + if parent.is_zero() { return Err(anyhow!( "descendant check: ancestor {ancestor} not in canonical ancestry of \ candidate {candidate} — walk reached genesis" @@ -165,18 +164,20 @@ mod tests { BlockHeader, db::{BlockMetaStorageRW, OnChainStorageRW}, }; + use gprimitives::H256; /// Synthetic linear chain, oldest-first; parent[0] == zero. - fn linear_chain(db: &Database, len: usize) -> Vec { + fn linear_chain(db: &Database, len: usize) -> Vec> { let mut hashes = Vec::with_capacity(len); - let mut parent = H256::zero(); + let mut parent = HashOf::::zero(); for i in 0..len { let mut hash_bytes = [0u8; 32]; // bias high bytes so each hash is distinct and non-zero. hash_bytes[0] = 0xA0 + (i as u8 % 0x60); hash_bytes[1] = (i >> 8) as u8; hash_bytes[2] = i as u8; - let hash = H256::from(hash_bytes); + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + let hash = unsafe { HashOf::::new(H256::from(hash_bytes)) }; db.set_block_header( hash, BlockHeader { @@ -197,7 +198,10 @@ mod tests { let db = Database::memory(); let hashes = linear_chain(&db, 3); // arbitrary candidate; ancestor = zero (pre-genesis sentinel) - assert!(is_strict_descendant_of(&db, hashes[2], H256::zero(), H256::zero()).unwrap()); + assert!( + is_strict_descendant_of(&db, hashes[2], HashOf::::zero(), HashOf::::zero()) + .unwrap() + ); } #[test] @@ -223,7 +227,8 @@ mod tests { // ancestor = a hash that's not in the chain at all let mut orphan_bytes = [0xFFu8; 32]; orphan_bytes[0] = 0x42; - let orphan = H256::from(orphan_bytes); + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + let orphan = unsafe { HashOf::::new(H256::from(orphan_bytes)) }; let res = is_strict_descendant_of(&db, hashes[4], orphan, hashes[0]); assert!(res.is_err(), "expected Err for orphan ancestor: {res:?}"); } @@ -255,7 +260,7 @@ mod tests { header: ethexe_common::BlockHeader { height: (chain_len - 1) as u32, timestamp: (chain_len - 1) as u64, - parent_hash: if chain_len >= 2 { hashes[chain_len - 2] } else { H256::zero() }, + parent_hash: if chain_len >= 2 { hashes[chain_len - 2] } else { HashOf::::zero() }, }, }; // start_block = genesis (so the fence never trips). @@ -306,7 +311,7 @@ mod tests { let hashes = linear_chain(&db, chain_len); let head_hash = hashes[head_idx]; let head_height = head_idx as u32; - let head_parent = if head_idx > 0 { hashes[head_idx - 1] } else { H256::zero() }; + let head_parent = if head_idx > 0 { hashes[head_idx - 1] } else { HashOf::::zero() }; let head = SimpleBlockData { hash: head_hash, header: ethexe_common::BlockHeader { diff --git a/ethexe/malachite/service/src/service.rs b/ethexe/malachite/service/src/service.rs index dc2495325ad..a813be51584 100644 --- a/ethexe/malachite/service/src/service.rs +++ b/ethexe/malachite/service/src/service.rs @@ -25,13 +25,12 @@ use std::{ use anyhow::{Context as _, Result, anyhow}; use ethexe_common::{ - Address, SimpleBlockData, + Address, EB, HashOf, SimpleBlockData, db::{ConfigStorageRO, OnChainStorageRO}, injected::SignedInjectedTransaction, }; use ethexe_db::Database; use futures::{Stream, stream::FusedStream}; -use gprimitives::H256; use gsigner::{Signer, schemes::secp256k1::Secp256k1}; use tokio::sync::{Notify, mpsc}; @@ -260,7 +259,7 @@ impl MalachiteService { /// the prerequisite for downstream `compute_mb` not racing the /// code-validation pipeline — see the prerequisite check inside /// the externalities impl. - pub fn receive_eb_prepared(&self, _eb_hash: H256) { + pub fn receive_eb_prepared(&self, _eb_hash: HashOf) { // Drain inspects each queued entry's prerequisite against the // current `block_meta.prepared` flag, so we don't need to use // `_eb_hash` here — the FIFO drain releases everything that diff --git a/ethexe/malachite/service/src/tx_validity.rs b/ethexe/malachite/service/src/tx_validity.rs index 78fe303879c..e45c63d6a2c 100644 --- a/ethexe/malachite/service/src/tx_validity.rs +++ b/ethexe/malachite/service/src/tx_validity.rs @@ -26,16 +26,16 @@ use anyhow::{Result, anyhow}; use ethexe_common::{ - HashOf, ProgramStates, SimpleBlockData, + EB, HashOf, ProgramStates, SimpleBlockData, db::{GlobalsStorageRO, MbStorageRO, OnChainStorageRO}, events::{BlockRequestEvent, RouterRequestEvent, router::ProgramCreatedEvent}, gear::INJECTED_MESSAGE_PANIC_GAS_CHARGE_THRESHOLD, injected::{InjectedTransaction, SignedInjectedTransaction, VALIDITY_WINDOW}, - malachite::Operation, + malachite::{MB, Operation}, }; use ethexe_db::Database; use ethexe_runtime_common::state::Storage; -use gprimitives::{ActorId, H256}; +use gprimitives::ActorId; use std::collections::HashSet; /// Minimum executable balance a destination program must have to receive @@ -82,7 +82,7 @@ pub enum TxValidity { pub struct TxValidityChecker { db: Database, chain_head: SimpleBlockData, - start_block_hash: H256, + start_block_hash: HashOf, recent_included_txs: HashSet>, latest_states: ProgramStates, } @@ -96,7 +96,7 @@ impl TxValidityChecker { pub fn new_for_mb( db: Database, chain_head: SimpleBlockData, - parent_mb_hash: H256, + parent_mb_hash: HashOf, ) -> Result { // Walk back to the most recent MB whose `meta.computed` is set — // that's the snapshot whose `program_states` we can trust. The @@ -172,7 +172,10 @@ impl TxValidityChecker { Ok(TxValidity::Valid) } - fn is_reference_block_within_validity_window(&self, reference_block: H256) -> Result { + fn is_reference_block_within_validity_window( + &self, + reference_block: HashOf, + ) -> Result { let Some(reference_block_height) = self.db.block_header(reference_block).map(|h| h.height) else { return Ok(false); @@ -183,7 +186,7 @@ impl TxValidityChecker { && reference_block_height.saturating_add(VALIDITY_WINDOW as u32) > chain_head_height) } - fn is_reference_block_on_current_branch(&self, reference_block: H256) -> Result { + fn is_reference_block_on_current_branch(&self, reference_block: HashOf) -> Result { let mut block_hash = self.chain_head.hash; for _ in 0..VALIDITY_WINDOW { if block_hash == reference_block { @@ -219,7 +222,7 @@ impl TxValidityChecker { /// pragmatic break-on-missing for fast-sync recovery. pub fn collect_recent_included_txs( db: &Database, - parent_mb: H256, + parent_mb: HashOf, ) -> Result>> { let mut txs = HashSet::new(); let mut mb_hash = parent_mb; @@ -275,8 +278,8 @@ impl TxValidityChecker { /// layer. Do not rely on the returned set for anything beyond the cap. pub fn eb_touched_programs( db: &Database, - last_advanced_eb: H256, - advanced_eb: H256, + last_advanced_eb: HashOf, + advanced_eb: HashOf, ) -> Result> { if advanced_eb.is_zero() || advanced_eb == last_advanced_eb { return Ok(HashSet::new()); @@ -370,7 +373,7 @@ mod tests { use ethexe_runtime_common::state::{ ActiveProgram, MessageQueueHashWithSize, Program, ProgramState, }; - use gprimitives::ActorId; + use gprimitives::{ActorId, H256}; // ------------------------------------------------------------------ // Master-style helpers (announce → MB). @@ -381,7 +384,7 @@ mod tests { } fn test_injected_transaction( - reference_block: H256, + reference_block: HashOf, destination: ActorId, ) -> InjectedTransaction { InjectedTransaction { @@ -397,7 +400,7 @@ mod tests { SignedMessage::create(PrivateKey::random(), tx).unwrap() } - fn mock_tx(reference_block: H256) -> SignedInjectedTransaction { + fn mock_tx(reference_block: HashOf) -> SignedInjectedTransaction { signed_tx(test_injected_transaction(reference_block, ActorId::zero())) } @@ -437,8 +440,8 @@ mod tests { db: &Database, injected_transactions: Vec, destination_initialized: bool, - parent_mb: H256, - ) -> H256 { + parent_mb: HashOf, + ) -> HashOf { setup_mb_with_balance( db, injected_transactions, @@ -453,8 +456,8 @@ mod tests { injected_transactions: Vec, destination_initialized: bool, executable_balance: u128, - parent_mb: H256, - ) -> H256 { + parent_mb: HashOf, + ) -> HashOf { let ops = Operations::new( injected_transactions .into_iter() @@ -462,13 +465,14 @@ mod tests { .collect(), ); let operations_hash = db.set_operations(ops); - let mb_hash = H256::random(); + let mb_hash = HashOf::::random(); db.set_mb_compact_block( mb_hash, CompactMb { parent: parent_mb, height: u64::MAX / 2, operations_hash, + reserved: [0u8; 64], }, ); @@ -570,7 +574,7 @@ mod tests { chain.blocks.iter().skip(9).for_each(|block| { let mut header = block.to_simple().header; header.parent_hash = parent; - let hash = H256::random(); + let hash = HashOf::::random(); db.set_block_header(hash, header); blocks_branch2.push(SimpleBlockData { hash, header }); parent = hash; @@ -639,7 +643,7 @@ mod tests { let chain = test_block_chain(10).setup(&db); let chain_head = chain.blocks[9].to_simple(); - let tx = test_injected_transaction(H256::zero(), ActorId::zero()); + let tx = test_injected_transaction(HashOf::::zero(), ActorId::zero()); let parent_mb = setup_mb(&db, vec![], true, chain.mb_hash_at(8)); let tx_checker = TxValidityChecker::new_for_mb(db.clone(), chain_head, parent_mb).unwrap(); @@ -712,9 +716,12 @@ mod tests { fn genesis_parent_has_empty_states_so_every_tx_unknown_destination() { let db = Database::memory(); let chain = test_block_chain(2).setup(&db); - let checker = - TxValidityChecker::new_for_mb(db.clone(), chain.blocks[1].to_simple(), H256::zero()) - .unwrap(); + let checker = TxValidityChecker::new_for_mb( + db.clone(), + chain.blocks[1].to_simple(), + HashOf::::zero(), + ) + .unwrap(); let tx = mock_tx(chain.blocks[1].hash); assert_eq!( checker.check_tx_validity(&tx).unwrap(), @@ -730,7 +737,7 @@ mod tests { let chain = test_block_chain(10).setup(&db); let mb_grand = setup_mb(&db, vec![], true, chain.mb_hash_at(8)); - let mb_parent = H256::random(); + let mb_parent = HashOf::::random(); let operations_hash = db.set_operations(Operations::new(vec![])); db.set_mb_compact_block( mb_parent, @@ -738,6 +745,7 @@ mod tests { parent: mb_grand, height: u64::MAX / 2 + 1, operations_hash, + reserved: [0u8; 64], }, ); // mb_parent's mb_meta.computed stays false → checker walks past it. @@ -761,8 +769,8 @@ mod tests { .unwrap(); // value != 0 AND ref_block not in DB. NonZeroValue wins. - let tx = - test_injected_transaction(H256::random(), ActorId::zero()).tap_mut(|tx| tx.value = 1); + let tx = test_injected_transaction(HashOf::::random(), ActorId::zero()) + .tap_mut(|tx| tx.value = 1); assert_eq!( checker.check_tx_validity(&signed_tx(tx)).unwrap(), TxValidity::NonZeroValue, diff --git a/ethexe/malachite/service/tests/restart_resilience.rs b/ethexe/malachite/service/tests/restart_resilience.rs index ad7a3582ee9..8e8b0879db0 100644 --- a/ethexe/malachite/service/tests/restart_resilience.rs +++ b/ethexe/malachite/service/tests/restart_resilience.rs @@ -20,9 +20,10 @@ use std::{path::Path, sync::Arc, time::Duration}; use async_trait::async_trait; use ethexe_common::{ - BlockHeader, SimpleBlockData, + BlockHeader, EB, HashOf, SimpleBlockData, db::{BlockMetaStorageRW, CompactMb, GlobalsStorageRO, MbStorageRO, OnChainStorageRW}, injected::{PurgedTransaction, SignedInjectedTransaction}, + malachite::MB, }; use ethexe_db::Database; use ethexe_malachite::{ @@ -77,11 +78,11 @@ impl Mempool for EmptyMempool { /// compute service's `prepare_block` pipeline; tests that don't /// run that pipeline must seed it manually. fn seed_chain(db: &Database, len: usize, seed: u32) -> Vec { - // The producer builds the genesis MB with `parent == H256::zero()`; seed + // The producer builds the genesis MB with `parent == zero`; seed // that zero ancestor as a computed MB exactly as `initialize_empty_db` does. ethexe_common::mock::seed_genesis_zero_mb(db); let mut chain = Vec::with_capacity(len); - let mut parent = H256::zero(); + let mut parent = HashOf::::zero(); for i in 0..len { let mut hb = [0u8; 32]; hb[0] = (seed & 0xff) as u8; @@ -90,7 +91,8 @@ fn seed_chain(db: &Database, len: usize, seed: u32) -> Vec { hb[3] = ((i >> 8) & 0xff) as u8; // bias high so the produced hash is always non-zero hb[4] = 0x80; - let hash = H256::from(hb); + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + let hash = unsafe { HashOf::::new(H256::from(hb)) }; let header = BlockHeader { height: i as u32, timestamp: i as u64, @@ -286,7 +288,7 @@ async fn single_validator_finalizes_and_recovers_after_restart() { /// Walk back from `head` via [`CompactMb::parent`] and assert /// the height chain is contiguous (`expected_height`, `expected_height - 1`, /// …, 1) and that each step is reachable from the DB. -fn assert_chain_contiguous(db: &Database, head: H256, expected_height: u64) { +fn assert_chain_contiguous(db: &Database, head: HashOf, expected_height: u64) { let mut current = head; let mut expected = expected_height; loop { diff --git a/ethexe/network/src/db_sync/responses.rs b/ethexe/network/src/db_sync/responses.rs index 61de09fadd0..f490e45edb2 100644 --- a/ethexe/network/src/db_sync/responses.rs +++ b/ethexe/network/src/db_sync/responses.rs @@ -78,7 +78,11 @@ impl OngoingResponses { InnerHashesResponse(response).into() } InnerRequest::ProgramIds(request) => { - let actor_ids = match db.mb_program_states(request.at) { + // SAFETY: `request.at` is the MB envelope hash sent over the wire. + let mb_hash = unsafe { + ethexe_common::HashOf::::new(request.at) + }; + let actor_ids = match db.mb_program_states(mb_hash) { Some(states) => states.into_keys().collect(), None => { log::warn!( diff --git a/ethexe/network/src/lib.rs b/ethexe/network/src/lib.rs index 10b1f5ec942..26bd83a4735 100644 --- a/ethexe/network/src/lib.rs +++ b/ethexe/network/src/lib.rs @@ -41,7 +41,7 @@ use crate::{ }; use anyhow::{Context, anyhow}; use ethexe_common::{ - Address, BlockHeader, ValidatorsVec, + Address, BlockHeader, EB, HashOf, ValidatorsVec, db::ConfigStorageRO, ecdsa::PublicKey, injected::{SignedCompactTxReceipt, SignedInjectedTransaction}, @@ -49,7 +49,6 @@ use ethexe_common::{ }; use ethexe_db::Database; use futures::{Stream, future::Either, ready, stream::FusedStream}; -use gprimitives::H256; use gsigner::secp256k1::Signer; use libp2p::{ Multiaddr, PeerId, Swarm, Transport, connection_limits, @@ -614,7 +613,7 @@ impl NetworkService { /// /// This updates both validator-message verification and validator /// discovery so they use the latest validator snapshot. - pub fn set_chain_head(&mut self, chain_head: H256) -> anyhow::Result<()> { + pub fn set_chain_head(&mut self, chain_head: HashOf) -> anyhow::Result<()> { let snapshot = self.validator_list.set_chain_head(chain_head)?; self.validator_topic.on_new_snapshot(snapshot.clone()); @@ -922,7 +921,7 @@ mod tests { const GENESIS_BLOCK_HEADER: BlockHeader = BlockHeader { height: 0, timestamp: 0, - parent_hash: H256::zero(), + parent_hash: HashOf::::zero(), }; const TIMELINES: ProtocolTimelines = ProtocolTimelines { genesis_ts: GENESIS_BLOCK_HEADER.timestamp, diff --git a/ethexe/network/src/validator/list.rs b/ethexe/network/src/validator/list.rs index 418ab9e0229..78a15491dde 100644 --- a/ethexe/network/src/validator/list.rs +++ b/ethexe/network/src/validator/list.rs @@ -6,8 +6,9 @@ use crate::validator::ValidatorDatabase; use anyhow::Context; -use ethexe_common::{Address, BlockHeader, ProtocolTimelines, ValidatorsVec, db::OnChainStorageRO}; -use gprimitives::H256; +use ethexe_common::{ + Address, BlockHeader, EB, HashOf, ProtocolTimelines, ValidatorsVec, db::OnChainStorageRO, +}; use std::sync::Arc; /// Lightweight snapshot of [`ValidatorList`] to be used in other validator-related structures. @@ -73,7 +74,7 @@ impl ValidatorList { /// Refresh the current chain head and validator set snapshot. pub(crate) fn set_chain_head( &mut self, - chain_head: H256, + chain_head: HashOf, ) -> anyhow::Result> { let chain_head_header = self .db @@ -107,8 +108,14 @@ mod tests { use core::convert::TryFrom; use ethexe_common::db::OnChainStorageRW; use ethexe_db::Database; + use gprimitives::H256; use std::num::NonZeroU64; + fn eb(raw: u64) -> HashOf { + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + unsafe { HashOf::::new(H256::from_low_u64_be(raw)) } + } + const TIMELINES: ProtocolTimelines = ProtocolTimelines { genesis_ts: 0, era: NonZeroU64::new(10).unwrap(), @@ -129,17 +136,17 @@ mod tests { BlockHeader { height, timestamp, - parent_hash: H256::zero(), + parent_hash: HashOf::::zero(), } } #[test] fn validator_list_advances() { - let genesis_hash = H256::from_low_u64_be(0); + let genesis_hash = eb(0); let genesis_block_header = header(0, 0); - let same_era_hash = H256::from_low_u64_be(1); - let next_committed_validators_hash = H256::from_low_u64_be(2); - let next_era_hash = H256::from_low_u64_be(3); + let same_era_hash = eb(1); + let next_committed_validators_hash = eb(2); + let next_era_hash = eb(3); let db = Database::memory(); db.set_block_header(genesis_hash, genesis_block_header); diff --git a/ethexe/observer/src/lib.rs b/ethexe/observer/src/lib.rs index 30d3e1c2857..5f868dd1d14 100644 --- a/ethexe/observer/src/lib.rs +++ b/ethexe/observer/src/lib.rs @@ -63,7 +63,7 @@ use alloy::{ }; use anyhow::{Context as _, Result}; use ethexe_common::{ - Address, BlockHeader, ProtocolTimelines, SimpleBlockData, db::ConfigStorageRO, + Address, BlockHeader, EB, HashOf, ProtocolTimelines, SimpleBlockData, db::ConfigStorageRO, }; use ethexe_db::Database; use ethexe_ethereum::router::RouterQuery; @@ -87,12 +87,12 @@ type HeadersSubscriptionFuture = BoxFuture<'static, TransportResult>>; +type SyncFuture = future_timing::Timed>>>; #[derive(Debug, Clone, PartialEq, Eq)] pub enum ObserverEvent { Block(SimpleBlockData), - BlockSynced(H256), + BlockSynced(HashOf), } pub struct ObserverConfig<'a> { @@ -193,11 +193,13 @@ impl Stream for ObserverService { self.metrics.last_block_number.set(header.number as f64); let data = SimpleBlockData { - hash: H256(header.hash.0), + // SAFETY: real Ethereum block hash from the chain — carried verbatim. + hash: unsafe { HashOf::::new(H256(header.hash.0)) }, header: BlockHeader { height: header.number as u32, timestamp: header.timestamp, - parent_hash: H256(header.parent_hash.0), + // SAFETY: real Ethereum parent block hash from the chain — carried verbatim. + parent_hash: unsafe { HashOf::::new(H256(header.parent_hash.0)) }, }, }; diff --git a/ethexe/observer/src/sync.rs b/ethexe/observer/src/sync.rs index 67e136d415b..028c0247044 100644 --- a/ethexe/observer/src/sync.rs +++ b/ethexe/observer/src/sync.rs @@ -14,7 +14,7 @@ use alloy::{ }; use anyhow::{Context as _, anyhow}; use ethexe_common::{ - self, BlockData, BlockHeader, CodeBlobInfo, SimpleBlockData, + self, BlockData, BlockHeader, CodeBlobInfo, EB, HashOf, SimpleBlockData, db::{GlobalsStorageRO, GlobalsStorageRW, OnChainStorageRO, OnChainStorageRW}, events::{BlockEvent, RouterEvent, router::CodeValidationRequestedEvent}, }; @@ -85,13 +85,15 @@ impl ChainSync { } } - pub async fn sync(self, chain_head: Header) -> SyncResult { + pub async fn sync(self, chain_head: Header) -> SyncResult> { let block = SimpleBlockData { - hash: H256(chain_head.hash.0), + // SAFETY: real Ethereum block hash from the chain — carried verbatim. + hash: unsafe { HashOf::::new(H256(chain_head.hash.0)) }, header: BlockHeader { height: chain_head.number as u32, timestamp: chain_head.timestamp, - parent_hash: H256(chain_head.parent_hash.0), + // SAFETY: real Ethereum parent block hash from the chain — carried verbatim. + parent_hash: unsafe { HashOf::::new(H256(chain_head.parent_hash.0)) }, }, }; @@ -106,7 +108,7 @@ impl ChainSync { async fn load_chain( &self, block: &SimpleBlockData, - mut blocks_data: HashMap, + mut blocks_data: HashMap, BlockData>, ) -> Result> { let mut chain = Vec::new(); @@ -162,7 +164,7 @@ impl ChainSync { } /// Loads blocks if there is a gap between the `header`'s height and the latest synced block height. - async fn pre_load_data(&self, header: &BlockHeader) -> Result> { + async fn pre_load_data(&self, header: &BlockHeader) -> Result, BlockData>> { let latest_synced_eb_height = self.db.globals().latest_synced_eb.header.height; if header.height <= latest_synced_eb_height { diff --git a/ethexe/observer/src/utils.rs b/ethexe/observer/src/utils.rs index 7b9cdd2367e..1d2340f500d 100644 --- a/ethexe/observer/src/utils.rs +++ b/ethexe/observer/src/utils.rs @@ -16,7 +16,9 @@ use alloy::{ }, }; use anyhow::{Context, Result}; -use ethexe_common::{Address, BlockData, BlockHeader, SimpleBlockData, events::BlockEvent}; +use ethexe_common::{ + Address, BlockData, BlockHeader, EB, HashOf, SimpleBlockData, events::BlockEvent, +}; use ethexe_ethereum::{abi::IRouter, mirror, router}; use futures::{TryFutureExt, future}; use gprimitives::H256; @@ -32,7 +34,7 @@ const LOGS_MAX_CONCURRENCY: usize = 8; #[derive(Debug, Copy, Clone, PartialEq, Eq, derive_more::From)] pub enum BlockId { - Hash(H256), + Hash(HashOf), Latest, Finalized, } @@ -40,7 +42,7 @@ pub enum BlockId { impl BlockId { fn as_alloy(self) -> alloy::eips::BlockId { match self { - BlockId::Hash(hash) => alloy::eips::BlockId::hash(hash.0.into()), + BlockId::Hash(hash) => alloy::eips::BlockId::hash(hash.inner().0.into()), BlockId::Latest => alloy::eips::BlockId::latest(), BlockId::Finalized => alloy::eips::BlockId::finalized(), } @@ -51,9 +53,10 @@ impl BlockId { pub trait BlockLoader { async fn load_simple(&self, block: BlockId) -> Result; - async fn load(&self, block: H256, header: Option) -> Result; + async fn load(&self, block: HashOf, header: Option) -> Result; - async fn load_many(&self, range: RangeInclusive) -> Result>; + async fn load_many(&self, range: RangeInclusive) + -> Result, BlockData>>; } #[derive(Debug, Clone)] @@ -98,10 +101,13 @@ impl EthereumBlockLoader { Filter::new().event_signature(topic) } - fn logs_to_events(&self, logs: Vec) -> Result>> { - let block_hash_of = |log: &Log| -> Result { + fn logs_to_events(&self, logs: Vec) -> Result, Vec>> { + let block_hash_of = |log: &Log| -> Result> { log.block_hash - .map(|v| v.0.into()) + .map(|v| { + // SAFETY: real Ethereum block hash from the chain — carried verbatim. + unsafe { HashOf::::new(v.0.into()) } + }) .context("block hash is missing") }; @@ -129,13 +135,15 @@ impl EthereumBlockLoader { Ok(res) } - fn block_response_to_data(block: Block) -> (H256, BlockHeader) { - let block_hash = H256(block.header.hash.0); + fn block_response_to_data(block: Block) -> (HashOf, BlockHeader) { + // SAFETY: real Ethereum block hash from the chain — carried verbatim. + let block_hash = unsafe { HashOf::::new(H256(block.header.hash.0)) }; let header = BlockHeader { height: block.header.number as u32, timestamp: block.header.timestamp, - parent_hash: H256(block.header.parent_hash.0), + // SAFETY: real Ethereum parent block hash from the chain — carried verbatim. + parent_hash: unsafe { HashOf::::new(H256(block.header.parent_hash.0)) }, }; (block_hash, header) @@ -207,8 +215,8 @@ impl BlockLoader for EthereumBlockLoader { Ok(SimpleBlockData { hash, header }) } - async fn load(&self, block: H256, header: Option) -> Result { - let filter = Self::log_filter().at_block_hash(block.0); + async fn load(&self, block: HashOf, header: Option) -> Result { + let filter = Self::log_filter().at_block_hash(block.inner().0); // Preserve concrete error type so SyncError's classifier can downcast. let logs_request = self.provider.get_logs(&filter).map_err(anyhow::Error::from); @@ -248,7 +256,10 @@ impl BlockLoader for EthereumBlockLoader { }) } - async fn load_many(&self, range: RangeInclusive) -> Result> { + async fn load_many( + &self, + range: RangeInclusive, + ) -> Result, BlockData>> { if range.is_empty() { return Ok(HashMap::new()); } @@ -266,7 +277,7 @@ impl BlockLoader for EthereumBlockLoader { .await?; let mut events = self.logs_to_events(logs)?; - let mut blocks_data: HashMap = HashMap::new(); + let mut blocks_data: HashMap, BlockData> = HashMap::new(); for block in headers_batches.into_iter().flatten() { let (hash, header) = Self::block_response_to_data(block); let events = events.remove(&hash).unwrap_or_default(); diff --git a/ethexe/processor/src/promise.rs b/ethexe/processor/src/promise.rs index 8274532414d..08c46ab806d 100644 --- a/ethexe/processor/src/promise.rs +++ b/ethexe/processor/src/promise.rs @@ -1,11 +1,10 @@ // Copyright (C) Gear Technologies Inc. // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 -use ethexe_common::injected::Promise; -use gprimitives::H256; +use ethexe_common::{HashOf, injected::Promise, malachite::MB}; use tokio::sync::mpsc::{UnboundedSender, error::SendError}; -type SinkEvent = (H256, Promise); +type SinkEvent = (HashOf, Promise); /// Wrapper on top of [tokio::sync::mpsc::UnboundedSender]. /// [BoundPromiseSink] is responsible for sending the promises with @@ -13,17 +12,17 @@ type SinkEvent = (H256, Promise); #[derive(Clone)] pub struct BoundPromiseSink { sender: UnboundedSender, - mb_hash: H256, + mb_hash: HashOf, } impl BoundPromiseSink { /// Creates new instance of [BoundPromiseSink]. - pub fn new(sender: UnboundedSender, mb_hash: H256) -> Self { + pub fn new(sender: UnboundedSender, mb_hash: HashOf) -> Self { Self { sender, mb_hash } } /// Sends [Promise] to outer service. - /// Internally wraps result into `(H256, Promise)`. + /// Internally wraps result into `(HashOf, Promise)`. pub fn send(&self, promise: Promise) -> Result<(), SendError> { let event = (self.mb_hash, promise); self.sender.send(event).map_err(|err| SendError(err.0.1)) diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index e210e697f24..6598d1cd011 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -4,8 +4,8 @@ use crate::*; use anyhow::{Result, anyhow}; use ethexe_common::{ - DEFAULT_BLOCK_GAS_LIMIT, OUTGOING_MESSAGES_SOFT_LIMIT, PROGRAM_MODIFICATIONS_SOFT_LIMIT, - PrivateKey, ScheduledTask, SignedMessage, + DEFAULT_BLOCK_GAS_LIMIT, EB, HashOf, OUTGOING_MESSAGES_SOFT_LIMIT, + PROGRAM_MODIFICATIONS_SOFT_LIMIT, PrivateKey, ScheduledTask, SignedMessage, db::*, events::{ BlockRequestEvent, MirrorRequestEvent, RouterRequestEvent, @@ -13,6 +13,7 @@ use ethexe_common::{ router::ProgramCreatedEvent, }, gear::Message, + malachite::MB, mock::*, }; use ethexe_runtime_common::{ @@ -24,7 +25,7 @@ use gear_core::{ message::{ErrorReplyReason, ReplyCode, SuccessReplyReason}, }; use gear_core_errors::{SimpleExecutionError, SimpleUnavailableActorError}; -use gprimitives::{ActorId, MessageId}; +use gprimitives::{ActorId, H256, MessageId}; use parity_scale_codec::Encode; use tokio::sync::mpsc; use utils::*; @@ -118,7 +119,8 @@ mod utils { destination, payload: payload.as_ref().to_vec().try_into().unwrap(), value, - reference_block: H256::random(), + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + reference_block: unsafe { HashOf::::new(H256::random()) }, salt: H256::random().0.to_vec().try_into().unwrap(), } } @@ -1531,7 +1533,7 @@ async fn injected_ping_pong() { init_logger(); let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); - let promise_sink = BoundPromiseSink::new(promise_sender, H256::zero()); + let promise_sink = BoundPromiseSink::new(promise_sender, HashOf::::zero()); let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY]).await; let block1 = chain.blocks[1].to_simple(); @@ -1651,7 +1653,7 @@ async fn injected_prioritized_over_canonical() { init_logger(); let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); - let promise_sink = BoundPromiseSink::new(promise_sender, H256::zero()); + let promise_sink = BoundPromiseSink::new(promise_sender, HashOf::::zero()); let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY]).await; @@ -1867,7 +1869,7 @@ async fn executable_balance_injected_panic_not_charged() { init_logger(); let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); - let promise_sink = BoundPromiseSink::new(promise_sender, H256::zero()); + let promise_sink = BoundPromiseSink::new(promise_sender, HashOf::::zero()); let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_panic_payload::WASM_BINARY]).await; @@ -2255,7 +2257,8 @@ async fn injected_and_events_then_tasks_then_queues() { destination: actor_id, payload: vec![].try_into().unwrap(), value: 0, - reference_block: H256::random(), + // SAFETY: synthetic chain hash for tests — same invariant as a real EB hash. + reference_block: unsafe { HashOf::::new(H256::random()) }, salt: H256::random().0.to_vec().try_into().unwrap(), }; let signed_injected = SignedMessage::create(injected_user_pk, injected_tx).unwrap(); @@ -2274,7 +2277,7 @@ async fn injected_and_events_then_tasks_then_queues() { }]; let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); - let promise_sink = BoundPromiseSink::new(promise_sender, H256::zero()); + let promise_sink = BoundPromiseSink::new(promise_sender, HashOf::::zero()); let executable = ExecutableData { height: block3.header.height, diff --git a/ethexe/rpc/src/apis/block.rs b/ethexe/rpc/src/apis/block.rs index 225e7094476..4723c3733cf 100644 --- a/ethexe/rpc/src/apis/block.rs +++ b/ethexe/rpc/src/apis/block.rs @@ -51,7 +51,7 @@ impl BlockServer for BlockApi { hash: Option, ) -> jsonrpsee::core::RpcResult<(H256, BlockHeader)> { let SimpleBlockData { hash, header } = utils::block_at_or_latest_synced(&self.db, hash)?; - Ok((hash, header)) + Ok((hash.inner(), header)) } async fn block_events( diff --git a/ethexe/rpc/src/utils.rs b/ethexe/rpc/src/utils.rs index 460e0d1b7d0..9e99efa4f52 100644 --- a/ethexe/rpc/src/utils.rs +++ b/ethexe/rpc/src/utils.rs @@ -3,8 +3,9 @@ use crate::errors; use ethexe_common::{ - SimpleBlockData, + EB, HashOf, SimpleBlockData, db::{GlobalsStorageRO, OnChainStorageRO}, + malachite::MB, }; use ethexe_db::Database; use jsonrpsee::core::RpcResult; @@ -14,11 +15,13 @@ pub fn block_at_or_latest_synced( db: &Database, at: impl Into>, ) -> RpcResult { - let hash = if let Some(hash) = at.into() { - if !db.block_synced(hash) { + let hash: HashOf = if let Some(hash) = at.into() { + // SAFETY: RPC caller supplied an EB hash; treat it verbatim. + let typed = unsafe { HashOf::::new(hash) }; + if !db.block_synced(typed) { return Err(errors::db("Requested block is not synced")); } - hash + typed } else { db.globals().latest_synced_eb.hash }; @@ -33,6 +36,6 @@ pub fn block_at_or_latest_synced( /// At genesis this is the zero MB, which `initialize_empty_db` seeds with the /// genesis / re-genesis program states — so zero is a valid source for RPC /// reads (it carries the dump state under re-genesis) rather than "no state". -pub fn latest_computed_mb(db: &Database) -> RpcResult { +pub fn latest_computed_mb(db: &Database) -> RpcResult> { Ok(db.globals().latest_computed_mb_hash) } diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 6169d4ad832..81f6f489e5c 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -15,6 +15,7 @@ use alloy::{ providers::{Provider as _, WalletProvider, ext::AnvilApi}, }; use ethexe_common::{ + EB, HashOf, db::{CodesStorageRO, GlobalsStorageRO, InjectedStorageRO, MbStorageRO, OnChainStorageRO}, ecdsa::ContractSignature, events::{ @@ -25,6 +26,7 @@ use ethexe_common::{ injected::{ InjectedTransaction, InjectedTransactionAcceptance, Receipt, TransactionPurgedReason, }, + malachite::MB, mock::*, }; use ethexe_consensus::BatchCommitter; @@ -582,7 +584,8 @@ async fn mailbox() { let schedule = node .db - .mb_schedule(mb_hash) + // SAFETY: on-chain MB hash from Router event. + .mb_schedule(unsafe { HashOf::::new(mb_hash) }) .expect("MB schedule must exist"); assert_eq!(schedule, expected_schedule); @@ -703,7 +706,8 @@ async fn mailbox() { let schedule = node .db - .mb_schedule(mb_hash) + // SAFETY: on-chain MB hash from Router event. + .mb_schedule(unsafe { HashOf::::new(mb_hash) }) .expect("MB schedule must exist"); assert!(schedule.is_empty(), "{schedule:?}"); @@ -1940,7 +1944,7 @@ async fn injected_tx_purged_receipt() { destination: ActorId::from(H160::random()), payload: vec![].try_into().unwrap(), value: 0, - reference_block: H256::zero(), + reference_block: HashOf::::zero(), salt: vec![1].try_into().unwrap(), }; let tx_hash = tx.to_hash(); @@ -2185,7 +2189,7 @@ async fn execution_with_canonical_events_quarantine() { }) .await; - let latest_block: H256 = env.latest_block().await.hash; + let latest_block: HashOf = env.latest_block().await.hash; test_info!("📗 waiting block-prepared for block {latest_block}"); validator.events().find_block_prepared(latest_block).await; @@ -3203,7 +3207,7 @@ async fn re_genesis_with_state_dump() { env.ethereum.router().reinitialize().await.unwrap(); env.ethereum.router().lookup_genesis_hash().await.unwrap(); - let new_genesis_hash: H256 = env + let new_genesis_hash_raw: H256 = env .ethereum .router() .query() @@ -3212,6 +3216,8 @@ async fn re_genesis_with_state_dump() { .unwrap() .0 .into(); + // SAFETY: real EB hash from on-chain Router query. + let new_genesis_hash = unsafe { HashOf::::new(new_genesis_hash_raw) }; log::info!("New genesis block hash: {new_genesis_hash:?}"); // Wait until the node commits an MB covering the new genesis block before @@ -3228,7 +3234,7 @@ async fn re_genesis_with_state_dump() { dump.programs.len(), dump.blobs.len(), ); - assert_eq!(dump.eb_hash, new_genesis_hash); + assert_eq!(dump.eb_hash, new_genesis_hash_raw); assert!(!dump.codes.is_empty()); assert!(!dump.programs.is_empty()); @@ -3376,7 +3382,7 @@ async fn re_genesis_delayed_message() { env.ethereum.router().reinitialize().await.unwrap(); env.ethereum.router().lookup_genesis_hash().await.unwrap(); - let new_genesis_hash: H256 = env + let new_genesis_hash_raw: H256 = env .ethereum .router() .query() @@ -3385,6 +3391,8 @@ async fn re_genesis_delayed_message() { .unwrap() .0 .into(); + // SAFETY: real EB hash from on-chain Router query. + let new_genesis_hash = unsafe { HashOf::::new(new_genesis_hash_raw) }; log::info!("New genesis block hash: {new_genesis_hash:?}"); // Wait until the node commits an MB covering the new genesis block before @@ -3404,7 +3412,7 @@ async fn re_genesis_delayed_message() { dump.programs.len(), dump.blobs.len(), ); - assert_eq!(dump.eb_hash, new_genesis_hash); + assert_eq!(dump.eb_hash, new_genesis_hash_raw); // Verify the dispatch stash is non-empty (delayed message pending). { @@ -3441,7 +3449,7 @@ async fn re_genesis_delayed_message() { // genesis state lives under the zero MB (ancestor of the malachite genesis // block), seeded by `initialize_empty_db`. { - let schedule = new_db.mb_schedule(H256::zero()).unwrap(); + let schedule = new_db.mb_schedule(HashOf::::zero()).unwrap(); let total_tasks: usize = schedule.values().map(|tasks| tasks.len()).sum(); log::info!( "Restored schedule: {total_tasks} tasks across {} blocks", diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index 4c8530ecd5e..64e720c155b 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -8,13 +8,14 @@ use alloy::providers::{RootProvider, ext::AnvilApi}; use async_broadcast::{Receiver, RecvError, Sender}; use ethexe_blob_loader::BlobLoaderEvent; use ethexe_common::{ - Address, HashOf, SimpleBlockData, + Address, EB, HashOf, SimpleBlockData, db::*, events::BlockEvent, injected::{ InjectedTransaction, InjectedTransactionAcceptance, SignedCompactTxReceipt, SignedInjectedTransaction, }, + malachite::MB, network::VerifiedValidatorMessage, }; use ethexe_compute::ComputeEvent; @@ -310,7 +311,7 @@ impl KickingStream> { impl TestingEventReceiver { #[allow(dead_code)] - pub async fn find_block_synced(&mut self) -> H256 { + pub async fn find_block_synced(&mut self) -> HashOf { self.find_map(|event| { if let TestingEvent::Observer(ObserverEvent::BlockSynced(block_hash)) = event { Some(block_hash) @@ -324,7 +325,7 @@ impl TestingEventReceiver { /// Drive the compute stream forward until a `BlockPrepared(target)` event /// arrives. #[allow(dead_code)] - pub async fn find_block_prepared(&mut self, target: H256) -> H256 { + pub async fn find_block_prepared(&mut self, target: HashOf) -> HashOf { self.find_map(|event| match event { TestingEvent::Compute(ComputeEvent::BlockPrepared(h)) if h == target => Some(h), _ => None, @@ -334,7 +335,7 @@ impl TestingEventReceiver { /// Wait until any MB becomes computed, returning its hash. #[allow(dead_code)] - pub async fn find_any_mb_computed(&mut self) -> H256 { + pub async fn find_any_mb_computed(&mut self) -> HashOf { self.find_map(|event| match event { TestingEvent::Compute(ComputeEvent::MbComputed(mb_hash)) => Some(mb_hash), _ => None, @@ -348,7 +349,7 @@ impl TestingEventReceiver { /// ancestor of this MB's `last_advanced_eb` (i.e., it sits inside /// the eth-chain segment this MB advanced over). #[allow(dead_code)] - pub async fn wait_till_eth_block_finalized_in_mb(&mut self, target_eth_block: H256) { + pub async fn wait_till_eth_block_finalized_in_mb(&mut self, target_eth_block: HashOf) { self.find_map_with_db(|db, event| { let TestingEvent::Malachite(MalachiteEvent::BlockFinalized { mb_hash, .. }) = event else { @@ -361,7 +362,7 @@ impl TestingEventReceiver { // Anchor: previous MB's `last_advanced_eb` (genesis if none). let prev_advanced = match db.mb_compact_block(mb_hash) { Some(c) if !c.parent.is_zero() => db.mb_meta(c.parent).last_advanced_eb, - _ => H256::zero(), + _ => HashOf::::zero(), }; // Walk the eth chain from this MB's `last_advanced_eb` back to // the previous anchor; if the target is in that segment, the MB