Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Personal Rules

If the file `~/.config/claude/gear-rules.md` exists, read it at the start of each session and follow the instructions there. It contains developer-specific preferences that override or supplement the rules below.

## Project Overview

Gear Protocol — a Substrate-based platform for running WebAssembly smart contracts (programs) with an actor-model message-passing architecture. The main network is **Vara**. The repo also contains **ethexe**, a layer that runs Gear programs on Ethereum.
Expand Down
7 changes: 7 additions & 0 deletions ethexe/common/src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,10 @@ impl<T> From<HashOf<T>> for MaybeHashOf<T> {
Self(Some(value))
}
}

/// Hash of value with the value itself.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WithHashOf<T: 'static> {
pub hash: HashOf<T>,
pub value: T,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please replace it to data: T, because value sounds like transaction value or something that have digital type.

Also in announces.rs file it would be easy to read:

Current announce.value from first view is not clearly describe the logic

let not_base_announce_hash = db.find_block_announce(block_hash, |announce| {
    announce.value.parent == best_parent && !announce.value.is_base()
})?;

}
2 changes: 1 addition & 1 deletion ethexe/common/src/primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ pub struct SimpleBlockData {
#[cfg_attr(feature = "serde", derive(Hash))]
#[derive(Clone, Debug, Encode, Decode, TypeInfo, PartialEq, Eq, derive_more::Display)]
#[display(
"Announce(block: {block_hash}, parent: {parent}, gas: {gas_allowance:?}, txs: {injected_transactions:?})"
"Announce(hash: {}, block: {block_hash}, parent: {parent}, gas: {gas_allowance:?}, txs: {injected_transactions:?})", self.to_hash()
)]
pub struct Announce {
pub block_hash: H256,
Expand Down
277 changes: 268 additions & 9 deletions ethexe/consensus/src/announces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
use crate::tx_validation::{TxValidity, TxValidityChecker};
use anyhow::{Result, anyhow, ensure};
use ethexe_common::{
Announce, HashOf, MAX_TOUCHED_PROGRAMS_PER_ANNOUNCE, SimpleBlockData,
Announce, HashOf, MAX_TOUCHED_PROGRAMS_PER_ANNOUNCE, SimpleBlockData, WithHashOf,
db::{
AnnounceStorageRW, BlockMetaStorageRW, GlobalsStorageRO, InjectedStorageRW,
OnChainStorageRO,
Expand Down Expand Up @@ -128,6 +128,13 @@ pub trait DBAnnouncesExt:
&self,
announces: impl IntoIterator<Item = HashOf<Announce>>,
) -> Result<BTreeSet<HashOf<Announce>>>;

/// Find announce in the same block with the same parent as provided announce.
fn find_block_base_announce_with_parent(
&self,
block_hash: H256,
parent: HashOf<Announce>,
) -> Result<Option<WithHashOf<Announce>>>;
}

impl<
Expand Down Expand Up @@ -208,6 +215,32 @@ impl<
})
.collect()
}

fn find_block_base_announce_with_parent(
&self,
block_hash: H256,
parent: HashOf<Announce>,
) -> Result<Option<WithHashOf<Announce>>> {
let announces = self
.block_meta(block_hash)
.announces
.ok_or_else(|| anyhow!("announces not found for block({block_hash})"))?;

for announce_hash in announces {
let announce = self
.announce(announce_hash)
.ok_or_else(|| anyhow!("announce({announce_hash}) not found"))?;

if announce.parent == parent && announce.is_base() {
return Ok(Some(WithHashOf {
hash: announce_hash,
value: announce,
}));
}
}

Ok(None)
}
}

/// Propagate announces along the provided chain of blocks.
Expand Down Expand Up @@ -601,21 +634,81 @@ pub fn best_parent_announce(
block_hash: H256,
commitment_delay_limit: u32,
) -> Result<HashOf<Announce>> {
let announces = db
.block_meta(block_hash)
.announces
.ok_or_else(|| anyhow!("announces not found for block {block_hash}"))?;

// We do not take announces directly from parent block,
// because some of them may be expired at `block_hash`,
// so we take parents of all announces from `block_hash`,
// to be sure that we take only not expired parent announces.
let parent_announces =
db.announces_parents(db.block_meta(block_hash).announces.into_iter().flatten())?;
let candidates = db.announces_parents(announces)?;

best_announce(db, parent_announces, commitment_delay_limit)
best_announce(db, candidates, commitment_delay_limit - 1)
Comment thread
grishasobol marked this conversation as resolved.
Outdated
}

