Skip to content

Commit a5e2859

Browse files
committed
refactor(vara.eth): introduce typed MB/EB hashes, move MB/BlockPayload to ethexe-common
Move the consensus block envelope `MB` (formerly `ethexe_malachite_core::Block`), the versioned `BlockPayload`, and the on-disk `CompactMb` shape into `ethexe_common::malachite` so the executor (`ethexe-compute`, etc.) can reason about them without depending on the consensus layer. Type-safe MB / EB hashes: - `CompactMb { parent: HashOf<MB>, height, payload_hash, reserved }` — same shape as `MB` with the opaque payload bytes replaced by the payload digest. Renames the legacy `transactions_hash` field. - `MbMeta::last_advanced_eb: HashOf<EB>`. - `BlockMeta::{last_committed_mb, last_committed_eb}` typed. - `DBGlobals::{start_block_hash, latest_prepared_eb_hash, latest_finalized_mb_hash, latest_computed_mb_hash}` typed. - `DBConfig::genesis_block_hash: HashOf<EB>`. - New `pub type EB = SimpleBlockData` alias + `SimpleBlockData::typed_hash()` / `eb_hash_of(H256)` for lifting raw Ethereum block hashes into the typed envelope. `HashOf<T>` SCALE-encodes as a bare 32-byte hash (phantom is `#[codec(skip)]`), so the on-wire/on-disk byte layout is identical to the previous bare `H256` columns where these fields used to be plain hashes. Trait sigs in `MbStorageRO/RW` keep `mb_hash: H256` for now — the gain from threading `HashOf<MB>` through every storage call site (compute, cli, consensus, processor, etc.) doesn't justify the cascade right now; typed wrappers at boundary structs already catch the EB-vs-MB confusion the refactor was about. Call sites pass `field.inner()` where needed. Fixes #5507
1 parent eca538f commit a5e2859

27 files changed

Lines changed: 412 additions & 239 deletions

File tree

