Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions ethexe/common/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ pub struct CompactMb {
#[derive(Debug, Clone, Default, Encode, Decode, TypeInfo, PartialEq, Eq, Hash)]
pub struct MbMeta {
pub computed: bool,
pub last_advanced_eb: H256,
pub finalized: bool,
pub last_advanced_eb: Option<H256>,
}

#[auto_impl::auto_impl(&, Box)]
Expand Down Expand Up @@ -267,7 +268,7 @@ mod tests {
#[test]
fn ensure_types_unchanged() {
const EXPECTED_TYPE_INFO_HASH: &str =
"cbf21dc97ec57cc6f653a7808672dc2c086fdfae28d1435e93f0dfe812de21c3";
"6f7363ba820e33c8b6acfa633b782f998f530fcf6fe15f19afe141996261baf4";

let types = [
meta_type::<BlockMeta>(),
Expand Down
2 changes: 1 addition & 1 deletion ethexe/common/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ where
db.set_mb_outcome(H256::zero(), Vec::new());
db.mutate_mb_meta(H256::zero(), |m| {
m.computed = true;
m.last_advanced_eb = H256::zero();
m.last_advanced_eb = Some(H256::zero());
});
}

Expand Down
27 changes: 20 additions & 7 deletions ethexe/compute/src/compute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,16 +244,22 @@ pub fn prepare_executable_for_mb(
let schedule = db
.mb_schedule(parent)
.ok_or(ComputeError::ParentMbScheduleMissing(parent))?;
let advanced_block = db.mb_meta(parent).last_advanced_eb;
// `process_mb_proposal` always propagates `last_advanced_eb` when it stores
// an MB (and init seeds the zero genesis parent), so a computed parent
// missing it is a DB-invariant violation.
let advanced_block = db
.mb_meta(parent)
.last_advanced_eb
.expect("parent MB must have last_advanced_eb set");
Comment thread
grishasobol marked this conversation as resolved.

build_executable_data(db, mb_payload, program_states, schedule, advanced_block)
}

/// Walk the MB's `Operations` list and prepare processor input.
///
/// Synthetic block height/timestamp come from `last_advanced_eb` (the latest
/// EB pinned by this MB or any ancestor); if none, fall back to the router's
/// genesis block from [`ConfigStorageRO::config`].
/// EB pinned by this MB or any ancestor); the zero sentinel falls back to the
/// router's genesis block from [`ConfigStorageRO::config`].
fn build_executable_data(
db: &Database,
operations: Operations,
Expand All @@ -265,6 +271,8 @@ fn build_executable_data(
let mut injected_transactions = Vec::new();
let mut gas_allowance: Option<u64> = None;

// The zero sentinel (MB never advanced past pre-genesis) means "no anchor
// yet" — the genesis-block fallback below stands in.
let mut current_anchor = if advanced_block.is_zero() {
None
} else {
Expand Down Expand Up @@ -565,7 +573,7 @@ mod tests {
/// via the `Some` branch — exactly as the malachite service propagates it.
fn seed_genesis_eth(db: &Database) -> H256 {
let gen_eb = synthetic_eb(db, 1, vec![]);
db.mutate_mb_meta(H256::zero(), |m| m.last_advanced_eb = gen_eb);
db.mutate_mb_meta(H256::zero(), |m| m.last_advanced_eb = Some(gen_eb));
gen_eb
}

Expand Down Expand Up @@ -595,13 +603,18 @@ mod tests {
operations_hash,
},
);
// Mimic `process_mb_proposal`, which always populates `last_advanced_eb`
// before an MB is computed.
db.mutate_mb_meta(mb_hash, |meta| {
meta.last_advanced_eb = Some(H256::zero());
});
}

/// `seed_mb` plus the malachite-side bookkeeping: record the advanced EB
/// as this MB's `last_advanced_eb`, so its child walks a depth-1 chain.
fn seed_mb_advancing(db: &Database, mb_hash: H256, parent: H256, height: u64, eb_height: u32) {
seed_mb(db, mb_hash, parent, height, dummy_ops(db, eb_height));
db.mutate_mb_meta(mb_hash, |m| m.last_advanced_eb = eb_hash(eb_height));
db.mutate_mb_meta(mb_hash, |m| m.last_advanced_eb = Some(eb_hash(eb_height)));
}

/// Tail-only queue still computes all uncomputed predecessors.
Expand Down Expand Up @@ -866,7 +879,7 @@ mod tests {
}];
ops.extend(mb_bookend());
seed_mb(db, creator, H256::zero(), 0, Operations::new(ops));
db.mutate_mb_meta(creator, |m| m.last_advanced_eb = create_eb);
db.mutate_mb_meta(creator, |m| m.last_advanced_eb = Some(create_eb));
mb_hashes.push(creator);

// MB #1.. — each injects a single PING into the ping program.
Expand All @@ -886,7 +899,7 @@ mod tests {
i,
Operations::new(ops),
);
db.mutate_mb_meta(mb_hash, |m| m.last_advanced_eb = eb);
db.mutate_mb_meta(mb_hash, |m| m.last_advanced_eb = Some(eb));
mb_hashes.push(mb_hash);
}

Expand Down
2 changes: 1 addition & 1 deletion ethexe/compute/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ mod tests {
},
);
db.set_block_events(genesis_eb, &[]);
db.mutate_mb_meta(H256::zero(), |m| m.last_advanced_eb = genesis_eb);
db.mutate_mb_meta(H256::zero(), |m| m.last_advanced_eb = Some(genesis_eb));

// The EB this MB advances to, chained onto the genesis Eth block.
let eth_block_hash = H256::from_low_u64_be(0xEB01);
Expand Down
44 changes: 2 additions & 42 deletions ethexe/consensus/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,16 @@
//! This module provides utility functions and data structures for handling batch commitments,
//! validation requests, and multi-signature operations in the Ethexe system.

use anyhow::{Result, anyhow};
use anyhow::Result;
use ethexe_common::{
Address, Digest, 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};
use std::collections::BTreeMap;

/// A batch commitment, that has been signed by multiple validators.
/// This structure manages the collection of signatures from different validators
Expand Down Expand Up @@ -105,41 +103,3 @@ impl MultisignedBatchCommitment {
(self.batch, self.signatures.into_values().collect())
}
}
pub fn has_duplicates<T: std::hash::Hash + Eq>(data: &[T]) -> bool {
let mut seen = HashSet::new();
data.iter().any(|item| !seen.insert(item))
}

/// `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)`.
pub fn is_eth_block_canonical_to<DB: OnChainStorageRO>(
db: &DB,
target: H256,
head: H256,
) -> Result<bool> {
if target.is_zero() {
return Ok(true);
}
let target_height = db
.block_header(target)
.ok_or_else(|| anyhow!("eth chain walk: missing header for target {target}"))?
.height;

let mut current = head;
loop {
if current == target {
return Ok(true);
}
if current.is_zero() {
return Ok(false);
}
let header = db
.block_header(current)
.ok_or_else(|| anyhow!("eth chain walk: missing header for {current}"))?;
if header.height <= target_height {
return Ok(false);
}
current = header.parent_hash;
}
}
127 changes: 63 additions & 64 deletions ethexe/consensus/src/validator/batch/filler.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// Copyright (C) Gear Technologies Inc.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0

use super::types::{BatchLimits, BatchParts, BatchSizeCounter, ValidationRejectReason};
use std::{mem, num::NonZero};

use super::{
types::{BatchParts, BatchSizeCounter},
utils,
};

use ethexe_common::gear::{
ChainCommitment, CodeCommitment, RewardsCommitment, ValidatorsCommitment,
Expand Down Expand Up @@ -29,73 +34,49 @@ pub enum BatchIncludeError {
SizeLimitExceeded,
}

impl From<BatchIncludeError> for ValidationRejectReason {
fn from(value: BatchIncludeError) -> Self {
match value {
BatchIncludeError::SizeLimitExceeded => Self::BatchSizeLimitExceeded,
}
}
}

type FillerResult = Result<(), BatchIncludeError>;

impl BatchFiller {
pub fn new(limits: BatchLimits) -> Self {
pub fn new(batch_size_limit: u64) -> Self {
Self {
parts: BatchParts::default(),
size_counter: BatchSizeCounter::new(limits.batch_size_limit),
size_counter: BatchSizeCounter::new(batch_size_limit),
}
}

pub fn into_parts(mut self) -> BatchParts {
if let Some(chain) = &mut self.parts.chain_commitment {
if let Some((chain, _len)) = &mut self.parts.chain_commitment {
chain.transitions =
super::utils::squash_transitions_by_actor(std::mem::take(&mut chain.transitions));
super::utils::sort_transitions_by_value_to_receive(&mut chain.transitions);
utils::squash_transitions_by_actor(mem::take(&mut chain.transitions));
utils::sort_transitions_by_value_to_receive(&mut chain.transitions);
}
self.parts
}

pub fn has_chain_commitment(&self) -> bool {
self.parts.chain_commitment.is_some()
}

pub fn include_validators_commitment(
&mut self,
commitment: ValidatorsCommitment,
) -> FillerResult {
let commitment = Some(commitment);
if !self
.size_counter
.charge_for_validators_commitment(&commitment)
{
return Err(BatchIncludeError::SizeLimitExceeded);
}

self.parts.validators_commitment = commitment;
self.parts.validators_commitment = Some(commitment);
Ok(())
}

pub fn include_rewards_commitment(&mut self, commitment: RewardsCommitment) -> FillerResult {
let commitment = Some(commitment);
if !self.size_counter.charge_for_rewards_commitment(&commitment) {
return Err(BatchIncludeError::SizeLimitExceeded);
}

self.parts.rewards_commitment = commitment;
self.parts.rewards_commitment = Some(commitment);
Ok(())
}

/// Probe whether a hypothetical chain commitment with `transitions` would
/// still fit the remaining batch budget. Used by the producer to grow the
/// chain commitment one MB at a time and stop *before* the size limit is
/// breached, so the call to [`Self::include_chain_commitment`] is
/// guaranteed to succeed.
pub fn would_fit_chain_commitment(&self, candidate: &ChainCommitment) -> bool {
let mut probe = self.size_counter.clone();
probe.charge_for_chain_commitment(&Some(candidate.clone()))
}

pub fn include_code_commitment(&mut self, commitment: CodeCommitment) -> FillerResult {
if !self.size_counter.charge_for_code_commitment(&commitment) {
return Err(BatchIncludeError::SizeLimitExceeded);
Expand All @@ -105,24 +86,33 @@ impl BatchFiller {
Ok(())
}

/// Include a freshly aggregated chain commitment in the batch.
///
/// A commitment with neither transitions nor an Ethereum-anchor
/// advance carries no payload and is dropped — the next coordinator
/// round will re-walk and pick up whatever has finalized since.
/// Empty-transitions checkpoints **with** a non-zero
/// `last_advanced_eth_block` are kept: they exist specifically to push
/// the on-chain Ethereum anchor forward during long quiet stretches.
pub fn include_chain_commitment(&mut self, commitment: ChainCommitment) -> FillerResult {
if commitment.transitions.is_empty() && commitment.last_advanced_eth_block.is_zero() {
return Ok(());
pub fn append_chain_commitment(&mut self, commitment: ChainCommitment) -> FillerResult {
if let Some((existing, len)) = &mut self.parts.chain_commitment {
let ChainCommitment {
head,
transitions,
last_advanced_eth_block,
} = commitment;

if !self.size_counter.charge_for_transitions(&transitions) {
return Err(BatchIncludeError::SizeLimitExceeded);
}

existing.head = head;
existing.transitions.extend(transitions);
existing.last_advanced_eth_block = last_advanced_eth_block;

*len = len
.checked_add(1)
.expect("u32 chain commitment len overflow");
Comment thread
grishasobol marked this conversation as resolved.
} else {
if !self.size_counter.charge_for_chain_commitment(&commitment) {
return Err(BatchIncludeError::SizeLimitExceeded);
}

self.parts.chain_commitment = Some((commitment, NonZero::new(1).expect("1 != 0")));
}

let commitment = Some(commitment);
if !self.size_counter.charge_for_chain_commitment(&commitment) {
return Err(BatchIncludeError::SizeLimitExceeded);
}
self.parts.chain_commitment = commitment;
Ok(())
}
}
Expand All @@ -134,24 +124,36 @@ mod tests {
use ethexe_ethereum::abi::Gear;
use gprimitives::{CodeId, H256};

/// Checkpoint chain commitments carry empty transitions but a
/// non-zero `last_advanced_eth_block` — they exist *specifically*
/// to push the on-chain Ethereum anchor forward when the chain has
/// been quiet for a long stretch. The filler must keep them.
const BIG_LIMIT: u64 = u64::MAX;

/// Appending a single chain commitment seeds the parts with a length of 1,
/// and a subsequent append extends it (head + anchor follow the newest MB).
#[test]
fn include_chain_commitment_keeps_checkpoint_with_no_transitions() {
let mut filler = BatchFiller::new(BatchLimits::default());
let checkpoint = ChainCommitment {
fn append_chain_commitment_seeds_then_extends() {
let mut filler = BatchFiller::new(BIG_LIMIT);
let first = ChainCommitment {
head: H256::from_low_u64_be(0xC0DE),
transitions: Vec::new(),
last_advanced_eth_block: H256::from_low_u64_be(0xEB),
};
let second = ChainCommitment {
head: H256::from_low_u64_be(0xBEEF),
transitions: Vec::new(),
last_advanced_eth_block: H256::from_low_u64_be(0xEC),
};

filler.include_chain_commitment(checkpoint).unwrap();
assert!(
filler.has_chain_commitment(),
"checkpoint with empty transitions but a non-zero advanced anchor must \
be retained — dropping it strands the Ethereum-side anchor advance"
filler.append_chain_commitment(first).unwrap();
filler.append_chain_commitment(second.clone()).unwrap();

let (chain, len) = filler
.into_parts()
.chain_commitment
.expect("chain commitment must be retained");
assert_eq!(len.get(), 2);
assert_eq!(chain.head, second.head);
assert_eq!(
chain.last_advanced_eth_block,
second.last_advanced_eth_block
);
}

Expand All @@ -166,10 +168,7 @@ mod tests {
};
let encoded: Gear::CodeCommitment = first.clone().into();
// Budget fits exactly one commitment; the second include must fail.
let mut filler = BatchFiller::new(BatchLimits {
batch_size_limit: encoded.abi_encoded_size() as u64,
..BatchLimits::default()
});
let mut filler = BatchFiller::new(encoded.abi_encoded_size() as u64);

filler.include_code_commitment(first.clone()).unwrap();
assert_eq!(
Expand Down
Loading
Loading