pub fn block_best_announce(
db: &impl DBAnnouncesExt,
block_hash: H256,
commitment_delay_limit: u32,
) -> Result<HashOf<Announce>> {
let candidates = db
.block_meta(block_hash)
.announces
.ok_or_else(|| anyhow!("announces not found for block {block_hash}"))?;

// We do not take announces directly from parent block,
// because some of them may be expired at `block_hash`,
// so we take parents of all announces from `block_hash`,
// to be sure that we take only not expired parent announces.
let parent_announces = db.announces_parents(candidates.iter().cloned())?;

let best_parent = best_announce(db, parent_announces, commitment_delay_limit - 1)?;
Comment thread
grishasobol marked this conversation as resolved.
Outdated

// Find child announces
let mut not_base_announce_hash = None;
let mut base_announce_hash = None;
for candidate in candidates {
let announce = db
.announce(candidate)
.ok_or_else(|| anyhow!("announce({candidate}) not found"))?;

if announce.parent == best_parent && !announce.is_base() {
if not_base_announce_hash.is_some() {
tracing::warn!("Found multiple not-base announces: maybe double announcement");
} else {
not_base_announce_hash = Some(candidate);
}
} else if announce.parent == best_parent && announce.is_base() {
if base_announce_hash.replace(candidate).is_some() {
unreachable!("Two different siblings base announces is impossible");
}
}
}

match (not_base_announce_hash, base_announce_hash) {
(Some(not_base), Some(base)) => {
if announces_have_equal_outcomes(db, base, not_base) {
// if base announce has the same outcome as not-base announce, then better to use base
Ok(base)
} else {
Ok(not_base)
}
}
(Some(not_base), None) => Ok(not_base),
(None, Some(base)) => Ok(base),
(None, None) => Err(anyhow!(
"No announces with parent {best_parent} found for block {block_hash}"
)),
}
}