ethexe/cli/src/commands/check.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ impl Checker {
163163
/// Traverses the persisted block DAG and validates referential integrity.
164164
async fn integrity_check(&self) -> Result<()> {
165165
let db = &self.db;
166-
let bottom = self.globals.start_block_hash;
166+
let bottom = self.globals.start_block_hash.inner();
167167
let head = self.globals.latest_synced_eb.hash;
168168

169169
let bottom = db
@@ -244,7 +244,7 @@ impl Checker {
244244
/// Each MB runs against an overlaid DB so writes from the
245245
/// re-execution don't pollute the on-disk state.
246246
async fn computation_check(&self) -> Result<()> {
247-
let head = self.globals.latest_finalized_mb_hash;
247+
let head = self.globals.latest_finalized_mb_hash.inner();
248248
if head.is_zero() {
249249
println!("📋 No finalized MB yet — nothing to verify");
250250
return Ok(());
@@ -337,10 +337,10 @@ impl Checker {
337337
pb.inc(1);
338338
};
339339

340-
if current_compact_mb.parent == H256::zero() {
340+
if current_compact_mb.parent.is_zero() {
341341
break;
342342
}
343-
current_mb = current_compact_mb.parent;
343+
current_mb = current_compact_mb.parent.inner();
344344
}
345345

346346
Ok(())

ethexe/cli/src/commands/dump.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ impl DumpCommand {
7676
let db = Database::try_from_raw(raw_db)?;
7777

7878
let block_hash = block_hash.unwrap_or_else(|| {
79-
let latest_prepared_block = db.globals().latest_prepared_eb_hash;
79+
let latest_prepared_block = db.globals().latest_prepared_eb_hash.inner();
8080
log::info!(
8181
"No block hash provided, using latest committed block: {latest_prepared_block:?}"
8282
);

ethexe/common/src/db.rs

Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44
//! Common db types and traits.
55
66
use crate::{
7-
Address, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, ProtocolTimelines, Schedule,
8-
SimpleBlockData, ValidatorsVec,
7+
Address, BlockHeader, CodeBlobInfo, Digest, EB, HashOf, ProgramStates, ProtocolTimelines,
8+
Schedule, ValidatorsVec,
99
events::BlockEvent,
1010
gear::StateTransition,
1111
injected::{InjectedTransaction, Promise, SignedCompactPromise, SignedInjectedTransaction},
12-
malachite::Transactions,
12+
malachite::{MB, Transactions},
1313
};
14+
15+
// Re-export so existing `ethexe_common::db::CompactMb` imports keep working
16+
// — the type itself now lives in `ethexe_common::malachite` alongside `MB`.
17+
pub use crate::malachite::CompactMb;
1418
use alloc::{
1519
collections::{BTreeSet, VecDeque},
1620
vec::Vec,
@@ -34,9 +38,9 @@ pub struct BlockMeta {
3438
/// Last committed on-chain batch hash (digest).
3539
pub last_committed_batch: Option<Digest>,
3640
/// Last committed MB hash.
37-
pub last_committed_mb: Option<H256>,
41+
pub last_committed_mb: Option<HashOf<MB>>,
3842
/// Last committed EB hash.
39-
pub last_committed_eb: Option<H256>,
43+
pub last_committed_eb: Option<HashOf<EB>>,
4044
/// Latest era with committed validators.
4145
pub latest_era_validators_committed: Option<u64>,
4246
}
@@ -88,8 +92,8 @@ pub trait OnChainStorageRO {
8892
fn block_synced(&self, block_hash: H256) -> bool;
8993
fn validators(&self, era_index: u64) -> Option<ValidatorsVec>;
9094

91-
fn block_simple_data(&self, block_hash: H256) -> Option<SimpleBlockData> {
92-
self.block_header(block_hash).map(|header| SimpleBlockData {
95+
fn block_simple_data(&self, block_hash: H256) -> Option<EB> {
96+
self.block_header(block_hash).map(|header| EB {
9397
hash: block_hash,
9498
header,
9599
})
@@ -129,35 +133,23 @@ pub trait InjectedStorageRW: InjectedStorageRO {
129133
fn set_compact_promise(&self, promise: &SignedCompactPromise);
130134
}
131135

132-
/// MB static identity. Keyed by the Blake2b envelope hash; existence implies
133-
/// the matching `Transactions` blob is in CAS at `transactions_hash`.
134-
#[derive(
135-
Debug, Clone, Copy, Default, Encode, Decode, TypeInfo, PartialEq, Eq, Hash, derive_more::Display,
136-
)]
137-
#[display("MB(height {height}, parent {parent}, transactions_hash {transactions_hash})")]
138-
pub struct CompactMb {
139-
pub parent: H256,
140-
pub height: u64,
141-
pub transactions_hash: H256,
142-
}
143-
144136
/// MB dynamic state. `last_advanced_eb` is propagated forward at save time
145-
/// (resets on `AdvanceTillEthereumBlock`); `synced` requires this MB and every
146-
/// ancestor to be persisted.
137+
/// (resets on `AdvanceTillEthereumBlock`).
147138
#[derive(Debug, Clone, Default, Encode, Decode, TypeInfo, PartialEq, Eq, Hash)]
148139
pub struct MbMeta {
149140
pub computed: bool,
150-
pub last_advanced_eb: H256,
141+
pub last_advanced_eb: HashOf<EB>,
151142
}
152143

153144
#[auto_impl::auto_impl(&, Box)]
154145
pub trait MbStorageRO {
155-
/// Static identity (parent + height + `transactions_hash`).
156-
/// Existence implies the matching [`Transactions`] blob is in the
157-
/// CAS at `transactions_hash`.
146+
/// Static identity (parent + height + `payload_hash`).
147+
/// Existence implies the matching application-side [`Transactions`]
148+
/// blob is in the CAS at `payload_hash`.
158149
fn mb_compact_block(&self, mb_hash: H256) -> Option<CompactMb>;
159-
/// Read the [`Transactions`] blob from CAS by its content hash.
160-
fn transactions(&self, transactions_hash: H256) -> Option<Transactions>;
150+
/// Read the application-level [`Transactions`] blob from CAS by
151+
/// the same content hash stored in [`CompactMb::payload_hash`].
152+
fn transactions(&self, payload_hash: H256) -> Option<Transactions>;
161153
fn mb_program_states(&self, mb_hash: H256) -> Option<ProgramStates>;
162154
fn mb_outcome(&self, mb_hash: H256) -> Option<Vec<StateTransition>>;
163155
fn mb_schedule(&self, mb_hash: H256) -> Option<Schedule>;
@@ -168,7 +160,7 @@ pub trait MbStorageRO {
168160
pub trait MbStorageRW: MbStorageRO {
169161
fn set_mb_compact_block(&self, mb_hash: H256, compact: CompactMb);
170162
/// Write a [`Transactions`] blob into the CAS and return its hash
171-
/// (the value stored in [`CompactMb::transactions_hash`]).
163+
/// (the value stored in [`CompactMb::payload_hash`]).
172164
fn set_transactions(&self, transactions: Transactions) -> H256;
173165
fn set_mb_program_states(&self, mb_hash: H256, program_states: ProgramStates);
174166
fn set_mb_outcome(&self, mb_hash: H256, outcome: Vec<StateTransition>);
@@ -182,8 +174,8 @@ pub struct PreparedBlockData {
182174
pub latest_era_with_committed_validators: u64,
183175
pub codes_queue: VecDeque<CodeId>,
184176
pub last_committed_batch: Digest,
185-
pub last_committed_mb: H256,
186-
pub last_committed_eb: H256,
177+
pub last_committed_mb: HashOf<MB>,
178+
pub last_committed_eb: HashOf<EB>,
187179
}
188180

189181
#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)]
@@ -192,24 +184,24 @@ pub struct DBConfig {
192184
pub chain_id: u64,
193185
pub router_address: Address,
194186
pub timelines: ProtocolTimelines,
195-
pub genesis_block_hash: H256,
187+
pub genesis_block_hash: HashOf<EB>,
196188
pub max_validators: u16,
197189
}
198190

199191
#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)]
200192
pub struct DBGlobals {
201-
pub start_block_hash: H256,
202-
pub latest_synced_eb: SimpleBlockData,
203-
pub latest_prepared_eb_hash: H256,
193+
pub start_block_hash: HashOf<EB>,
194+
pub latest_synced_eb: EB,
195+
pub latest_prepared_eb_hash: HashOf<EB>,
204196
/// Latest MB BFT-finalized by Malachite. Rows
205197
/// (`mb_program_states`/`mb_outcome`/`mb_schedule`) may not yet
206198
/// be persisted — use [`Self::latest_computed_mb_hash`] for any
207199
/// read that depends on those rows existing.
208-
pub latest_finalized_mb_hash: H256,
200+
pub latest_finalized_mb_hash: HashOf<MB>,
209201
/// Latest MB whose per-row state has been written by the compute
210202
/// pipeline. Trails `latest_finalized_mb_hash` until compute
211203
/// catches up.
212-
pub latest_computed_mb_hash: H256,
204+
pub latest_computed_mb_hash: HashOf<MB>,
213205
}
214206

215207
#[cfg(feature = "std")]
@@ -257,15 +249,17 @@ pub use mock_interfaces::{SetConfig, SetGlobals};
257249
#[cfg(test)]
258250
mod tests {
259251
use super::*;
260-
use crate::malachite::{BlockPayload, Transactions};
252+
use crate::malachite::{BlockPayload, MB, Transactions};
261253
use indoc::formatdoc;
262254
use scale_info::{PortableRegistry, Registry, meta_type};
263255
use sha3::{Digest, Sha3_256};
264256

265257
#[test]
266258
fn ensure_types_unchanged() {
259+
// Recomputed by running the test once after the typed-hash + MB
260+
// move refactor — see Task 2 (typed MB/EB hashes).
267261
const EXPECTED_TYPE_INFO_HASH: &str =
268-
"7e0ef3b2f5f720c051c6dc3084995abb1cf256e1fa5f0d818f64eaa2c99eae4d";
262+
"70489b881e7564a322d1b42766934a3e164c99affbc962f2b8ddd8a42d559499";
269263

270264
let types = [
271265
meta_type::<BlockMeta>(),
@@ -282,8 +276,9 @@ mod tests {
282276
meta_type::<StateTransition>(),
283277
meta_type::<Schedule>(),
284278
meta_type::<MbMeta>(),
285-
meta_type::<CompactMb>(),
279+
meta_type::<MB>(),
286280
meta_type::<BlockPayload>(),
281+
meta_type::<CompactMb>(),
287282
meta_type::<Transactions>(),
288283
meta_type::<DBConfig>(),
289284
meta_type::<DBGlobals>(),

ethexe/common/src/hash.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ impl<T> HashOf<T> {
109109
}
110110
}
111111

112+
/// True iff this is the zero-byte sentinel hash. Useful where a
113+
/// caller treats `H256::zero()` as "no value yet" (genesis pre-MB,
114+
/// uninitialised parent links, etc.).
115+
pub fn is_zero(&self) -> bool {
116+
self.hash.is_zero()
117+
}
118+
112119
#[cfg(feature = "mock")]
113120
pub fn random() -> Self {
114121
Self {

ethexe/common/src/malachite.rs

Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
// Copyright (C) Gear Technologies Inc.
22
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
33

4-
//! Application-level block shape produced by the Malachite sequencer
5-
//! and consumed by the ethexe executor.
6-
//!
7-
//! Two layers live here:
4+
//! Malachite block model shared by the consensus service
5+
//! (`ethexe-malachite-core`), the consensus glue layer
6+
//! (`ethexe-malachite`), and the executor (`ethexe-compute`).
87
//!
8+
//! - [`MB`] is the consensus block envelope: `(parent_hash, height,
9+
//! payload, reserved)`. Its hash is [`HashOf<MB>`].
910
//! - [`BlockPayload`] is the opaque, versioned, size-capped wire
10-
//! envelope the consensus engine ships around. The application
11-
//! schema — [`Transactions`] (an ordered list of [`Transaction`]s) —
12-
//! lives SCALE-encoded inside [`BlockPayload::bytes`].
13-
//! - Block-level identity (parent linkage, height) lives in
14-
//! [`crate::db::CompactMb`], indexed by the consensus block envelope
15-
//! hash. The matching [`BlockPayload`] / [`Transactions`] blob is
16-
//! stored in the content-addressed half of the ethexe db and
17-
//! referenced by `CompactMb::transactions_hash`.
11+
//! payload carried by an MB. The application schema
12+
//! ([`Transactions`]) lives inside [`BlockPayload::bytes`].
13+
//! - [`CompactMb`] is `MB` with the payload bytes replaced by
14+
//! `payload_hash` — what gets indexed in the ethexe DB once the
15+
//! matching [`BlockPayload`] / [`Transactions`] blob is in CAS.
16+
//! - [`Transactions`] is the application-level ordered list of
17+
//! [`Transaction`]s that the executor consumes.
1818
//!
1919
//! These types live in `ethexe-common` (rather than inside
2020
//! `ethexe-malachite`) so `ethexe-processor` can accept them without
2121
//! depending on the consensus layer.
2222
23-
use crate::injected::SignedInjectedTransaction;
23+
use crate::{HashOf, injected::SignedInjectedTransaction};
2424
use alloc::vec::Vec;
2525
use anyhow::{Result, anyhow};
2626
use derive_more::{Deref, DerefMut, IntoIterator};
@@ -33,8 +33,8 @@ use scale_info::TypeInfo;
3333
use serde::{Deserialize, Serialize};
3434

3535
/// Per-block payload size cap: 1000 KiB, leaving headroom under the
36-
/// 1 MiB engine block ceiling for the consensus block envelope
37-
/// (parent hash, height, reserved tail) and SCALE framing.
36+
/// 1 MiB consensus block ceiling for the [`MB`] envelope (parent hash,
37+
/// height, reserved tail) and SCALE framing.
3838
pub const MAX_BLOCK_PAYLOAD_BYTES: usize = 1024 * 1000;
3939

4040
/// Current `BlockPayload::version` written by this code path.
@@ -45,14 +45,12 @@ pub const MAX_BLOCK_PAYLOAD_BYTES: usize = 1024 * 1000;
4545
/// ones.
4646
pub const BLOCK_PAYLOAD_VERSION: u16 = 0;
4747

48-
/// Versioned, size-capped block payload.
48+
/// Versioned, size-capped block payload carried by an [`MB`].
4949
///
50-
/// The consensus engine treats `bytes` as an opaque byte string —
51-
/// the application crate is responsible for the schema (today, a
52-
/// SCALE-encoded [`Transactions`]). `version` exists so a future
53-
/// protocol bump can change the `bytes` encoding without breaking the
54-
/// consensus block wire shape: decoders inspect `version` and
55-
/// dispatch accordingly.
50+
/// The consensus service treats `bytes` as opaque. The ethexe
51+
/// application schema lives inside as a SCALE-encoded [`Transactions`]
52+
/// — `version` exists so a future protocol bump can change that
53+
/// encoding without breaking the [`MB`] wire shape.
5654
#[derive(Clone, Debug, Default, PartialEq, Eq, Encode, Decode, TypeInfo)]
5755
pub struct BlockPayload {
5856
pub version: u16,
@@ -73,6 +71,91 @@ impl BlockPayload {
7371
bytes,
7472
})
7573
}
74+
75+
/// Content-addressed hash of the application bytes (the value
76+
/// stored in [`CompactMb::payload_hash`]). The `version` prefix
77+
/// deliberately does NOT contribute to the digest: at v0 the
78+
/// bytes are SCALE-encoded [`Transactions`], so this hash
79+
/// matches the legacy `Transactions`-keyed CAS slot byte-for-byte.
80+
pub fn hash(&self) -> H256 {
81+
gear_core::utils::hash(self.bytes.as_ref()).into()
82+
}
83+
}
84+
85+
/// Malachite block envelope: opaque versioned payload plus
86+
/// chain-position fields (parent hash + height) and a [`Self::reserved`]
87+
/// tail for future protocol extensions.
88+
///
89+
/// The block hash ([`Self::hash`]) is [`gear_core::utils::hash`]
90+
/// (Blake2b-256) over a SCALE-encoded
91+
/// `(parent_hash, height, payload_hash, reserved)` tuple, where
92+
/// `payload_hash = BlockPayload::hash()`. Two nodes with the same
93+
/// envelope content produce the same hash.
94+
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo)]
95+
pub struct MB {
96+
pub parent_hash: HashOf<MB>,
97+
pub height: u64,
98+
pub payload: BlockPayload,
99+
pub reserved: [u8; 64],
100+
}
101+
102+
impl MB {
103+
/// Construct an MB with `reserved` zeroed out.
104+
pub fn new(parent_hash: HashOf<MB>, height: u64, payload: BlockPayload) -> Self {
105+
Self {
106+
parent_hash,
107+
height,
108+
payload,
109+
reserved: [0u8; 64],
110+
}
111+
}
112+
113+
/// Compute the canonical [`HashOf<MB>`] for this envelope.
114+
pub fn hash(&self) -> HashOf<MB> {
115+
let payload_hash = self.payload.hash();
116+
let inner = (self.parent_hash, self.height, payload_hash, self.reserved).encode();
117+
let raw: H256 = gear_core::utils::hash(&inner).into();
118+
// SAFETY: `raw` is the canonical MB envelope digest. Wrapping
119+
// it in `HashOf<MB>` is exactly what the constructor exists for.
120+
unsafe { HashOf::new(raw) }
121+
}
122+
}
123+
124+
/// MB static identity. Same shape as [`MB`] but with the opaque
125+
/// payload bytes replaced by `payload_hash`. Existence implies the
126+
/// matching application-level [`Transactions`] blob is in the
127+
/// content-addressed half of the ethexe DB at `payload_hash`.
128+
#[derive(
129+
Debug, Clone, Copy, Encode, Decode, TypeInfo, PartialEq, Eq, Hash, derive_more::Display,
130+
)]
131+
#[display("MB(height {height}, parent {parent}, payload_hash {payload_hash})")]
132+
pub struct CompactMb {
133+
pub parent: HashOf<MB>,
134+
pub height: u64,
135+
pub payload_hash: H256,
136+
pub reserved: [u8; 64],
137+
}
138+
139+
impl Default for CompactMb {
140+
fn default() -> Self {
141+
Self {
142+
parent: HashOf::zero(),
143+
height: 0,
144+
payload_hash: H256::zero(),
145+
reserved: [0u8; 64],
146+
}
147+
}
148+
}
149+
150+
impl CompactMb {
151+
/// Recompute the [`HashOf<MB>`] from this compact record. Matches
152+
/// [`MB::hash`] byte-for-byte by construction (same SCALE tuple).
153+
pub fn mb_hash(&self) -> HashOf<MB> {
154+
let inner = (self.parent, self.height, self.payload_hash, self.reserved).encode();
155+
let raw: H256 = gear_core::utils::hash(&inner).into();
156+
// SAFETY: identical derivation to `MB::hash`.
157+
unsafe { HashOf::new(raw) }
158+
}
76159
}
77160

78161
/// A single transaction in the malachite block.

0 commit comments

Comments
 (0)