/// Returns announce hash, which is supposed to be best among provided announces.
pub fn best_announce(
fn best_announce(
db: &impl DBAnnouncesExt,
announces: impl IntoIterator<Item = HashOf<Announce>>,
commitment_delay_limit: u32,
limit: u32,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think here must be ancestor_depth_limit.

Current name is misleading

) -> Result<HashOf<Announce>> {
let mut announces = announces.into_iter();
let Some(first) = announces.next() else {
Expand All @@ -626,7 +719,7 @@ pub fn best_announce(

let announce_points = |mut announce_hash| -> Result<u32> {
let mut points = 0;
for _ in 0..commitment_delay_limit {
for _ in 0..limit {
let announce = db
.announce(announce_hash)
.ok_or_else(|| anyhow!("Announce {announce_hash} not found in db"))?;
Expand Down Expand Up @@ -656,7 +749,37 @@ pub fn best_announce(
}
}

Ok(best_announce_hash)
let best_announce = db
.announce(best_announce_hash)
.ok_or_else(|| anyhow!("Best announce {best_announce_hash} not found in db"))?;
Comment thread
grishasobol marked this conversation as resolved.

if best_announce.is_base() {
// we can return it without checking siblings
return Ok(best_announce_hash);
}

let Some(base_announce) =
db.find_block_base_announce_with_parent(best_announce.block_hash, best_announce.parent)?
else {
return Ok(best_announce_hash);
};

if announces_have_equal_outcomes(db, base_announce.hash, best_announce_hash) {
// if base announce has the same outcome as best announce, then better to use base
Ok(base_announce.hash)
} else {
Ok(best_announce_hash)
}
}

pub fn announces_have_equal_outcomes(
db: &impl DBAnnouncesExt,
announce1_hash: HashOf<Announce>,
announce2_hash: HashOf<Announce>,
) -> bool {
db.announce_outcome(announce1_hash)
.map(|base_outcome| Some(base_outcome) == db.announce_outcome(announce2_hash))
.unwrap_or(false)
}
Comment thread
grishasobol marked this conversation as resolved.
Outdated

#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)]
Expand All @@ -678,7 +801,7 @@ pub enum AnnounceRejectionReason {
pub enum AnnounceStatus {
#[display("Announce {_0} accepted")]
Accepted(HashOf<Announce>),
#[display("Announce {announce:?} rejected: {reason:?}")]
#[display("Announce {announce} rejected: {reason:?}")]
Rejected {
announce: Announce,
reason: AnnounceRejectionReason,
Expand Down Expand Up @@ -779,6 +902,7 @@ mod tests {
StateHashWithQueueSize,
db::*,
events::{BlockEvent, MirrorEvent, mirror::MessageQueueingRequestedEvent},
gear::StateTransition,
injected::InjectedTransaction,
mock::*,
};
Expand Down Expand Up @@ -1127,4 +1251,139 @@ mod tests {
AnnounceRejectionReason::TooManyTouchedPrograms(MAX_TOUCHED_PROGRAMS_PER_ANNOUNCE + 1)
);
}

#[test]
fn best_announce_prefers_base_sibling_with_same_outcome() {
let db = Database::memory();

let mut chain = BlockChain::mock(5);

// Block 3 already has a base announce. Add a not-base sibling with the same parent.
let base_hash = chain.block_top_announce_hash(3);
let base_announce = &chain.block_top_announce(3).announce;
let parent = base_announce.parent;
let block_hash = base_announce.block_hash;

let not_base_announce = Announce::with_default_gas(block_hash, parent);
let not_base_hash = not_base_announce.to_hash();

chain.blocks[3]
.as_prepared_mut()
.announces
.as_mut()
.unwrap()
.insert(not_base_hash);

// Both announces computed with the same (empty) outcome
chain.announces.insert(
not_base_hash,
AnnounceData {
announce: not_base_announce,
computed: Some(MockComputedAnnounceData::default()),
},
);

let chain = chain.setup(&db);

// Not-base has more points (1 vs 0), but base sibling has the same outcome,
// so best_announce should prefer the base one.
let result = best_announce(&db, [not_base_hash, base_hash], 3).unwrap();
assert_eq!(
result, base_hash,
"Should prefer base announce when sibling outcomes are the same"
);

// Also verify via best_parent_announce: block 4 should pick base at block 3 as best parent
let best_parent_hash = best_parent_announce(&db, chain.blocks[4].hash, 3).unwrap();
assert_eq!(
best_parent_hash, base_hash,
"best_parent_announce should prefer base parent with same outcome"
);
}

#[test]
fn best_announce_keeps_not_base_when_outcomes_differ() {
let db = Database::memory();

let mut chain = BlockChain::mock(5);

let base_hash = chain.block_top_announce_hash(3);
let base_announce = &chain.block_top_announce(3).announce;
let parent = base_announce.parent;
let block_hash = base_announce.block_hash;

let not_base_announce = Announce::with_default_gas(block_hash, parent);
let not_base_hash = not_base_announce.to_hash();

chain.blocks[3]
.as_prepared_mut()
.announces
.as_mut()
.unwrap()
.insert(not_base_hash);

// Not-base announce has a different outcome (non-empty)
chain.announces.insert(
not_base_hash,
AnnounceData {
announce: not_base_announce,
computed: Some(MockComputedAnnounceData {
outcome: vec![StateTransition {
actor_id: ActorId::from(1u64),
..Default::default()
}],
..Default::default()
}),
},
);

let _chain = chain.setup(&db);

// Not-base has more points AND different outcome, so it wins.
let result = best_announce(&db, [not_base_hash, base_hash], 3).unwrap();
assert_eq!(
result, not_base_hash,
"Should keep not-base announce when outcomes differ"
);
}

#[test]
fn best_announce_not_computed_keeps_not_base() {
let db = Database::memory();

let mut chain = BlockChain::mock(5);

let base_hash = chain.block_top_announce_hash(3);
let base_announce = &chain.block_top_announce(3).announce;
let parent = base_announce.parent;
let block_hash = base_announce.block_hash;

let not_base_announce = Announce::with_default_gas(block_hash, parent);
let not_base_hash = not_base_announce.to_hash();

chain.blocks[3]
.as_prepared_mut()
.announces
.as_mut()
.unwrap()
.insert(not_base_hash);

// Not-base announce is NOT computed (computed: None)
chain.announces.insert(
not_base_hash,
AnnounceData {
announce: not_base_announce,
computed: None,
},
);

let _chain = chain.setup(&db);

// Not-base has more points; sibling check returns NotComputed, so not-base wins.
let result = best_announce(&db, [not_base_hash, base_hash], 3).unwrap();
assert_eq!(
result, not_base_hash,
"Should keep not-base announce when sibling is not computed"
);
}
}
2 changes: 1 addition & 1 deletion ethexe/consensus/src/connect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ impl ConsensusService for ConnectService {
self.process_announce_from_producer(announce, *producer)?;
self.state = State::WaitingForBlock;
} else {
tracing::warn!("Receive unexpected {announce:?}, save to pending announces");
tracing::warn!("Receive unexpected {announce}, save to pending announces");
self.pending_announces
.push((sender, announce.block_hash), announce);
}
Expand Down
Loading