From 41a85ff5b339e5ad6e66399a499dbc66991c1687 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 6 Feb 2026 13:47:55 +0700 Subject: [PATCH 01/59] draft implementation --- ethexe/common/src/db.rs | 7 +- ethexe/common/src/injected.rs | 73 +- ethexe/common/src/mock.rs | 21 +- ethexe/compute/src/compute.rs | 7 +- ethexe/consensus/src/lib.rs | 6 +- ethexe/consensus/src/validator/producer.rs | 26 +- ethexe/db/src/database.rs | 23 +- ethexe/network/src/gossipsub.rs | 10 +- ethexe/network/src/lib.rs | 14 +- ethexe/network/src/validator/topic.rs | 765 +++++++++++---------- ethexe/rpc/src/apis/injected.rs | 37 +- ethexe/rpc/src/lib.rs | 33 +- ethexe/rpc/src/tests.rs | 46 +- ethexe/service/src/lib.rs | 12 +- ethexe/service/src/tests/mod.rs | 10 +- ethexe/service/src/tests/utils/events.rs | 6 +- 16 files changed, 635 insertions(+), 461 deletions(-) diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index fb4f090a663..d8eb3eb76e1 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -23,7 +23,7 @@ use crate::{ Schedule, SimpleBlockData, ValidatorsVec, events::BlockEvent, gear::StateTransition, - injected::{InjectedTransaction, SignedInjectedTransaction}, + injected::{InjectedTransaction, Promise, SignedInjectedTransaction}, }; use alloc::{ collections::{BTreeSet, VecDeque}, @@ -134,11 +134,16 @@ pub trait InjectedStorageRO { &self, hash: HashOf, ) -> Option; + + /// Returns the promise by its transaction hash. + fn promise(&self, hash: HashOf) -> Option; } #[auto_impl::auto_impl(&)] pub trait InjectedStorageRW: InjectedStorageRO { fn set_injected_transaction(&self, tx: SignedInjectedTransaction); + + fn set_promise(&self, promise: Promise); } #[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Eq, Hash)] diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index 4b42a6ce6c1..9da5e1a43df 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -16,11 +16,20 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::{Address, HashOf, ToDigest, ecdsa::SignedMessage}; -use alloc::string::{String, ToString}; +use crate::{Address, Announce, HashOf, ToDigest, ecdsa::SignedMessage}; +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; use core::hash::Hash; use gear_core::rpc::ReplyInfo; use gprimitives::{ActorId, H256, MessageId}; +use gsigner::Signature; +#[cfg(feature = "std")] +use gsigner::{ + PrivateKey, PublicKey, SignerError, + secp256k1::{Secp256k1SignerExt, Signer}, +}; use parity_scale_codec::{Decode, Encode}; use sha3::{Digest, Keccak256}; use sp_core::Bytes; @@ -149,3 +158,63 @@ impl ToDigest for Promise { hasher.update(value.to_be_bytes()); } } + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct PromisesNetworkBundle { + /// The hash of [`Announce`] for which promises was created. + pub announce: HashOf, + /// The hashes of transactions with signatures + pub promises: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct CompactSignedPromise { + /// The hash of transaction, for which promise was created. + tx_hash: HashOf, + /// + address: Address, + /// The signature over the [`Promise`] for `tx_hash`. + signature: Signature, +} + +#[cfg(feature = "std")] +impl CompactSignedPromise { + pub fn create( + signer: &Signer, + public_key: PublicKey, + promise: Promise, + ) -> Result { + let tx_hash = promise.tx_hash; + let (address, signature) = signer + .signed_message(public_key, promise, None) + .map(|message| (message.address(), message.into_parts().1))?; + + Ok(Self { + tx_hash, + address, + signature, + }) + } + + pub fn create_from_private_key( + private_key: &PrivateKey, + promise: Promise, + ) -> Result { + let tx_hash = promise.tx_hash; + let signature = Signature::create(private_key, promise)?; + let address = private_key.public_key().to_address(); + Ok(Self { + tx_hash, + signature, + address, + }) + } + + pub fn tx_hash(&self) -> HashOf { + self.tx_hash + } + + pub fn into_parts(self) -> (HashOf, Address, Signature) { + (self.tx_hash, self.address, self.signature) + } +} diff --git a/ethexe/common/src/mock.rs b/ethexe/common/src/mock.rs index a133ecde58c..d3183388ab5 100644 --- a/ethexe/common/src/mock.rs +++ b/ethexe/common/src/mock.rs @@ -26,10 +26,14 @@ use crate::{ ecdsa::{PrivateKey, SignedMessage}, events::BlockEvent, gear::{BatchCommitment, ChainCommitment, CodeCommitment, Message, StateTransition}, - injected::{AddressedInjectedTransaction, InjectedTransaction}, + injected::{AddressedInjectedTransaction, InjectedTransaction, Promise}, }; use alloc::{collections::BTreeMap, vec}; -use gear_core::code::{CodeMetadata, InstrumentedCode}; +use gear_core::{ + code::{CodeMetadata, InstrumentedCode}, + message::{ReplyCode, SuccessReplyReason}, + rpc::ReplyInfo, +}; use gprimitives::{CodeId, H256}; use itertools::Itertools; use std::collections::{BTreeSet, VecDeque}; @@ -650,3 +654,16 @@ impl Mock> for ComputedAnnounce { } } } + +impl Mock> for Promise { + fn mock(tx_hash: HashOf) -> Self { + Self { + tx_hash, + reply: ReplyInfo { + payload: vec![], + value: 0, + code: ReplyCode::Success(SuccessReplyReason::Manual), + }, + } + } +} diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 91de6be4ce4..7f84fcf9d66 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -21,7 +21,7 @@ use ethexe_common::{ Announce, ComputedAnnounce, HashOf, SimpleBlockData, db::{ AnnounceStorageRO, AnnounceStorageRW, BlockMetaStorageRO, CodesStorageRW, - LatestDataStorageRO, LatestDataStorageRW, OnChainStorageRO, + InjectedStorageRW, LatestDataStorageRO, LatestDataStorageRW, OnChainStorageRO, }, events::BlockEvent, }; @@ -201,6 +201,11 @@ impl ComputeSubService

{ }) .ok_or(ComputeError::LatestDataNotFound)?; + // TODO: remove in this PR ComputedAnnounce struct. + promises.clone().into_iter().for_each(|promise| { + db.set_promise(promise); + }); + Ok(ComputedAnnounce { announce_hash, promises, diff --git a/ethexe/consensus/src/lib.rs b/ethexe/consensus/src/lib.rs index 1bd348bdec2..dddaadd2500 100644 --- a/ethexe/consensus/src/lib.rs +++ b/ethexe/consensus/src/lib.rs @@ -36,7 +36,9 @@ use anyhow::Result; use ethexe_common::{ Announce, ComputedAnnounce, Digest, HashOf, SimpleBlockData, consensus::{BatchCommitmentValidationReply, VerifiedAnnounce, VerifiedValidationRequest}, - injected::{SignedInjectedTransaction, SignedPromise}, + injected::{ + PromisesNetworkBundle, SignedInjectedTransaction, + }, network::{AnnouncesRequest, AnnouncesResponse, SignedValidatorMessage}, }; use futures::{Stream, stream::FusedStream}; @@ -124,5 +126,5 @@ pub enum ConsensusEvent { Warning(String), /// Promises for [`ethexe_common::injected::InjectedTransaction`]s execution in some announce. #[from] - Promises(Vec), + Promises(PromisesNetworkBundle), } diff --git a/ethexe/consensus/src/validator/producer.rs b/ethexe/consensus/src/validator/producer.rs index 34091430069..419a79f33ce 100644 --- a/ethexe/consensus/src/validator/producer.rs +++ b/ethexe/consensus/src/validator/producer.rs @@ -24,11 +24,14 @@ use crate::{ announces::{self, DBAnnouncesExt}, validator::DefaultProcessing, }; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{ Result, anyhow}; use derive_more::{Debug, Display}; use ethexe_common::{ - Announce, ComputedAnnounce, HashOf, SimpleBlockData, ValidatorsVec, db::BlockMetaStorageRO, - gear::BatchCommitment, network::ValidatorMessage, + Announce, ComputedAnnounce, HashOf, SimpleBlockData, ValidatorsVec, + db::BlockMetaStorageRO, + gear::BatchCommitment, + injected::{CompactSignedPromise, PromisesNetworkBundle}, + network::ValidatorMessage, }; use ethexe_service_utils::Timer; use futures::{FutureExt, future::BoxFuture}; @@ -82,17 +85,22 @@ impl StateHandler for Producer { if *expected == computed_data.announce_hash => { if !computed_data.promises.is_empty() { - let signed_promises = computed_data + let promises = computed_data .promises .into_iter() .map(|promise| { - self.ctx - .sign_message(promise) - .context("producer: failed to sign promise") + CompactSignedPromise::create( + &self.ctx.core.signer, + self.ctx.core.pub_key, + promise, + ) }) .collect::>()?; - - self.ctx.output(ConsensusEvent::Promises(signed_promises)); + let bundle = PromisesNetworkBundle { + announce: computed_data.announce_hash, + promises, + }; + self.ctx.output(ConsensusEvent::Promises(bundle)); } // Aggregate commitment for the block and use `announce_hash` as head for chain commitment. diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index 6813e5fdee6..b89f3190158 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -33,7 +33,7 @@ use ethexe_common::{ }, events::BlockEvent, gear::StateTransition, - injected::{InjectedTransaction, SignedInjectedTransaction}, + injected::{InjectedTransaction, Promise, SignedInjectedTransaction}, }; use ethexe_runtime_common::state::{ @@ -76,7 +76,9 @@ enum Key { Timelines = 15, // TODO kuzmindev: temporal solution - must move into block meta or something else. - LatestEraValidatorsCommitted(H256), + LatestEraValidatorsCommitted(H256) = 16, + + Promise(HashOf) = 17, } impl Key { @@ -103,7 +105,9 @@ impl Key { | Self::AnnounceSchedule(hash) | Self::AnnounceMeta(hash) => [prefix.as_ref(), hash.inner().as_ref()].concat(), - Self::InjectedTransaction(hash) => [prefix.as_ref(), hash.inner().as_ref()].concat(), + Self::InjectedTransaction(hash) | Self::Promise(hash) => { + [prefix.as_ref(), hash.inner().as_ref()].concat() + } Self::ProgramToCodeId(program_id) => [prefix.as_ref(), program_id.as_ref()].concat(), @@ -619,6 +623,12 @@ impl InjectedStorageRO for Database { .expect("Failed to decode data into `SignedInjectedTransaction`") }) } + + fn promise(&self, tx_hash: HashOf) -> Option { + self.kv.get(&Key::Promise(tx_hash).to_bytes()).map(|data| { + Promise::decode(&mut data.as_slice()).expect("Failed to decode data into Promise") + }) + } } impl InjectedStorageRW for Database { @@ -629,6 +639,13 @@ impl InjectedStorageRW for Database { self.kv .put(&Key::InjectedTransaction(tx_hash).to_bytes(), tx.encode()); } + + fn set_promise(&self, promise: Promise) { + tracing::trace!(promise_tx_hash = ?promise.tx_hash, "Set promise for injected transaction"); + + self.kv + .put(&Key::Promise(promise.tx_hash).to_bytes(), promise.encode()) + } } impl AnnounceStorageRO for Database { diff --git a/ethexe/network/src/gossipsub.rs b/ethexe/network/src/gossipsub.rs index 93677ee32a2..bf1f318cc7b 100644 --- a/ethexe/network/src/gossipsub.rs +++ b/ethexe/network/src/gossipsub.rs @@ -23,7 +23,7 @@ use crate::{ peer_score, }; use anyhow::anyhow; -use ethexe_common::{Address, injected::SignedPromise, network::SignedValidatorMessage}; +use ethexe_common::{Address, injected::PromisesNetworkBundle, network::SignedValidatorMessage}; use libp2p::{ core::{Endpoint, transport::PortUse}, gossipsub, @@ -44,21 +44,21 @@ use std::{ pub enum Message { // TODO: rename to `Validators` Commitments(SignedValidatorMessage), - Promise(SignedPromise), + PromisesBundle(PromisesNetworkBundle), } impl Message { fn topic_hash(&self, behaviour: &Behaviour) -> TopicHash { match self { Message::Commitments(_) => behaviour.commitments_topic.hash(), - Message::Promise(_) => behaviour.promises_topic.hash(), + Message::PromisesBundle(_) => behaviour.promises_topic.hash(), } } fn encode(&self) -> Vec { match self { Message::Commitments(message) => message.encode(), - Message::Promise(message) => message.encode(), + Message::PromisesBundle(message) => message.encode(), } } } @@ -177,7 +177,7 @@ impl Behaviour { let res = if topic == self.commitments_topic.hash() { SignedValidatorMessage::decode(&mut &data[..]).map(Message::Commitments) } else if topic == self.promises_topic.hash() { - SignedPromise::decode(&mut &data[..]).map(Message::Promise) + PromisesNetworkBundle::decode(&mut &data[..]).map(Message::PromisesBundle) } else { unreachable!("topic we never subscribed to: {topic:?}"); }; diff --git a/ethexe/network/src/lib.rs b/ethexe/network/src/lib.rs index 33308f60930..d3b51f20f9e 100644 --- a/ethexe/network/src/lib.rs +++ b/ethexe/network/src/lib.rs @@ -39,7 +39,7 @@ use anyhow::{Context, anyhow}; use ethexe_common::{ Address, BlockHeader, ValidatorsVec, ecdsa::PublicKey, - injected::{AddressedInjectedTransaction, SignedPromise}, + injected::{AddressedInjectedTransaction, PromisesNetworkBundle}, network::{SignedValidatorMessage, VerifiedValidatorMessage}, }; use futures::{Stream, future::Either, ready, stream::FusedStream}; @@ -80,7 +80,7 @@ impl NetworkServiceDatabase for T where T: DbSyncDatabase + ValidatorDatabase pub enum NetworkEvent { // gossipsub ValidatorMessage(VerifiedValidatorMessage), - PromiseMessage(SignedPromise), + PromisesBundle(PromisesNetworkBundle), // validator-identity ValidatorIdentityUpdated(Address), // injected-tx @@ -493,11 +493,11 @@ impl NetworkService { .verify_validator_message(source, message); (acceptance, message.map(NetworkEvent::ValidatorMessage)) } - gossipsub::Message::Promise(promise) => { + gossipsub::Message::PromisesBundle(bundle) => { // FIXME: previous era validators are ignored let (acceptance, promise) = - self.validator_topic.verify_promise(source, promise); - (acceptance, promise.map(NetworkEvent::PromiseMessage)) + self.validator_topic.verify_promises_bundle(source, bundle); + (acceptance, promise.map(NetworkEvent::PromisesBundle)) } }) } @@ -572,8 +572,8 @@ impl NetworkService { .send_transaction(behaviour.validator_discovery.identities(), data) } - pub fn publish_promise(&mut self, promise: SignedPromise) { - self.swarm.behaviour_mut().gossipsub.publish(promise) + pub fn publish_promises_bundle(&mut self, bundle: PromisesNetworkBundle) { + self.swarm.behaviour_mut().gossipsub.publish(bundle) } } diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index 3738239c541..c2b373c37c0 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -25,7 +25,7 @@ use crate::{ }; use ethexe_common::{ Address, HashOf, - injected::{InjectedTransaction, SignedPromise}, + injected::{InjectedTransaction, PromisesNetworkBundle}, network::VerifiedValidatorMessage, }; use lru::LruCache; @@ -263,31 +263,33 @@ impl ValidatorTopic { } } - fn inner_verify_promise( + fn inner_verify_promises_bundle( &self, _source: PeerId, - promise: SignedPromise, - ) -> Result { - let address = promise.address(); - let tx_hash = promise.data().tx_hash; + bundle: PromisesNetworkBundle, + ) -> Result { + // TODO: uncomment this - if !self.snapshot.contains(address) { - return Err(VerifyPromiseError::UnknownValidator { address, tx_hash }); - } + // let address = promise.address(); + // let tx_hash = promise.data().tx_hash; + + // if !self.snapshot.contains(address) { + // return Err(VerifyPromiseError::UnknownValidator { address, tx_hash }); + // } - Ok(promise) + Ok(bundle) } // FIXME: messages from previous era validators are ignored - pub fn verify_promise( + pub fn verify_promises_bundle( &self, source: PeerId, - promise: SignedPromise, - ) -> (MessageAcceptance, Option) { - match self.inner_verify_promise(source, promise) { - Ok(promise) => (MessageAcceptance::Accept, Some(promise)), + bundle: PromisesNetworkBundle, + ) -> (MessageAcceptance, Option) { + match self.inner_verify_promises_bundle(source, bundle) { + Ok(bundle) => (MessageAcceptance::Accept, Some(bundle)), Err(err) => { - log::trace!("failed to verify promise: {err}"); + log::trace!("failed to verify promises bundle: {err}"); (MessageAcceptance::Ignore, None) } } @@ -299,368 +301,369 @@ impl ValidatorTopic { } } -#[cfg(test)] -mod tests { - use super::*; - use assert_matches::assert_matches; - use ethexe_common::{ - Announce, - gear_core::{message::ReplyCode, rpc::ReplyInfo}, - injected::Promise, - mock::Mock, - network::{SignedValidatorMessage, ValidatorMessage}, - }; - use gsigner::secp256k1::{Secp256k1SignerExt, Signer}; - use nonempty::{NonEmpty, nonempty}; - - const CHAIN_HEAD_ERA: u64 = 10; - - fn new_snapshot( - current_era_index: u64, - current_validators: NonEmpty

, - ) -> Arc { - Arc::new(ValidatorListSnapshot { - current_era_index, - current_validators: current_validators.into(), - next_validators: None, - }) - } - - fn new_topic(validators: NonEmpty
) -> ValidatorTopic { - ValidatorTopic::new( - peer_score::Handle::new_test(), - new_snapshot(CHAIN_HEAD_ERA, validators), - ) - } - - fn new_validator_message(era_index: u64) -> VerifiedValidatorMessage { - let signer = Signer::memory(); - let pub_key = signer.generate().unwrap(); - - signer - .signed_data( - pub_key, - ValidatorMessage { - era_index, - payload: Announce::mock(()), - }, - None, - ) - .map(SignedValidatorMessage::from) - .unwrap() - .into_verified() - } - - fn signed_promise() -> SignedPromise { - let signer = Signer::memory(); - let pub_key = signer.generate().unwrap(); - let promise = Promise { - tx_hash: Default::default(), - reply: ReplyInfo { - payload: vec![], - value: 0, - code: ReplyCode::Unsupported, - }, - }; - - signer.signed_message(pub_key, promise, None).unwrap() - } - - #[test] - fn too_old_era() { - let bob_message = new_validator_message(CHAIN_HEAD_ERA - 2); - let mut alice = new_topic(nonempty![bob_message.address()]); - - let err = alice - .inner_verify_validator_message(&bob_message) - .unwrap_err() - .unwrap_reject(); - assert_eq!( - err, - VerificationRejectReason::TooOldEra { - expected_era: CHAIN_HEAD_ERA, - received_era: CHAIN_HEAD_ERA - 2 - } - ); - - let bob_source = PeerId::random(); - let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); - assert_matches!(acceptance, MessageAcceptance::Reject); - assert_eq!(verified_msg, None); - assert_eq!(alice.cached_messages.len(), 0); - assert_eq!(alice.next_message(), None); - } - - #[test] - fn old_era() { - let bob_message = new_validator_message(CHAIN_HEAD_ERA - 1); - let mut alice = new_topic(nonempty![bob_message.address()]); - - let err = alice - .inner_verify_validator_message(&bob_message) - .unwrap_err() - .unwrap_ignore(); - assert_eq!( - err, - VerificationIgnoreReason::OldEra { - expected_era: CHAIN_HEAD_ERA, - received_era: CHAIN_HEAD_ERA - 1 - } - ); - - let bob_source = PeerId::random(); - let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); - assert_matches!(acceptance, MessageAcceptance::Ignore); - assert_eq!(verified_msg, None); - assert_eq!(alice.cached_messages.len(), 0); - assert_eq!(alice.next_message(), None); - } - - #[test] - fn too_new_era() { - let bob_message = new_validator_message(CHAIN_HEAD_ERA + 2); - let mut alice = new_topic(nonempty![bob_message.address()]); - - let err = alice - .inner_verify_validator_message(&bob_message) - .unwrap_err() - .unwrap_reject(); - assert_eq!( - err, - VerificationRejectReason::TooNewEra { - expected_era: CHAIN_HEAD_ERA, - received_era: CHAIN_HEAD_ERA + 2 - } - ); - - let bob_source = PeerId::random(); - let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); - assert_matches!(acceptance, MessageAcceptance::Reject); - assert_eq!(verified_msg, None); - assert_eq!(alice.cached_messages.len(), 0); - } - - #[test] - fn new_era() { - const BOB_BLOCK_ERA: u64 = CHAIN_HEAD_ERA + 1; - - let bob_message = new_validator_message(BOB_BLOCK_ERA); - let snapshot = ValidatorListSnapshot { - current_era_index: CHAIN_HEAD_ERA, - current_validators: Default::default(), - next_validators: Some(nonempty![bob_message.address()].into()), - }; - let mut alice = ValidatorTopic::new(peer_score::Handle::new_test(), Arc::new(snapshot)); - - let err = alice - .inner_verify_validator_message(&bob_message) - .unwrap_err() - .unwrap_cache(); - assert_eq!( - err, - VerificationCacheReason::NewEra { - expected_era: CHAIN_HEAD_ERA, - received_era: CHAIN_HEAD_ERA + 1 - } - ); - - let bob_source = PeerId::random(); - let (acceptance, verified_msg) = - alice.verify_validator_message(bob_source, bob_message.clone()); - assert_matches!(acceptance, MessageAcceptance::Ignore); - assert_eq!(verified_msg, None); - assert_eq!(alice.cached_messages.len(), 1); - - let snapshot = new_snapshot(BOB_BLOCK_ERA, nonempty![bob_message.address()]); - alice.on_new_snapshot(snapshot); - - assert_eq!(alice.next_message(), Some(bob_message)); - } - - #[test] - fn current_era_address_is_not_validator() { - let mut alice = new_topic(nonempty![Address::default()]); - let bob_message = new_validator_message(CHAIN_HEAD_ERA); - - let err = alice - .inner_verify_validator_message(&bob_message) - .unwrap_err() - .unwrap_reject(); - assert_eq!( - err, - VerificationRejectReason::AddressIsNotValidator { - address: bob_message.address() - } - ); - - let bob_source = PeerId::random(); - let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); - assert_matches!(acceptance, MessageAcceptance::Reject); - assert_eq!(verified_msg, None); - assert_eq!(alice.cached_messages.len(), 0); - assert_eq!(alice.next_message(), None); - } - - #[test] - fn next_era_address_is_not_validator() { - let mut alice = new_topic(nonempty![Address::default()]); - let bob_message = new_validator_message(CHAIN_HEAD_ERA + 1); - - let err = alice - .inner_verify_validator_message(&bob_message) - .unwrap_err() - .unwrap_reject(); - assert_eq!( - err, - VerificationRejectReason::AddressIsNotValidator { - address: bob_message.address() - } - ); - - let bob_source = PeerId::random(); - let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); - assert_matches!(acceptance, MessageAcceptance::Reject); - assert_eq!(verified_msg, None); - assert_eq!(alice.cached_messages.len(), 0); - assert_eq!(alice.next_message(), None); - } - - #[test] - fn new_era_address_is_not_validator() { - let bob_message = new_validator_message(CHAIN_HEAD_ERA); - let charlie_message = new_validator_message(CHAIN_HEAD_ERA + 1); - - let mut alice = new_topic(nonempty![Default::default()]); - - for message in [bob_message, charlie_message] { - let err = alice - .inner_verify_validator_message(&message) - .unwrap_err() - .unwrap_reject(); - assert_eq!( - err, - VerificationRejectReason::AddressIsNotValidator { - address: message.address() - } - ); - - let bob_source = PeerId::random(); - let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, message); - assert_matches!(acceptance, MessageAcceptance::Reject); - assert_eq!(verified_msg, None); - assert_eq!(alice.cached_messages.len(), 0); - assert_eq!(alice.next_message(), None); - } - } - - #[test] - fn success() { - let bob_message = new_validator_message(CHAIN_HEAD_ERA); - let mut alice = new_topic(nonempty![bob_message.address()]); - - alice.inner_verify_validator_message(&bob_message).unwrap(); - - let bob_source = PeerId::random(); - let (acceptance, verified_msg) = - alice.verify_validator_message(bob_source, bob_message.clone()); - assert_matches!(acceptance, MessageAcceptance::Accept); - assert_eq!(verified_msg, Some(bob_message)); - } - - #[test] - fn next_validators_arrive_later() { - const NEXT_ERA: u64 = CHAIN_HEAD_ERA + 1; - - // current set validators - let bob_message = new_validator_message(CHAIN_HEAD_ERA); - let charlie_message = new_validator_message(CHAIN_HEAD_ERA); - // new validator in next set - let dave_message = new_validator_message(NEXT_ERA); - - let bob_source = PeerId::random(); - let charlie_source = PeerId::random(); - let dave_source = PeerId::random(); - - let mut alice = new_topic(nonempty![bob_message.address(), charlie_message.address()]); - - let (bob_acceptance, bob_verified_msg) = - alice.verify_validator_message(bob_source, bob_message.clone()); - assert_matches!(bob_acceptance, MessageAcceptance::Accept); - assert_eq!(bob_verified_msg, Some(bob_message.clone())); - - let (charlie_acceptance, charlie_verified_msg) = - alice.verify_validator_message(charlie_source, charlie_message.clone()); - assert_matches!(charlie_acceptance, MessageAcceptance::Accept); - assert_eq!(charlie_verified_msg, Some(charlie_message.clone())); - - // we have no next validators yet, so the message should be rejected - let (dave_acceptance, dave_verified_msg) = - alice.verify_validator_message(dave_source, dave_message.clone()); - assert_matches!(dave_acceptance, MessageAcceptance::Reject); - assert!(dave_verified_msg.is_none()); - assert_eq!(alice.cached_messages.len(), 0); - - // Dave now is in the next validator set - let snapshot = ValidatorListSnapshot { - current_era_index: CHAIN_HEAD_ERA, - current_validators: Default::default(), - next_validators: Some(nonempty![dave_message.address()].into()), - }; - alice.on_new_snapshot(Arc::new(snapshot)); - - // Dave's message is cached - let (dave_acceptance, dave_verified_msg) = - alice.verify_validator_message(dave_source, dave_message.clone()); - assert_matches!(dave_acceptance, MessageAcceptance::Ignore); - assert!(dave_verified_msg.is_none()); - assert_eq!(alice.cached_messages.len(), 1); - - // Dave's message now is in the current validator set - let snapshot = ValidatorListSnapshot { - current_era_index: NEXT_ERA, - current_validators: nonempty![dave_message.address()].into(), - next_validators: None, - }; - alice.on_new_snapshot(Arc::new(snapshot)); - - let dave_verified_msg = alice.next_message().unwrap(); - assert_eq!(dave_verified_msg, dave_message); - } - - #[test] - fn verify_promise_unknown_validator() { - let topic = new_topic(nonempty![Address::default()]); - let promise = signed_promise(); - let peer_id = PeerId::random(); - - let err = topic - .inner_verify_promise(peer_id, promise.clone()) - .unwrap_err(); - assert_eq!( - err, - VerifyPromiseError::UnknownValidator { - address: promise.address(), - tx_hash: promise.data().tx_hash, - } - ); - - let (acceptance, promise) = topic.verify_promise(peer_id, promise); - assert_matches!(acceptance, MessageAcceptance::Ignore); - assert_eq!(promise, None); - } - - #[tokio::test] - async fn verify_promise_ok() { - let promise = signed_promise(); - let topic = new_topic(nonempty![promise.address()]); - let peer_id = PeerId::random(); - - topic - .inner_verify_promise(peer_id, promise.clone()) - .unwrap(); - - let (acceptance, returned_promise) = topic.verify_promise(peer_id, promise.clone()); - assert_matches!(acceptance, MessageAcceptance::Accept); - assert_eq!(returned_promise, Some(promise)); - } -} +// #[cfg(test)] +// mod tests { +// use super::*; +// use assert_matches::assert_matches; +// use ethexe_common::{ +// Announce, +// gear_core::{message::ReplyCode, rpc::ReplyInfo}, +// injected::Promise, +// mock::Mock, +// network::{SignedValidatorMessage, ValidatorMessage}, +// }; +// use gsigner::secp256k1::{Secp256k1SignerExt, Signer}; +// use nonempty::{NonEmpty, nonempty}; + +// const CHAIN_HEAD_ERA: u64 = 10; + +// fn new_snapshot( +// current_era_index: u64, +// current_validators: NonEmpty
, +// ) -> Arc { +// Arc::new(ValidatorListSnapshot { +// current_era_index, +// current_validators: current_validators.into(), +// next_validators: None, +// }) +// } + +// fn new_topic(validators: NonEmpty
) -> ValidatorTopic { +// ValidatorTopic::new( +// peer_score::Handle::new_test(), +// new_snapshot(CHAIN_HEAD_ERA, validators), +// ) +// } + +// fn new_validator_message(era_index: u64) -> VerifiedValidatorMessage { +// let signer = Signer::memory(); +// let pub_key = signer.generate().unwrap(); + +// signer +// .signed_data( +// pub_key, +// ValidatorMessage { +// era_index, +// payload: Announce::mock(()), +// }, +// None, +// ) +// .map(SignedValidatorMessage::from) +// .unwrap() +// .into_verified() +// } + +// fn signed_promise() -> SignedPromise { +// let signer = Signer::memory(); +// let pub_key = signer.generate().unwrap(); +// let promise = Promise { +// tx_hash: Default::default(), +// reply: ReplyInfo { +// payload: vec![], +// value: 0, +// code: ReplyCode::Unsupported, +// }, +// }; + +// signer.signed_message(pub_key, promise, None).unwrap() +// } + +// #[test] +// fn too_old_era() { +// let bob_message = new_validator_message(CHAIN_HEAD_ERA - 2); +// let mut alice = new_topic(nonempty![bob_message.address()]); + +// let err = alice +// .inner_verify_validator_message(&bob_message) +// .unwrap_err() +// .unwrap_reject(); +// assert_eq!( +// err, +// VerificationRejectReason::TooOldEra { +// expected_era: CHAIN_HEAD_ERA, +// received_era: CHAIN_HEAD_ERA - 2 +// } +// ); + +// let bob_source = PeerId::random(); +// let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); +// assert_matches!(acceptance, MessageAcceptance::Reject); +// assert_eq!(verified_msg, None); +// assert_eq!(alice.cached_messages.len(), 0); +// assert_eq!(alice.next_message(), None); +// } + +// #[test] +// fn old_era() { +// let bob_message = new_validator_message(CHAIN_HEAD_ERA - 1); +// let mut alice = new_topic(nonempty![bob_message.address()]); + +// let err = alice +// .inner_verify_validator_message(&bob_message) +// .unwrap_err() +// .unwrap_ignore(); +// assert_eq!( +// err, +// VerificationIgnoreReason::OldEra { +// expected_era: CHAIN_HEAD_ERA, +// received_era: CHAIN_HEAD_ERA - 1 +// } +// ); + +// let bob_source = PeerId::random(); +// let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); +// assert_matches!(acceptance, MessageAcceptance::Ignore); +// assert_eq!(verified_msg, None); +// assert_eq!(alice.cached_messages.len(), 0); +// assert_eq!(alice.next_message(), None); +// } + +// #[test] +// fn too_new_era() { +// let bob_message = new_validator_message(CHAIN_HEAD_ERA + 2); +// let mut alice = new_topic(nonempty![bob_message.address()]); + +// let err = alice +// .inner_verify_validator_message(&bob_message) +// .unwrap_err() +// .unwrap_reject(); +// assert_eq!( +// err, +// VerificationRejectReason::TooNewEra { +// expected_era: CHAIN_HEAD_ERA, +// received_era: CHAIN_HEAD_ERA + 2 +// } +// ); + +// let bob_source = PeerId::random(); +// let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); +// assert_matches!(acceptance, MessageAcceptance::Reject); +// assert_eq!(verified_msg, None); +// assert_eq!(alice.cached_messages.len(), 0); +// } + +// #[test] +// fn new_era() { +// const BOB_BLOCK_ERA: u64 = CHAIN_HEAD_ERA + 1; + +// let bob_message = new_validator_message(BOB_BLOCK_ERA); +// let snapshot = ValidatorListSnapshot { +// current_era_index: CHAIN_HEAD_ERA, +// current_validators: Default::default(), +// next_validators: Some(nonempty![bob_message.address()].into()), +// }; +// let mut alice = ValidatorTopic::new(peer_score::Handle::new_test(), Arc::new(snapshot)); + +// let err = alice +// .inner_verify_validator_message(&bob_message) +// .unwrap_err() +// .unwrap_cache(); +// assert_eq!( +// err, +// VerificationCacheReason::NewEra { +// expected_era: CHAIN_HEAD_ERA, +// received_era: CHAIN_HEAD_ERA + 1 +// } +// ); + +// let bob_source = PeerId::random(); +// let (acceptance, verified_msg) = +// alice.verify_validator_message(bob_source, bob_message.clone()); +// assert_matches!(acceptance, MessageAcceptance::Ignore); +// assert_eq!(verified_msg, None); +// assert_eq!(alice.cached_messages.len(), 1); + +// let snapshot = new_snapshot(BOB_BLOCK_ERA, nonempty![bob_message.address()]); +// alice.on_new_snapshot(snapshot); + +// assert_eq!(alice.next_message(), Some(bob_message)); +// } + +// #[test] +// fn current_era_address_is_not_validator() { +// let mut alice = new_topic(nonempty![Address::default()]); +// let bob_message = new_validator_message(CHAIN_HEAD_ERA); + +// let err = alice +// .inner_verify_validator_message(&bob_message) +// .unwrap_err() +// .unwrap_reject(); +// assert_eq!( +// err, +// VerificationRejectReason::AddressIsNotValidator { +// address: bob_message.address() +// } +// ); + +// let bob_source = PeerId::random(); +// let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); +// assert_matches!(acceptance, MessageAcceptance::Reject); +// assert_eq!(verified_msg, None); +// assert_eq!(alice.cached_messages.len(), 0); +// assert_eq!(alice.next_message(), None); +// } + +// #[test] +// fn next_era_address_is_not_validator() { +// let mut alice = new_topic(nonempty![Address::default()]); +// let bob_message = new_validator_message(CHAIN_HEAD_ERA + 1); + +// let err = alice +// .inner_verify_validator_message(&bob_message) +// .unwrap_err() +// .unwrap_reject(); +// assert_eq!( +// err, +// VerificationRejectReason::AddressIsNotValidator { +// address: bob_message.address() +// } +// ); + +// let bob_source = PeerId::random(); +// let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); +// assert_matches!(acceptance, MessageAcceptance::Reject); +// assert_eq!(verified_msg, None); +// assert_eq!(alice.cached_messages.len(), 0); +// assert_eq!(alice.next_message(), None); +// } + +// #[test] +// fn new_era_address_is_not_validator() { +// let bob_message = new_validator_message(CHAIN_HEAD_ERA); +// let charlie_message = new_validator_message(CHAIN_HEAD_ERA + 1); + +// let mut alice = new_topic(nonempty![Default::default()]); + +// for message in [bob_message, charlie_message] { +// let err = alice +// .inner_verify_validator_message(&message) +// .unwrap_err() +// .unwrap_reject(); +// assert_eq!( +// err, +// VerificationRejectReason::AddressIsNotValidator { +// address: message.address() +// } +// ); + +// let bob_source = PeerId::random(); +// let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, message); +// assert_matches!(acceptance, MessageAcceptance::Reject); +// assert_eq!(verified_msg, None); +// assert_eq!(alice.cached_messages.len(), 0); +// assert_eq!(alice.next_message(), None); +// } +// } + +// #[test] +// fn success() { +// let bob_message = new_validator_message(CHAIN_HEAD_ERA); +// let mut alice = new_topic(nonempty![bob_message.address()]); + +// alice.inner_verify_validator_message(&bob_message).unwrap(); + +// let bob_source = PeerId::random(); +// let (acceptance, verified_msg) = +// alice.verify_validator_message(bob_source, bob_message.clone()); +// assert_matches!(acceptance, MessageAcceptance::Accept); +// assert_eq!(verified_msg, Some(bob_message)); +// } + +// #[test] +// fn next_validators_arrive_later() { +// const NEXT_ERA: u64 = CHAIN_HEAD_ERA + 1; + +// // current set validators +// let bob_message = new_validator_message(CHAIN_HEAD_ERA); +// let charlie_message = new_validator_message(CHAIN_HEAD_ERA); +// // new validator in next set +// let dave_message = new_validator_message(NEXT_ERA); + +// let bob_source = PeerId::random(); +// let charlie_source = PeerId::random(); +// let dave_source = PeerId::random(); + +// let mut alice = new_topic(nonempty![bob_message.address(), charlie_message.address()]); + +// let (bob_acceptance, bob_verified_msg) = +// alice.verify_validator_message(bob_source, bob_message.clone()); +// assert_matches!(bob_acceptance, MessageAcceptance::Accept); +// assert_eq!(bob_verified_msg, Some(bob_message.clone())); + +// let (charlie_acceptance, charlie_verified_msg) = +// alice.verify_validator_message(charlie_source, charlie_message.clone()); +// assert_matches!(charlie_acceptance, MessageAcceptance::Accept); +// assert_eq!(charlie_verified_msg, Some(charlie_message.clone())); + +// // we have no next validators yet, so the message should be rejected +// let (dave_acceptance, dave_verified_msg) = +// alice.verify_validator_message(dave_source, dave_message.clone()); +// assert_matches!(dave_acceptance, MessageAcceptance::Reject); +// assert!(dave_verified_msg.is_none()); +// assert_eq!(alice.cached_messages.len(), 0); + +// // Dave now is in the next validator set +// let snapshot = ValidatorListSnapshot { +// current_era_index: CHAIN_HEAD_ERA, +// current_validators: Default::default(), +// next_validators: Some(nonempty![dave_message.address()].into()), +// }; +// alice.on_new_snapshot(Arc::new(snapshot)); + +// // Dave's message is cached +// let (dave_acceptance, dave_verified_msg) = +// alice.verify_validator_message(dave_source, dave_message.clone()); +// assert_matches!(dave_acceptance, MessageAcceptance::Ignore); +// assert!(dave_verified_msg.is_none()); +// assert_eq!(alice.cached_messages.len(), 1); + +// // Dave's message now is in the current validator set +// let snapshot = ValidatorListSnapshot { +// current_era_index: NEXT_ERA, +// current_validators: nonempty![dave_message.address()].into(), +// next_validators: None, +// }; +// alice.on_new_snapshot(Arc::new(snapshot)); + +// let dave_verified_msg = alice.next_message().unwrap(); +// assert_eq!(dave_verified_msg, dave_message); +// } + +// #[test] +// fn verify_promise_unknown_validator() { +// let topic = new_topic(nonempty![Address::default()]); +// let promise = signed_promise(); +// let peer_id = PeerId::random(); + +// let err = topic +// .inner_verify_promise(peer_id, promise.clone()) +// .unwrap_err(); +// assert_eq!( +// err, +// VerifyPromiseError::UnknownValidator { +// address: promise.address(), +// tx_hash: promise.data().tx_hash, +// } +// ); + +// let (acceptance, promise) = topic.verify_promise(peer_id, promise); +// assert_matches!(acceptance, MessageAcceptance::Ignore); +// assert_eq!(promise, None); +// } + +// #[ignore = "TODO"] +// #[tokio::test] +// async fn verify_promise_ok() { +// let promise = signed_promise(); +// let topic = new_topic(nonempty![promise.address()]); +// let peer_id = PeerId::random(); + +// topic +// .inner_verify_promise(peer_id, promise.clone()) +// .unwrap(); + +// let (acceptance, returned_promise) = topic.verify_promises_bundle(peer_id, promise.clone()); +// assert_matches!(acceptance, MessageAcceptance::Accept); +// assert_eq!(returned_promise, Some(promise)); +// } +// } diff --git a/ethexe/rpc/src/apis/injected.rs b/ethexe/rpc/src/apis/injected.rs index d5fea2c9602..7708adae86f 100644 --- a/ethexe/rpc/src/apis/injected.rs +++ b/ethexe/rpc/src/apis/injected.rs @@ -19,12 +19,14 @@ use crate::{RpcEvent, errors}; use dashmap::DashMap; use ethexe_common::{ - HashOf, + HashOf, SignedMessage, Announce, + db::{AnnounceStorageRO, InjectedStorageRO}, injected::{ - AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, - SignedPromise, + AddressedInjectedTransaction, CompactSignedPromise, InjectedTransaction, + InjectedTransactionAcceptance, PromisesNetworkBundle, SignedPromise, }, }; +use ethexe_db::Database; use jsonrpsee::{ PendingSubscriptionSink, SubscriptionMessage, SubscriptionSink, core::{RpcResult, SubscriptionResult, async_trait}, @@ -57,14 +59,19 @@ pub trait Injected { } type PromiseWaiters = Arc, oneshot::Sender>>; +type PendingAnnouncePromises = Arc, Vec>>; /// Implementation of the injected transactions RPC API. #[derive(Debug, Clone)] pub struct InjectedApi { + /// The database for protocol data. + db: Database, /// Sender to forward RPC events to the main service. rpc_sender: mpsc::UnboundedSender, /// Map of promise waiters. promise_waiters: PromiseWaiters, + /// + _pending_promises: PendingAnnouncePromises, } #[async_trait] @@ -113,14 +120,34 @@ impl InjectedServer for InjectedApi { } impl InjectedApi { - pub(crate) fn new(rpc_sender: mpsc::UnboundedSender) -> Self { + pub(crate) fn new(db: Database, rpc_sender: mpsc::UnboundedSender) -> Self { Self { + db, rpc_sender, promise_waiters: PromiseWaiters::default(), + _pending_promises: PendingAnnouncePromises::default(), } } - pub fn send_promise(&self, promise: SignedPromise) { + pub fn receive_promises_bundle(&self, bundle: PromisesNetworkBundle) { + match self.db.announce_meta(bundle.announce).computed { + true => todo!("go to send promises to receivers"), + false => todo!("put hashes into pending and wait for announce computation"), + } + } + + + pub fn send_promise(&self, signed_hash: CompactSignedPromise) { + let (tx_hash, address, signature) = signed_hash.into_parts(); + + let Some(p) = self.db.promise(tx_hash) else { + todo!("Handle this case") + }; + + let Ok(promise) = SignedMessage::try_from_parts(p, signature, address) else { + todo!("handle invalid signature case") + }; + let Some((_, promise_sender)) = self.promise_waiters.remove(&promise.data().tx_hash) else { tracing::warn!(promise = ?promise, "receive unregistered promise"); return; diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index 6c69db952b3..2521114d9d5 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -24,8 +24,11 @@ use apis::{ BlockApi, BlockServer, CodeApi, CodeServer, InjectedApi, InjectedServer, ProgramApi, ProgramServer, }; -use ethexe_common::injected::{ - AddressedInjectedTransaction, InjectedTransactionAcceptance, SignedPromise, +use ethexe_common::{ + Announce, HashOf, + injected::{ + AddressedInjectedTransaction, InjectedTransactionAcceptance, PromisesNetworkBundle, + }, }; use ethexe_db::Database; use ethexe_processor::{Processor, ProcessorConfig}; @@ -112,7 +115,7 @@ impl RpcServer { code: CodeApi::new(self.db.clone()), block: BlockApi::new(self.db.clone()), program: ProgramApi::new(self.db.clone(), processor, self.config.gas_allowance), - injected: InjectedApi::new(rpc_sender), + injected: InjectedApi::new(self.db.clone(), rpc_sender), }; let injected_api = server_apis.injected.clone(); @@ -148,17 +151,25 @@ impl RpcService { } } - /// Provides a promise inside RPC service to be sent to subscribers. - pub fn provide_promise(&self, promise: SignedPromise) { - self.injected_api.send_promise(promise); + pub fn receive_computed_announce(&self, _announce_hash: HashOf) { + todo!("Handle the variant when announce computed and we can send promises") } - /// Provides a bundle of promises inside RPC service to be sent to subscribers. - pub fn provide_promises(&self, promises: Vec) { - promises.into_iter().for_each(|promise| { - self.provide_promise(promise); - }); + pub fn provide_promises_bundle(&self, bundle: PromisesNetworkBundle) { + self.injected_api.receive_promises_bundle(bundle); } + + // Provides a promise inside RPC service to be sent to subscribers. + // pub fn provide_promise(&self, promise: CompactSignedPromise) { + // self.injected_api.send_promise(promise); + // } + + // Provides a bundle of promises inside RPC service to be sent to subscribers. + // pub fn provide_promises(&self, promises: Vec) { + // promises.into_iter().for_each(|promise| { + // self.provide_promise(promise); + // }); + // } } impl Stream for RpcService { diff --git a/ethexe/rpc/src/tests.rs b/ethexe/rpc/src/tests.rs index 9ef72c8b4ae..14900cc0109 100644 --- a/ethexe/rpc/src/tests.rs +++ b/ethexe/rpc/src/tests.rs @@ -20,19 +20,19 @@ use crate::{ InjectedApi, InjectedClient, InjectedTransactionAcceptance, RpcConfig, RpcEvent, RpcServer, RpcService, }; - use ethexe_common::{ + HashOf, + db::InjectedStorageRW, ecdsa::PrivateKey, gear::MAX_BLOCK_GAS_LIMIT, - injected::{AddressedInjectedTransaction, Promise, SignedPromise}, + injected::{ + AddressedInjectedTransaction, CompactSignedPromise, Promise, PromisesNetworkBundle, + }, mock::Mock, }; use ethexe_db::Database; use futures::StreamExt; -use gear_core::{ - message::{ReplyCode, SuccessReplyReason}, - rpc::ReplyInfo, -}; +use gear_core::message::{ReplyCode, SuccessReplyReason}; use jsonrpsee::{server::ServerHandle, ws_client::WsClientBuilder}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use tokio::task::{JoinHandle, JoinSet}; @@ -42,13 +42,15 @@ use tokio::task::{JoinHandle, JoinSet}; struct MockService { rpc: RpcService, handle: ServerHandle, + db: Database, } impl MockService { /// Creates a new mock service which runs an RPC server listening on the given address. pub async fn new(listen_addr: SocketAddr) -> Self { + let db = Database::memory(); let (handle, rpc) = start_new_server(listen_addr).await; - Self { rpc, handle } + Self { rpc, handle, db } } pub fn injected_api(&self) -> InjectedApi { @@ -67,8 +69,8 @@ impl MockService { loop { tokio::select! { _ = tx_batch_interval.tick() => { - let promises = tx_batch.drain(..).map(Self::create_promise_for).collect(); - self.rpc.provide_promises(promises); + let bundle = self.create_promises_bundle(tx_batch.drain(..)); + self.rpc.provide_promises_bundle(bundle); }, _ = self.handle.clone().stopped() => { unreachable!("RPC server should not be stopped during the test") @@ -84,16 +86,22 @@ impl MockService { }) } - fn create_promise_for(tx: AddressedInjectedTransaction) -> SignedPromise { - let promise = Promise { - tx_hash: tx.tx.data().to_hash(), - reply: ReplyInfo { - payload: vec![], - value: 0, - code: ReplyCode::Success(SuccessReplyReason::Manual), - }, - }; - SignedPromise::create(PrivateKey::random(), promise).expect("Signing promise will succeed") + fn create_promises_bundle( + &self, + txs: impl IntoIterator, + ) -> PromisesNetworkBundle { + let pk = PrivateKey::random(); + let promises = txs + .into_iter() + .map(|tx| { + let promise = Promise::mock(tx.tx.data().to_hash()); + self.db.set_promise(promise.clone()); + CompactSignedPromise::create_from_private_key(&pk, promise).unwrap() + }) + .collect::>(); + + let announce = HashOf::random(); + PromisesNetworkBundle { announce, promises } } } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 07c3cafd3d8..7eff2da700d 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -597,9 +597,9 @@ impl Service { let _res = response_sender.send(acceptance); } }, - NetworkEvent::PromiseMessage(promise) => { + NetworkEvent::PromisesBundle(bundle) => { if let Some(rpc) = &rpc { - rpc.provide_promise(promise); + rpc.provide_promises_bundle(bundle); } } NetworkEvent::ValidatorIdentityUpdated(_) @@ -702,19 +702,17 @@ impl Service { ConsensusEvent::AnnounceAccepted(_) | ConsensusEvent::AnnounceRejected(_) => { // TODO #4940: consider to publish network message } - ConsensusEvent::Promises(promises) => { + ConsensusEvent::Promises(bundle) => { if rpc.is_none() && network.is_none() { panic!("Promise without network or rpc"); } if let Some(rpc) = &rpc { - rpc.provide_promises(promises.clone()); + rpc.provide_promises_bundle(bundle.clone()); } if let Some(network) = &mut network { - for promise in promises { - network.publish_promise(promise); - } + network.publish_promises_bundle(bundle); } } }, diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index c0e5be076d7..4f2e86dec65 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -2490,10 +2490,14 @@ async fn injected_tx_fungible_token() { // Listen for inclusion and check the expected payload. node.events() .find(|event| { - if let TestingEvent::Consensus(ConsensusEvent::Promises(promises)) = event - && !promises.is_empty() + if let TestingEvent::Consensus(ConsensusEvent::Promises(bundle)) = event + && !bundle.promises.is_empty() { - let promise = promises.first().unwrap().data(); + let promise_tx_hash = bundle.promises.first().unwrap().tx_hash(); + let promise = node + .db + .promise(promise_tx_hash) + .expect("promise exists in db"); assert_eq!(promise.reply.payload, expected_event.encode()); assert_eq!( promise.reply.code, diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index eb1bd74d8b7..fa926544dc0 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -27,7 +27,7 @@ use ethexe_common::{ events::BlockEvent, injected::{ AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, - SignedInjectedTransaction, SignedPromise, + PromisesNetworkBundle, SignedInjectedTransaction, }, network::VerifiedValidatorMessage, }; @@ -85,7 +85,7 @@ impl TestingNetworkInjectedEvent { #[derive(Debug, Clone, Eq, PartialEq)] pub enum TestingNetworkEvent { ValidatorMessage(VerifiedValidatorMessage), - PromiseMessage(SignedPromise), + PromisesBundle(PromisesNetworkBundle), ValidatorIdentityUpdated(Address), InjectedTransaction(TestingNetworkInjectedEvent), PeerBlocked(PeerId), @@ -96,7 +96,7 @@ impl TestingNetworkEvent { fn new(event: &NetworkEvent) -> Self { match event { NetworkEvent::ValidatorMessage(message) => Self::ValidatorMessage(message.clone()), - NetworkEvent::PromiseMessage(message) => Self::PromiseMessage(message.clone()), + NetworkEvent::PromisesBundle(message) => Self::PromisesBundle(message.clone()), NetworkEvent::ValidatorIdentityUpdated(address) => { Self::ValidatorIdentityUpdated(*address) } From cc740d539cb2274cd311f3469143e11ba0d23240 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Sun, 8 Feb 2026 10:41:19 +0700 Subject: [PATCH 02/59] fix changes | final design --- Cargo.lock | 2 + ethexe/common/Cargo.toml | 1 + ethexe/common/src/injected.rs | 70 +++++++++---------- ethexe/consensus/src/lib.rs | 6 +- ethexe/consensus/src/validator/mod.rs | 13 +--- ethexe/consensus/src/validator/producer.rs | 20 ++---- ethexe/network/src/gossipsub.rs | 10 +-- ethexe/network/src/lib.rs | 17 +++-- ethexe/network/src/validator/topic.rs | 52 +++++++++------ ethexe/rpc/Cargo.toml | 1 + ethexe/rpc/src/apis/injected.rs | 74 +++++++++++++++------ ethexe/rpc/src/lib.rs | 21 +++--- ethexe/rpc/src/tests.rs | 27 +++----- ethexe/service/src/lib.rs | 18 +++-- ethexe/service/src/tests/mod.rs | 6 +- ethexe/service/src/tests/utils/events.rs | 8 +-- gsigner/src/hash.rs | 45 +++++++++++++ gsigner/src/schemes/secp256k1/signature.rs | 34 +++++++++- gsigner/src/schemes/secp256k1/signer_ext.rs | 19 ++++++ 19 files changed, 288 insertions(+), 156 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91ec52b56cc..5ea9a46176e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5010,6 +5010,7 @@ dependencies = [ "sha3", "sp-core", "tap", + "thiserror 2.0.17", ] [[package]] @@ -5235,6 +5236,7 @@ dependencies = [ "tower 0.4.13", "tower-http 0.5.2", "tracing", + "tracing-subscriber", ] [[package]] diff --git a/ethexe/common/Cargo.toml b/ethexe/common/Cargo.toml index 93ff90d5dff..51dc7266730 100644 --- a/ethexe/common/Cargo.toml +++ b/ethexe/common/Cargo.toml @@ -27,6 +27,7 @@ gsigner = { workspace = true, default-features = false, features = [ sha3.workspace = true k256 = { version = "0.13.4", features = ["ecdsa"], default-features = false } nonempty.workspace = true +thiserror.workspace = true # mock deps itertools = { workspace = true, optional = true } diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index 5f7d8c31746..c12420edc49 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -16,20 +16,17 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::{Address, Announce, HashOf, ToDigest, ecdsa::SignedMessage}; -use alloc::{ - string::{String, ToString}, - vec::Vec, -}; +use crate::{Address, HashOf, ToDigest, ecdsa::SignedMessage}; +use alloc::string::{String, ToString}; use core::hash::Hash; use gear_core::rpc::ReplyInfo; use gprimitives::{ActorId, H256, MessageId}; -use gsigner::Signature; #[cfg(feature = "std")] use gsigner::{ PrivateKey, PublicKey, SignerError, secp256k1::{Secp256k1SignerExt, Signer}, }; +use gsigner::{Signature, hash::Eip191Hash}; use parity_scale_codec::{Decode, Encode}; use sha3::{Digest, Keccak256}; use sp_core::Bytes; @@ -159,22 +156,31 @@ impl ToDigest for Promise { } } -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -pub struct PromisesNetworkBundle { - /// The hash of [`Announce`] for which promises was created. - pub announce: HashOf, - /// The hashes of transactions with signatures - pub promises: Vec, -} +// The bundle of signed [`Promise`]s was produced in concrete announce. +// #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +// pub struct PromisesNetworkBundle { +// /// The hash of [`Announce`] for which promises was created. +// pub announce: HashOf, +// /// The hashes of transactions with signatures +// pub promises: Vec, +// } + +// impl ToDigest for PromisesNetworkBundle { +// fn update_hasher(&self, hasher: &mut sha3::Keccak256) { +// todo!() +// } +// } #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct CompactSignedPromise { /// The hash of transaction, for which promise was created. - tx_hash: HashOf, - /// The address converted from public key of block producer. - address: Address, + pub tx_hash: HashOf, + /// The [`Eip191Hash`] of the promise, was signed by block producer. + /// Important: this hash is needed to verify the signature correctness + /// without having a full promise body. + pub eip191_hash: Eip191Hash, /// The signature over the [`Promise`] for `tx_hash`. - signature: Signature, + pub signature: Signature, } #[cfg(feature = "std")] @@ -184,15 +190,12 @@ impl CompactSignedPromise { public_key: PublicKey, promise: Promise, ) -> Result { - let tx_hash = promise.tx_hash; - let (address, signature) = signer - .signed_message(public_key, promise, None) - .map(|message| (message.address(), message.into_parts().1))?; + let eip191_hash = Eip191Hash::new(&promise); Ok(Self { - tx_hash, - address, - signature, + tx_hash: promise.tx_hash, + signature: signer.sign_eip191_hash(public_key, eip191_hash, None)?, + eip191_hash, }) } @@ -200,23 +203,14 @@ impl CompactSignedPromise { private_key: &PrivateKey, promise: Promise, ) -> Result { - let tx_hash = promise.tx_hash; - let signature = Signature::create(private_key, promise)?; - let address = private_key.public_key().to_address(); + let eip191_hash = Eip191Hash::new(&promise); + Ok(Self { - tx_hash, - signature, - address, + tx_hash: promise.tx_hash, + eip191_hash, + signature: Signature::create_from_eip191_hash(private_key, eip191_hash)?, }) } - - pub fn tx_hash(&self) -> HashOf { - self.tx_hash - } - - pub fn into_parts(self) -> (HashOf, Address, Signature) { - (self.tx_hash, self.address, self.signature) - } } #[cfg(test)] diff --git a/ethexe/consensus/src/lib.rs b/ethexe/consensus/src/lib.rs index dddaadd2500..dc9c673e5e3 100644 --- a/ethexe/consensus/src/lib.rs +++ b/ethexe/consensus/src/lib.rs @@ -36,9 +36,7 @@ use anyhow::Result; use ethexe_common::{ Announce, ComputedAnnounce, Digest, HashOf, SimpleBlockData, consensus::{BatchCommitmentValidationReply, VerifiedAnnounce, VerifiedValidationRequest}, - injected::{ - PromisesNetworkBundle, SignedInjectedTransaction, - }, + injected::{CompactSignedPromise, SignedInjectedTransaction}, network::{AnnouncesRequest, AnnouncesResponse, SignedValidatorMessage}, }; use futures::{Stream, stream::FusedStream}; @@ -126,5 +124,5 @@ pub enum ConsensusEvent { Warning(String), /// Promises for [`ethexe_common::injected::InjectedTransaction`]s execution in some announce. #[from] - Promises(PromisesNetworkBundle), + Promises(Vec), } diff --git a/ethexe/consensus/src/validator/mod.rs b/ethexe/consensus/src/validator/mod.rs index 2c303ae7e74..a53c2064f37 100644 --- a/ethexe/consensus/src/validator/mod.rs +++ b/ethexe/consensus/src/validator/mod.rs @@ -54,10 +54,10 @@ use anyhow::{Result, anyhow}; pub use core::BatchCommitter; use derive_more::{Debug, From}; use ethexe_common::{ - Address, ComputedAnnounce, SimpleBlockData, ToDigest, + Address, ComputedAnnounce, SimpleBlockData, consensus::{VerifiedAnnounce, VerifiedValidationRequest}, db::OnChainStorageRO, - ecdsa::{PublicKey, SignedMessage}, + ecdsa::PublicKey, injected::SignedInjectedTransaction, network::AnnouncesResponse, }; @@ -69,7 +69,7 @@ use futures::{ stream::{FusedStream, FuturesUnordered}, }; use gprimitives::H256; -use gsigner::secp256k1::{Secp256k1SignerExt, Signer}; +use gsigner::secp256k1::Signer; use initial::Initial; use std::{ collections::VecDeque, @@ -547,11 +547,4 @@ impl ValidatorContext { pub fn pending(&mut self, event: impl Into) { self.pending_events.push_front(event.into()); } - - pub fn sign_message(&self, data: T) -> Result> { - Ok(self - .core - .signer - .signed_message(self.core.pub_key, data, None)?) - } } diff --git a/ethexe/consensus/src/validator/producer.rs b/ethexe/consensus/src/validator/producer.rs index 419a79f33ce..8b84153d5d9 100644 --- a/ethexe/consensus/src/validator/producer.rs +++ b/ethexe/consensus/src/validator/producer.rs @@ -24,14 +24,11 @@ use crate::{ announces::{self, DBAnnouncesExt}, validator::DefaultProcessing, }; -use anyhow::{ Result, anyhow}; +use anyhow::{Result, anyhow}; use derive_more::{Debug, Display}; use ethexe_common::{ - Announce, ComputedAnnounce, HashOf, SimpleBlockData, ValidatorsVec, - db::BlockMetaStorageRO, - gear::BatchCommitment, - injected::{CompactSignedPromise, PromisesNetworkBundle}, - network::ValidatorMessage, + Announce, ComputedAnnounce, HashOf, SimpleBlockData, ValidatorsVec, db::BlockMetaStorageRO, + gear::BatchCommitment, injected::CompactSignedPromise, network::ValidatorMessage, }; use ethexe_service_utils::Timer; use futures::{FutureExt, future::BoxFuture}; @@ -85,22 +82,19 @@ impl StateHandler for Producer { if *expected == computed_data.announce_hash => { if !computed_data.promises.is_empty() { - let promises = computed_data + let signed_promises = computed_data .promises .into_iter() .map(|promise| { CompactSignedPromise::create( &self.ctx.core.signer, self.ctx.core.pub_key, - promise, + promise.clone(), ) }) .collect::>()?; - let bundle = PromisesNetworkBundle { - announce: computed_data.announce_hash, - promises, - }; - self.ctx.output(ConsensusEvent::Promises(bundle)); + + self.ctx.output(ConsensusEvent::Promises(signed_promises)); } // Aggregate commitment for the block and use `announce_hash` as head for chain commitment. diff --git a/ethexe/network/src/gossipsub.rs b/ethexe/network/src/gossipsub.rs index bf1f318cc7b..a5dbb67006a 100644 --- a/ethexe/network/src/gossipsub.rs +++ b/ethexe/network/src/gossipsub.rs @@ -23,7 +23,7 @@ use crate::{ peer_score, }; use anyhow::anyhow; -use ethexe_common::{Address, injected::PromisesNetworkBundle, network::SignedValidatorMessage}; +use ethexe_common::{Address, injected::CompactSignedPromise, network::SignedValidatorMessage}; use libp2p::{ core::{Endpoint, transport::PortUse}, gossipsub, @@ -44,21 +44,21 @@ use std::{ pub enum Message { // TODO: rename to `Validators` Commitments(SignedValidatorMessage), - PromisesBundle(PromisesNetworkBundle), + Promise(CompactSignedPromise), } impl Message { fn topic_hash(&self, behaviour: &Behaviour) -> TopicHash { match self { Message::Commitments(_) => behaviour.commitments_topic.hash(), - Message::PromisesBundle(_) => behaviour.promises_topic.hash(), + Message::Promise(_) => behaviour.promises_topic.hash(), } } fn encode(&self) -> Vec { match self { Message::Commitments(message) => message.encode(), - Message::PromisesBundle(message) => message.encode(), + Message::Promise(message) => message.encode(), } } } @@ -177,7 +177,7 @@ impl Behaviour { let res = if topic == self.commitments_topic.hash() { SignedValidatorMessage::decode(&mut &data[..]).map(Message::Commitments) } else if topic == self.promises_topic.hash() { - PromisesNetworkBundle::decode(&mut &data[..]).map(Message::PromisesBundle) + CompactSignedPromise::decode(&mut &data[..]).map(Message::Promise) } else { unreachable!("topic we never subscribed to: {topic:?}"); }; diff --git a/ethexe/network/src/lib.rs b/ethexe/network/src/lib.rs index d3b51f20f9e..9e74824f9cd 100644 --- a/ethexe/network/src/lib.rs +++ b/ethexe/network/src/lib.rs @@ -39,7 +39,7 @@ use anyhow::{Context, anyhow}; use ethexe_common::{ Address, BlockHeader, ValidatorsVec, ecdsa::PublicKey, - injected::{AddressedInjectedTransaction, PromisesNetworkBundle}, + injected::{AddressedInjectedTransaction, CompactSignedPromise}, network::{SignedValidatorMessage, VerifiedValidatorMessage}, }; use futures::{Stream, future::Either, ready, stream::FusedStream}; @@ -80,7 +80,7 @@ impl NetworkServiceDatabase for T where T: DbSyncDatabase + ValidatorDatabase pub enum NetworkEvent { // gossipsub ValidatorMessage(VerifiedValidatorMessage), - PromisesBundle(PromisesNetworkBundle), + PromiseMessage(CompactSignedPromise), // validator-identity ValidatorIdentityUpdated(Address), // injected-tx @@ -493,11 +493,11 @@ impl NetworkService { .verify_validator_message(source, message); (acceptance, message.map(NetworkEvent::ValidatorMessage)) } - gossipsub::Message::PromisesBundle(bundle) => { + gossipsub::Message::Promise(compact_promise) => { // FIXME: previous era validators are ignored let (acceptance, promise) = - self.validator_topic.verify_promises_bundle(source, bundle); - (acceptance, promise.map(NetworkEvent::PromisesBundle)) + self.validator_topic.verify_promise(source, compact_promise); + (acceptance, promise.map(NetworkEvent::PromiseMessage)) } }) } @@ -572,8 +572,11 @@ impl NetworkService { .send_transaction(behaviour.validator_discovery.identities(), data) } - pub fn publish_promises_bundle(&mut self, bundle: PromisesNetworkBundle) { - self.swarm.behaviour_mut().gossipsub.publish(bundle) + pub fn publish_promise(&mut self, compact_promise: CompactSignedPromise) { + self.swarm + .behaviour_mut() + .gossipsub + .publish(compact_promise) } } diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index c2b373c37c0..15685cfb788 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -25,9 +25,10 @@ use crate::{ }; use ethexe_common::{ Address, HashOf, - injected::{InjectedTransaction, PromisesNetworkBundle}, + injected::{CompactSignedPromise, InjectedTransaction}, network::VerifiedValidatorMessage, }; +use gsigner::{Signature, SignerError}; use lru::LruCache; use std::{cmp::Ordering, collections::VecDeque, mem, num::NonZeroUsize, sync::Arc}; @@ -83,13 +84,17 @@ enum VerificationError { Reject(VerificationRejectReason), } -#[derive(Debug, PartialEq, Eq, derive_more::Display)] +#[derive(Debug, derive_more::Display)] enum VerifyPromiseError { #[display("unknown validator: address={address}, tx_hash={tx_hash}")] UnknownValidator { address: Address, tx_hash: HashOf, }, + #[display("failed to recover validator's public key: signature={signature}")] + ValidatorPublicKeyRecover { signature: Signature }, + #[display("failed to verify promises signatures bundle: error={signer_error}")] + SignatureVerification { signer_error: SignerError }, } /// Tracks validator-signed messages and admits each one once the on-chain @@ -263,31 +268,40 @@ impl ValidatorTopic { } } - fn inner_verify_promises_bundle( + fn inner_verify_promise( &self, _source: PeerId, - bundle: PromisesNetworkBundle, - ) -> Result { - // TODO: uncomment this - - // let address = promise.address(); - // let tx_hash = promise.data().tx_hash; - - // if !self.snapshot.contains(address) { - // return Err(VerifyPromiseError::UnknownValidator { address, tx_hash }); - // } + compact_promise: CompactSignedPromise, + ) -> Result { + let CompactSignedPromise { + tx_hash, + signature, + eip191_hash, + } = compact_promise.clone(); + + let public_key = signature + .recover_from_eip191_hash(eip191_hash) + .map_err(|_| VerifyPromiseError::ValidatorPublicKeyRecover { signature })?; + + let address = public_key.to_address(); + if !self.snapshot.contains(address) { + return Err(VerifyPromiseError::UnknownValidator { address, tx_hash }); + } - Ok(bundle) + match signature.verify_with_eip191_hash(public_key, eip191_hash) { + Ok(()) => Ok(compact_promise), + Err(signer_error) => Err(VerifyPromiseError::SignatureVerification { signer_error }), + } } // FIXME: messages from previous era validators are ignored - pub fn verify_promises_bundle( + pub fn verify_promise( &self, source: PeerId, - bundle: PromisesNetworkBundle, - ) -> (MessageAcceptance, Option) { - match self.inner_verify_promises_bundle(source, bundle) { - Ok(bundle) => (MessageAcceptance::Accept, Some(bundle)), + compact_promise: CompactSignedPromise, + ) -> (MessageAcceptance, Option) { + match self.inner_verify_promise(source, compact_promise) { + Ok(compact_promise) => (MessageAcceptance::Accept, Some(compact_promise)), Err(err) => { log::trace!("failed to verify promises bundle: {err}"); (MessageAcceptance::Ignore, None) diff --git a/ethexe/rpc/Cargo.toml b/ethexe/rpc/Cargo.toml index dbc04186b41..77617926545 100644 --- a/ethexe/rpc/Cargo.toml +++ b/ethexe/rpc/Cargo.toml @@ -34,6 +34,7 @@ gear-workspace-hack.workspace = true jsonrpsee = { workspace = true, features = ["client"] } ethexe-common = { workspace = true, features = ["std", "mock"] } ntest.workspace = true +tracing-subscriber.workspace = true [features] client = ["jsonrpsee/client"] diff --git a/ethexe/rpc/src/apis/injected.rs b/ethexe/rpc/src/apis/injected.rs index 41dcbc94fb0..cd167b2266e 100644 --- a/ethexe/rpc/src/apis/injected.rs +++ b/ethexe/rpc/src/apis/injected.rs @@ -19,11 +19,11 @@ use crate::{RpcEvent, errors}; use dashmap::DashMap; use ethexe_common::{ - Announce, HashOf, SignedMessage, - db::{AnnounceStorageRO, InjectedStorageRO}, + HashOf, SignedMessage, + db::InjectedStorageRO, injected::{ AddressedInjectedTransaction, CompactSignedPromise, InjectedTransaction, - InjectedTransactionAcceptance, PromisesNetworkBundle, SignedPromise, + InjectedTransactionAcceptance, Promise, SignedPromise, }, }; use ethexe_db::Database; @@ -65,7 +65,6 @@ pub trait Injected { } type PromiseWaiters = Arc, oneshot::Sender>>; -type PendingAnnouncePromises = Arc, Vec>>; /// Implementation of the injected transactions RPC API. #[derive(Debug, Clone)] @@ -76,8 +75,9 @@ pub struct InjectedApi { rpc_sender: mpsc::UnboundedSender, /// Map of promise waiters. promise_waiters: PromiseWaiters, + /// - _pending_promises: PendingAnnouncePromises, + promises_computation_waiting: Arc, CompactSignedPromise>>, } #[async_trait] @@ -128,7 +128,7 @@ impl InjectedServer for InjectedApi { &self, tx_hash: HashOf, ) -> RpcResult> { - let Some(promise) = self.db.promise(hash) else { + let Some(promise) = self.db.promise(tx_hash) else { tracing::trace!(?tx_hash, "promise not found for injected transaction"); return Ok(None); }; @@ -139,7 +139,7 @@ impl InjectedServer for InjectedApi { match SignedMessage::try_from_parts(promise, signature, address) { Ok(message) => Ok(Some(message)), - Err(err) => { + Err(_err) => { tracing::trace!(""); Ok(None) } @@ -153,34 +153,64 @@ impl InjectedApi { db, rpc_sender, promise_waiters: PromiseWaiters::default(), - _pending_promises: PendingAnnouncePromises::default(), + promises_computation_waiting: Default::default(), } } - pub fn receive_promises_bundle(&self, bundle: PromisesNetworkBundle) { - match self.db.announce_meta(bundle.announce).computed { - true => todo!("go to send promises to receivers"), - false => todo!("put hashes into pending and wait for announce computation"), + pub fn receive_compact_promise(&self, compact_promise: CompactSignedPromise) { + match self.db.promise(compact_promise.tx_hash) { + Some(promise) => { + tracing::trace!(tx_hash = ?promise.tx_hash, "Promise already computed, send to user"); + self.send_promise(promise, compact_promise); + } + None => { + tracing::trace!(tx_hash = ?compact_promise.tx_hash, "Promise doesn't compute yet, waiting for producer's signature"); + self.promises_computation_waiting + .insert(compact_promise.tx_hash, compact_promise); + } } } - pub fn send_promise(&self, signed_hash: CompactSignedPromise) { - let (tx_hash, address, signature) = signed_hash.into_parts(); + pub fn receive_computed_promises(&self, promises: Vec) { + promises.into_iter().for_each(|promise| { + // In case of `None` nothing to do, because of promise already in RPC database. + if let Some((_, compact_promise)) = + self.promises_computation_waiting.remove(&promise.tx_hash) + { + self.send_promise(promise, compact_promise); + } + }) + } - let Some(p) = self.db.promise(tx_hash) else { - todo!("Handle this case") - }; + pub fn send_promise(&self, promise: Promise, compact_promise: CompactSignedPromise) { + let CompactSignedPromise { + tx_hash, + signature, + eip191_hash, + } = compact_promise; - let Ok(promise) = SignedMessage::try_from_parts(p, signature, address) else { - todo!("handle invalid signature case") + let Ok(public_key) = signature.recover_from_eip191_hash(eip191_hash) else { + todo!() + }; + let address = public_key.to_address(); + + // TODO: remove clone here. + let Ok(message) = SignedMessage::try_from_parts(promise.clone(), signature, address) else { + tracing::trace!( + ?promise, + ?compact_promise, + "failed to build `SignedMessage` from parts, invalid signature" + ); + todo!("handle invalid signature case"); }; - let Some((_, promise_sender)) = self.promise_waiters.remove(&promise.data().tx_hash) else { - tracing::warn!(promise = ?promise, "receive unregistered promise"); + let Some((_, promise_sender)) = self.promise_waiters.remove(&compact_promise.tx_hash) + else { + tracing::warn!(?tx_hash, promise = ?promise, "receive unregistered promise for injected transaction"); return; }; - if let Err(promise) = promise_sender.send(promise) { + if let Err(promise) = promise_sender.send(message) { tracing::trace!(promise = ?promise, "rpc promise receiver dropped"); } } diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index 2521114d9d5..e67daf8d2e8 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -25,10 +25,8 @@ use apis::{ ProgramServer, }; use ethexe_common::{ - Announce, HashOf, - injected::{ - AddressedInjectedTransaction, InjectedTransactionAcceptance, PromisesNetworkBundle, - }, + ComputedAnnounce, + injected::{AddressedInjectedTransaction, CompactSignedPromise, InjectedTransactionAcceptance}, }; use ethexe_db::Database; use ethexe_processor::{Processor, ProcessorConfig}; @@ -151,12 +149,19 @@ impl RpcService { } } - pub fn receive_computed_announce(&self, _announce_hash: HashOf) { - todo!("Handle the variant when announce computed and we can send promises") + pub fn receive_computed_data(&self, computed_data: ComputedAnnounce) { + self.injected_api + .receive_computed_promises(computed_data.promises); + } + + pub fn provide_compact_promise(&self, compact_promise: CompactSignedPromise) { + self.injected_api.receive_compact_promise(compact_promise); } - pub fn provide_promises_bundle(&self, bundle: PromisesNetworkBundle) { - self.injected_api.receive_promises_bundle(bundle); + pub fn provide_compact_promises(&self, compact_promises: Vec) { + compact_promises + .into_iter() + .for_each(|compact_promise| self.provide_compact_promise(compact_promise)); } // Provides a promise inside RPC service to be sent to subscribers. diff --git a/ethexe/rpc/src/tests.rs b/ethexe/rpc/src/tests.rs index 14900cc0109..0d6bc7d6dc0 100644 --- a/ethexe/rpc/src/tests.rs +++ b/ethexe/rpc/src/tests.rs @@ -21,13 +21,10 @@ use crate::{ RpcService, }; use ethexe_common::{ - HashOf, db::InjectedStorageRW, ecdsa::PrivateKey, gear::MAX_BLOCK_GAS_LIMIT, - injected::{ - AddressedInjectedTransaction, CompactSignedPromise, Promise, PromisesNetworkBundle, - }, + injected::{AddressedInjectedTransaction, CompactSignedPromise, Promise}, mock::Mock, }; use ethexe_db::Database; @@ -49,7 +46,7 @@ impl MockService { /// Creates a new mock service which runs an RPC server listening on the given address. pub async fn new(listen_addr: SocketAddr) -> Self { let db = Database::memory(); - let (handle, rpc) = start_new_server(listen_addr).await; + let (handle, rpc) = start_new_server(listen_addr, db.clone()).await; Self { rpc, handle, db } } @@ -69,8 +66,8 @@ impl MockService { loop { tokio::select! { _ = tx_batch_interval.tick() => { - let bundle = self.create_promises_bundle(tx_batch.drain(..)); - self.rpc.provide_promises_bundle(bundle); + let promises = self.create_promises_bundle(tx_batch.drain(..)); + self.rpc.provide_compact_promises(promises); }, _ = self.handle.clone().stopped() => { unreachable!("RPC server should not be stopped during the test") @@ -89,31 +86,27 @@ impl MockService { fn create_promises_bundle( &self, txs: impl IntoIterator, - ) -> PromisesNetworkBundle { + ) -> Vec { let pk = PrivateKey::random(); - let promises = txs - .into_iter() + txs.into_iter() .map(|tx| { let promise = Promise::mock(tx.tx.data().to_hash()); self.db.set_promise(promise.clone()); CompactSignedPromise::create_from_private_key(&pk, promise).unwrap() }) - .collect::>(); - - let announce = HashOf::random(); - PromisesNetworkBundle { announce, promises } + .collect() } } /// Starts a new RPC server listening on the given address. -async fn start_new_server(listen_addr: SocketAddr) -> (ServerHandle, RpcService) { +async fn start_new_server(listen_addr: SocketAddr, db: Database) -> (ServerHandle, RpcService) { let rpc_config = RpcConfig { listen_addr, cors: None, gas_allowance: MAX_BLOCK_GAS_LIMIT, chunk_size: 2, }; - RpcServer::new(rpc_config, Database::memory()) + RpcServer::new(rpc_config, db) .run_server() .await .expect("RPC Server will start successfully") @@ -129,6 +122,8 @@ async fn wait_for_closed_subscriptions(injected_api: InjectedApi) { #[tokio::test] #[ntest::timeout(20_000)] async fn test_cleanup_promise_subscribers() { + let _ = tracing_subscriber::fmt::try_init(); + let listen_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8002); let service = MockService::new(listen_addr).await; let injected_api = service.injected_api(); diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 7eff2da700d..5ad86de8107 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -545,7 +545,11 @@ impl Service { blob_loader.load_codes(codes)?; } ComputeEvent::AnnounceComputed(computed_data) => { - consensus.receive_computed_announce(computed_data)? + if let Some(ref mut rpc) = rpc { + rpc.receive_computed_data(computed_data.clone()); + } + + consensus.receive_computed_announce(computed_data)?; } ComputeEvent::BlockPrepared(block_hash) => { consensus.receive_prepared_block(block_hash)? @@ -597,9 +601,9 @@ impl Service { let _res = response_sender.send(acceptance); } }, - NetworkEvent::PromisesBundle(bundle) => { + NetworkEvent::PromiseMessage(compact_promise) => { if let Some(rpc) = &rpc { - rpc.provide_promises_bundle(bundle); + rpc.provide_compact_promise(compact_promise); } } NetworkEvent::ValidatorIdentityUpdated(_) @@ -702,17 +706,19 @@ impl Service { ConsensusEvent::AnnounceAccepted(_) | ConsensusEvent::AnnounceRejected(_) => { // TODO #4940: consider to publish network message } - ConsensusEvent::Promises(bundle) => { + ConsensusEvent::Promises(compact_promises) => { if rpc.is_none() && network.is_none() { panic!("Promise without network or rpc"); } if let Some(rpc) = &rpc { - rpc.provide_promises_bundle(bundle.clone()); + rpc.provide_compact_promises(compact_promises.clone()); } if let Some(network) = &mut network { - network.publish_promises_bundle(bundle); + compact_promises.into_iter().for_each(|compact_promise| { + network.publish_promise(compact_promise); + }); } } }, diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 4f2e86dec65..13b7dd47aa6 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -2490,10 +2490,10 @@ async fn injected_tx_fungible_token() { // Listen for inclusion and check the expected payload. node.events() .find(|event| { - if let TestingEvent::Consensus(ConsensusEvent::Promises(bundle)) = event - && !bundle.promises.is_empty() + if let TestingEvent::Consensus(ConsensusEvent::Promises(promises)) = event + && !promises.is_empty() { - let promise_tx_hash = bundle.promises.first().unwrap().tx_hash(); + let promise_tx_hash = promises.first().unwrap().tx_hash; let promise = node .db .promise(promise_tx_hash) diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index fa926544dc0..be71ec41718 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -26,8 +26,8 @@ use ethexe_common::{ db::*, events::BlockEvent, injected::{ - AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, - PromisesNetworkBundle, SignedInjectedTransaction, + AddressedInjectedTransaction, CompactSignedPromise, InjectedTransaction, + InjectedTransactionAcceptance, SignedInjectedTransaction, }, network::VerifiedValidatorMessage, }; @@ -85,7 +85,7 @@ impl TestingNetworkInjectedEvent { #[derive(Debug, Clone, Eq, PartialEq)] pub enum TestingNetworkEvent { ValidatorMessage(VerifiedValidatorMessage), - PromisesBundle(PromisesNetworkBundle), + PromiseMessage(CompactSignedPromise), ValidatorIdentityUpdated(Address), InjectedTransaction(TestingNetworkInjectedEvent), PeerBlocked(PeerId), @@ -96,7 +96,7 @@ impl TestingNetworkEvent { fn new(event: &NetworkEvent) -> Self { match event { NetworkEvent::ValidatorMessage(message) => Self::ValidatorMessage(message.clone()), - NetworkEvent::PromisesBundle(message) => Self::PromisesBundle(message.clone()), + NetworkEvent::PromiseMessage(message) => Self::PromiseMessage(message.clone()), NetworkEvent::ValidatorIdentityUpdated(address) => { Self::ValidatorIdentityUpdated(*address) } diff --git a/gsigner/src/hash.rs b/gsigner/src/hash.rs index 6b3eebf8e42..4dc9e305381 100644 --- a/gsigner/src/hash.rs +++ b/gsigner/src/hash.rs @@ -18,8 +18,12 @@ //! Lightweight hashing helpers shared across schemes. +use crate::ToDigest; +use core::marker::PhantomData; +use parity_scale_codec::{Decode, Encode}; use sha3::{Digest as _, Keccak256}; + /// Compute the Keccak-256 hash of a byte slice. #[inline] pub fn keccak256(data: &[u8]) -> [u8; 32] { @@ -41,3 +45,44 @@ where } hasher.finalize().into() } + +/// Representing the EIP-191 hash standard. +#[derive(Debug, PartialEq, Eq, Encode, Decode)] +pub struct Eip191Hash { + hash: [u8; 32], + _phantom: PhantomData, +} + +impl Copy for Eip191Hash {} + +impl Clone for Eip191Hash { + fn clone(&self) -> Self { + *self + } +} + +impl Eip191Hash { + pub fn inner(&self) -> &[u8; 32] { + &self.hash + } +} + +impl Eip191Hash +where + T: ToDigest, +{ + /// Constructs the [`Eip191Hash`] from [`Digest`]. + pub fn new(value: &T) -> Eip191Hash { + let digest = value.to_digest(); + let mut hasher = Keccak256::new(); + + hasher.update(b"\x19Ethereum Signed Message:\n"); + hasher.update(b"32"); + hasher.update(digest.0.as_ref()); + + Eip191Hash { + hash: hasher.finalize().into(), + _phantom: PhantomData, + } + } +} diff --git a/gsigner/src/schemes/secp256k1/signature.rs b/gsigner/src/schemes/secp256k1/signature.rs index 68ccac363ed..88a559e0ebe 100644 --- a/gsigner/src/schemes/secp256k1/signature.rs +++ b/gsigner/src/schemes/secp256k1/signature.rs @@ -19,7 +19,10 @@ //! Secp256k1 signature types and utilities backed by `sp_core` primitives. use super::{Address, Digest, PrivateKey, PublicKey}; -use crate::{error::SignerError, hash::keccak256_iter}; +use crate::{ + error::SignerError, + hash::{Eip191Hash, keccak256_iter}, +}; #[cfg(feature = "serde")] use alloc::{format, string::String}; use core::hash::{Hash, Hasher}; @@ -65,6 +68,16 @@ impl Signature { Ok(Self::new(private_key.as_pair().sign_prehashed(&digest.0))) } + /// Create a recoverable signature from a precomputed digest. + pub fn create_from_eip191_hash( + private_key: &PrivateKey, + eip191_hash: Eip191Hash, + ) -> SignResult { + Ok(Self::new( + private_key.as_pair().sign_prehashed(eip191_hash.inner()), + )) + } + /// Create a recoverable signature for the provided digest using the private key according to EIP-191. pub fn create_message(private_key: &PrivateKey, data: T) -> SignResult where @@ -95,6 +108,13 @@ impl Signature { .ok_or_else(|| SignerError::Crypto("Failed to recover public key".into())) } + pub fn recover_from_eip191_hash(&self, hash: Eip191Hash) -> SignResult { + self.0 + .recover_prehashed(hash.inner()) + .map(PublicKey::from) + .ok_or_else(|| SignerError::Crypto("Failed to recover public key".into())) + } + /// Recovers public key which was used to create the signature for the signed message /// according to EIP-191 standard. pub fn recover_message(&self, data: T) -> SignResult @@ -127,6 +147,18 @@ impl Signature { } } + pub fn verify_with_eip191_hash( + &self, + public_key: PublicKey, + eip191_hash: Eip191Hash, + ) -> SignResult<()> { + if SpPair::verify_prehashed(&self.0, eip191_hash.inner(), &SpPublic::from(public_key)) { + Ok(()) + } else { + Err(SignerError::Crypto("Verification failed".into())) + } + } + /// Verifies message using [`Self::verify`] method according to EIP-191 standard. pub fn verify_message(&self, public_key: PublicKey, data: T) -> SignResult<()> where diff --git a/gsigner/src/schemes/secp256k1/signer_ext.rs b/gsigner/src/schemes/secp256k1/signer_ext.rs index 110abf0fe73..5f3314c13fc 100644 --- a/gsigner/src/schemes/secp256k1/signer_ext.rs +++ b/gsigner/src/schemes/secp256k1/signer_ext.rs @@ -25,6 +25,7 @@ use super::{ use crate::{ Signer, error::{Result, SignerError}, + hash::Eip191Hash, }; /// Extension trait for Secp256k1 signers. @@ -48,6 +49,13 @@ pub trait Secp256k1SignerExt { password: Option<&str>, ) -> Result; + fn sign_eip191_hash( + &self, + public_key: PublicKey, + eip191_hash: Eip191Hash, + password: Option<&str>, + ) -> Result; + /// Create signed data (signature + data). fn signed_data( &self, @@ -117,6 +125,17 @@ impl Secp256k1SignerExt for Signer { .map_err(|e| SignerError::Crypto(format!("Signature creation failed: {e}"))) } + fn sign_eip191_hash( + &self, + public_key: PublicKey, + eip191_hash: Eip191Hash, + password: Option<&str>, + ) -> Result { + let private_key = self.get_private_key(public_key, password)?; + Signature::create_from_eip191_hash(&private_key, eip191_hash) + .map_err(|e| SignerError::Crypto(format!("Signature creation failed: {e}"))) + } + fn signed_data( &self, public_key: PublicKey, From d4dd026c42f10cdeb12223391020f74ae3a03427 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Sun, 8 Feb 2026 11:43:14 +0700 Subject: [PATCH 03/59] fix network tests --- Cargo.lock | 1 + ethexe/common/src/db.rs | 12 +- ethexe/common/src/mock.rs | 6 + ethexe/db/src/database.rs | 24 +- ethexe/network/src/validator/topic.rs | 743 +++++++++++++------------- ethexe/rpc/Cargo.toml | 1 + ethexe/rpc/src/apis/injected.rs | 36 +- 7 files changed, 419 insertions(+), 404 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ea9a46176e..8252a234404 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5226,6 +5226,7 @@ dependencies = [ "gear-core", "gear-workspace-hack", "gprimitives", + "gsigner", "hyper 1.8.1", "jsonrpsee", "ntest", diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index f5691ed0a4c..980ceba1482 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -19,7 +19,7 @@ //! Common db types and traits. use crate::{ - Address, Announce, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, ProtocolTimelines, + Announce, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, ProtocolTimelines, Schedule, SimpleBlockData, ValidatorsVec, events::BlockEvent, gear::StateTransition, @@ -139,8 +139,7 @@ pub trait InjectedStorageRO { /// Returns the promise by its transaction hash. fn promise(&self, hash: HashOf) -> Option; - /// - fn promise_signature(&self, hash: HashOf) -> Option<(Signature, Address)>; + fn promise_signature(&self, hash: HashOf) -> Option; } #[auto_impl::auto_impl(&)] @@ -149,12 +148,7 @@ pub trait InjectedStorageRW: InjectedStorageRO { fn set_promise(&self, promise: Promise); - fn set_promise_signature( - &self, - hash: HashOf, - address: Address, - signature: Signature, - ); + fn set_promise_signature(&self, hash: HashOf, signature: Signature); } #[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Eq, Hash)] diff --git a/ethexe/common/src/mock.rs b/ethexe/common/src/mock.rs index d3183388ab5..72a08831de8 100644 --- a/ethexe/common/src/mock.rs +++ b/ethexe/common/src/mock.rs @@ -667,3 +667,9 @@ impl Mock> for Promise { } } } + +impl Mock for Promise { + fn mock(_args: ()) -> Self { + Promise::mock(HashOf::zero()) + } +} diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index 1ab6baa9aff..01de9522beb 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -23,7 +23,7 @@ use crate::{ overlay::{CASOverlay, KVOverlay}, }; use ethexe_common::{ - Address, Announce, BlockHeader, CodeBlobInfo, HashOf, ProgramStates, ProtocolTimelines, + Announce, BlockHeader, CodeBlobInfo, HashOf, ProgramStates, ProtocolTimelines, Schedule, ValidatorsVec, db::{ AnnounceMeta, AnnounceStorageRO, AnnounceStorageRW, BlockMeta, BlockMetaStorageRO, @@ -631,14 +631,11 @@ impl InjectedStorageRO for Database { }) } - fn promise_signature( - &self, - tx_hash: HashOf, - ) -> Option<(Signature, Address)> { + fn promise_signature(&self, tx_hash: HashOf) -> Option { self.kv .get(&Key::PromiseSignature(tx_hash).to_bytes()) .map(|data| { - <(Signature, Address)>::decode(&mut data.as_slice()) + Signature::decode(&mut data.as_slice()) .expect("Failed to decode data into `(Signature, Address)`") }) } @@ -660,18 +657,11 @@ impl InjectedStorageRW for Database { .put(&Key::Promise(promise.tx_hash).to_bytes(), promise.encode()) } - fn set_promise_signature( - &self, - hash: HashOf, - address: Address, - signature: Signature, - ) { - tracing::trace!(tx_hash = ?hash, ?signature, ?address, "Set `(Signature, Address)` for injected transaction promise"); + fn set_promise_signature(&self, hash: HashOf, signature: Signature) { + tracing::trace!(tx_hash = ?hash, ?signature, "Set signature for injected transaction promise"); - self.kv.put( - &Key::PromiseSignature(hash).to_bytes(), - (signature, address).encode(), - ); + self.kv + .put(&Key::PromiseSignature(hash).to_bytes(), signature.encode()); } } diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index 15685cfb788..a4a805e7633 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -315,369 +315,380 @@ impl ValidatorTopic { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use assert_matches::assert_matches; -// use ethexe_common::{ -// Announce, -// gear_core::{message::ReplyCode, rpc::ReplyInfo}, -// injected::Promise, -// mock::Mock, -// network::{SignedValidatorMessage, ValidatorMessage}, -// }; -// use gsigner::secp256k1::{Secp256k1SignerExt, Signer}; -// use nonempty::{NonEmpty, nonempty}; - -// const CHAIN_HEAD_ERA: u64 = 10; - -// fn new_snapshot( -// current_era_index: u64, -// current_validators: NonEmpty
, -// ) -> Arc { -// Arc::new(ValidatorListSnapshot { -// current_era_index, -// current_validators: current_validators.into(), -// next_validators: None, -// }) -// } - -// fn new_topic(validators: NonEmpty
) -> ValidatorTopic { -// ValidatorTopic::new( -// peer_score::Handle::new_test(), -// new_snapshot(CHAIN_HEAD_ERA, validators), -// ) -// } - -// fn new_validator_message(era_index: u64) -> VerifiedValidatorMessage { -// let signer = Signer::memory(); -// let pub_key = signer.generate().unwrap(); - -// signer -// .signed_data( -// pub_key, -// ValidatorMessage { -// era_index, -// payload: Announce::mock(()), -// }, -// None, -// ) -// .map(SignedValidatorMessage::from) -// .unwrap() -// .into_verified() -// } - -// fn signed_promise() -> SignedPromise { -// let signer = Signer::memory(); -// let pub_key = signer.generate().unwrap(); -// let promise = Promise { -// tx_hash: Default::default(), -// reply: ReplyInfo { -// payload: vec![], -// value: 0, -// code: ReplyCode::Unsupported, -// }, -// }; - -// signer.signed_message(pub_key, promise, None).unwrap() -// } - -// #[test] -// fn too_old_era() { -// let bob_message = new_validator_message(CHAIN_HEAD_ERA - 2); -// let mut alice = new_topic(nonempty![bob_message.address()]); - -// let err = alice -// .inner_verify_validator_message(&bob_message) -// .unwrap_err() -// .unwrap_reject(); -// assert_eq!( -// err, -// VerificationRejectReason::TooOldEra { -// expected_era: CHAIN_HEAD_ERA, -// received_era: CHAIN_HEAD_ERA - 2 -// } -// ); - -// let bob_source = PeerId::random(); -// let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); -// assert_matches!(acceptance, MessageAcceptance::Reject); -// assert_eq!(verified_msg, None); -// assert_eq!(alice.cached_messages.len(), 0); -// assert_eq!(alice.next_message(), None); -// } - -// #[test] -// fn old_era() { -// let bob_message = new_validator_message(CHAIN_HEAD_ERA - 1); -// let mut alice = new_topic(nonempty![bob_message.address()]); - -// let err = alice -// .inner_verify_validator_message(&bob_message) -// .unwrap_err() -// .unwrap_ignore(); -// assert_eq!( -// err, -// VerificationIgnoreReason::OldEra { -// expected_era: CHAIN_HEAD_ERA, -// received_era: CHAIN_HEAD_ERA - 1 -// } -// ); - -// let bob_source = PeerId::random(); -// let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); -// assert_matches!(acceptance, MessageAcceptance::Ignore); -// assert_eq!(verified_msg, None); -// assert_eq!(alice.cached_messages.len(), 0); -// assert_eq!(alice.next_message(), None); -// } - -// #[test] -// fn too_new_era() { -// let bob_message = new_validator_message(CHAIN_HEAD_ERA + 2); -// let mut alice = new_topic(nonempty![bob_message.address()]); - -// let err = alice -// .inner_verify_validator_message(&bob_message) -// .unwrap_err() -// .unwrap_reject(); -// assert_eq!( -// err, -// VerificationRejectReason::TooNewEra { -// expected_era: CHAIN_HEAD_ERA, -// received_era: CHAIN_HEAD_ERA + 2 -// } -// ); - -// let bob_source = PeerId::random(); -// let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); -// assert_matches!(acceptance, MessageAcceptance::Reject); -// assert_eq!(verified_msg, None); -// assert_eq!(alice.cached_messages.len(), 0); -// } - -// #[test] -// fn new_era() { -// const BOB_BLOCK_ERA: u64 = CHAIN_HEAD_ERA + 1; - -// let bob_message = new_validator_message(BOB_BLOCK_ERA); -// let snapshot = ValidatorListSnapshot { -// current_era_index: CHAIN_HEAD_ERA, -// current_validators: Default::default(), -// next_validators: Some(nonempty![bob_message.address()].into()), -// }; -// let mut alice = ValidatorTopic::new(peer_score::Handle::new_test(), Arc::new(snapshot)); - -// let err = alice -// .inner_verify_validator_message(&bob_message) -// .unwrap_err() -// .unwrap_cache(); -// assert_eq!( -// err, -// VerificationCacheReason::NewEra { -// expected_era: CHAIN_HEAD_ERA, -// received_era: CHAIN_HEAD_ERA + 1 -// } -// ); - -// let bob_source = PeerId::random(); -// let (acceptance, verified_msg) = -// alice.verify_validator_message(bob_source, bob_message.clone()); -// assert_matches!(acceptance, MessageAcceptance::Ignore); -// assert_eq!(verified_msg, None); -// assert_eq!(alice.cached_messages.len(), 1); - -// let snapshot = new_snapshot(BOB_BLOCK_ERA, nonempty![bob_message.address()]); -// alice.on_new_snapshot(snapshot); - -// assert_eq!(alice.next_message(), Some(bob_message)); -// } - -// #[test] -// fn current_era_address_is_not_validator() { -// let mut alice = new_topic(nonempty![Address::default()]); -// let bob_message = new_validator_message(CHAIN_HEAD_ERA); - -// let err = alice -// .inner_verify_validator_message(&bob_message) -// .unwrap_err() -// .unwrap_reject(); -// assert_eq!( -// err, -// VerificationRejectReason::AddressIsNotValidator { -// address: bob_message.address() -// } -// ); - -// let bob_source = PeerId::random(); -// let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); -// assert_matches!(acceptance, MessageAcceptance::Reject); -// assert_eq!(verified_msg, None); -// assert_eq!(alice.cached_messages.len(), 0); -// assert_eq!(alice.next_message(), None); -// } - -// #[test] -// fn next_era_address_is_not_validator() { -// let mut alice = new_topic(nonempty![Address::default()]); -// let bob_message = new_validator_message(CHAIN_HEAD_ERA + 1); - -// let err = alice -// .inner_verify_validator_message(&bob_message) -// .unwrap_err() -// .unwrap_reject(); -// assert_eq!( -// err, -// VerificationRejectReason::AddressIsNotValidator { -// address: bob_message.address() -// } -// ); - -// let bob_source = PeerId::random(); -// let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); -// assert_matches!(acceptance, MessageAcceptance::Reject); -// assert_eq!(verified_msg, None); -// assert_eq!(alice.cached_messages.len(), 0); -// assert_eq!(alice.next_message(), None); -// } - -// #[test] -// fn new_era_address_is_not_validator() { -// let bob_message = new_validator_message(CHAIN_HEAD_ERA); -// let charlie_message = new_validator_message(CHAIN_HEAD_ERA + 1); - -// let mut alice = new_topic(nonempty![Default::default()]); - -// for message in [bob_message, charlie_message] { -// let err = alice -// .inner_verify_validator_message(&message) -// .unwrap_err() -// .unwrap_reject(); -// assert_eq!( -// err, -// VerificationRejectReason::AddressIsNotValidator { -// address: message.address() -// } -// ); - -// let bob_source = PeerId::random(); -// let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, message); -// assert_matches!(acceptance, MessageAcceptance::Reject); -// assert_eq!(verified_msg, None); -// assert_eq!(alice.cached_messages.len(), 0); -// assert_eq!(alice.next_message(), None); -// } -// } - -// #[test] -// fn success() { -// let bob_message = new_validator_message(CHAIN_HEAD_ERA); -// let mut alice = new_topic(nonempty![bob_message.address()]); - -// alice.inner_verify_validator_message(&bob_message).unwrap(); - -// let bob_source = PeerId::random(); -// let (acceptance, verified_msg) = -// alice.verify_validator_message(bob_source, bob_message.clone()); -// assert_matches!(acceptance, MessageAcceptance::Accept); -// assert_eq!(verified_msg, Some(bob_message)); -// } - -// #[test] -// fn next_validators_arrive_later() { -// const NEXT_ERA: u64 = CHAIN_HEAD_ERA + 1; - -// // current set validators -// let bob_message = new_validator_message(CHAIN_HEAD_ERA); -// let charlie_message = new_validator_message(CHAIN_HEAD_ERA); -// // new validator in next set -// let dave_message = new_validator_message(NEXT_ERA); - -// let bob_source = PeerId::random(); -// let charlie_source = PeerId::random(); -// let dave_source = PeerId::random(); - -// let mut alice = new_topic(nonempty![bob_message.address(), charlie_message.address()]); - -// let (bob_acceptance, bob_verified_msg) = -// alice.verify_validator_message(bob_source, bob_message.clone()); -// assert_matches!(bob_acceptance, MessageAcceptance::Accept); -// assert_eq!(bob_verified_msg, Some(bob_message.clone())); - -// let (charlie_acceptance, charlie_verified_msg) = -// alice.verify_validator_message(charlie_source, charlie_message.clone()); -// assert_matches!(charlie_acceptance, MessageAcceptance::Accept); -// assert_eq!(charlie_verified_msg, Some(charlie_message.clone())); - -// // we have no next validators yet, so the message should be rejected -// let (dave_acceptance, dave_verified_msg) = -// alice.verify_validator_message(dave_source, dave_message.clone()); -// assert_matches!(dave_acceptance, MessageAcceptance::Reject); -// assert!(dave_verified_msg.is_none()); -// assert_eq!(alice.cached_messages.len(), 0); - -// // Dave now is in the next validator set -// let snapshot = ValidatorListSnapshot { -// current_era_index: CHAIN_HEAD_ERA, -// current_validators: Default::default(), -// next_validators: Some(nonempty![dave_message.address()].into()), -// }; -// alice.on_new_snapshot(Arc::new(snapshot)); - -// // Dave's message is cached -// let (dave_acceptance, dave_verified_msg) = -// alice.verify_validator_message(dave_source, dave_message.clone()); -// assert_matches!(dave_acceptance, MessageAcceptance::Ignore); -// assert!(dave_verified_msg.is_none()); -// assert_eq!(alice.cached_messages.len(), 1); - -// // Dave's message now is in the current validator set -// let snapshot = ValidatorListSnapshot { -// current_era_index: NEXT_ERA, -// current_validators: nonempty![dave_message.address()].into(), -// next_validators: None, -// }; -// alice.on_new_snapshot(Arc::new(snapshot)); - -// let dave_verified_msg = alice.next_message().unwrap(); -// assert_eq!(dave_verified_msg, dave_message); -// } - -// #[test] -// fn verify_promise_unknown_validator() { -// let topic = new_topic(nonempty![Address::default()]); -// let promise = signed_promise(); -// let peer_id = PeerId::random(); - -// let err = topic -// .inner_verify_promise(peer_id, promise.clone()) -// .unwrap_err(); -// assert_eq!( -// err, -// VerifyPromiseError::UnknownValidator { -// address: promise.address(), -// tx_hash: promise.data().tx_hash, -// } -// ); - -// let (acceptance, promise) = topic.verify_promise(peer_id, promise); -// assert_matches!(acceptance, MessageAcceptance::Ignore); -// assert_eq!(promise, None); -// } - -// #[ignore = "TODO"] -// #[tokio::test] -// async fn verify_promise_ok() { -// let promise = signed_promise(); -// let topic = new_topic(nonempty![promise.address()]); -// let peer_id = PeerId::random(); - -// topic -// .inner_verify_promise(peer_id, promise.clone()) -// .unwrap(); - -// let (acceptance, returned_promise) = topic.verify_promises_bundle(peer_id, promise.clone()); -// assert_matches!(acceptance, MessageAcceptance::Accept); -// assert_eq!(returned_promise, Some(promise)); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + use ethexe_common::{ + self, Announce, + injected::{Promise, SignedPromise}, + mock::Mock, + network::{SignedValidatorMessage, ValidatorMessage}, + }; + use gsigner::{ + PublicKey, + secp256k1::{Secp256k1SignerExt, Signer}, + }; + use nonempty::{NonEmpty, nonempty}; + + const CHAIN_HEAD_ERA: u64 = 10; + + fn new_snapshot( + current_era_index: u64, + current_validators: NonEmpty
, + ) -> Arc { + Arc::new(ValidatorListSnapshot { + current_era_index, + current_validators: current_validators.into(), + next_validators: None, + }) + } + + fn new_topic(validators: NonEmpty
) -> ValidatorTopic { + ValidatorTopic::new( + peer_score::Handle::new_test(), + new_snapshot(CHAIN_HEAD_ERA, validators), + ) + } + + fn new_validator_message(era_index: u64) -> VerifiedValidatorMessage { + let signer = Signer::memory(); + let pub_key = signer.generate().unwrap(); + + signer + .signed_data( + pub_key, + ValidatorMessage { + era_index, + payload: Announce::mock(()), + }, + None, + ) + .map(SignedValidatorMessage::from) + .unwrap() + .into_verified() + } + + fn signer_with_pubkey() -> (PublicKey, Signer) { + let signer = Signer::memory(); + (signer.generate().unwrap(), signer) + } + + fn signed_promise(signer: Signer, public_key: PublicKey) -> SignedPromise { + let promise = Promise::mock(()); + signer.signed_message(public_key, promise, None).unwrap() + } + + fn compact_signed_promise( + signer: &Signer, + public_key: PublicKey, + promise: Promise, + ) -> CompactSignedPromise { + CompactSignedPromise::create(signer, public_key, promise).unwrap() + } + + #[test] + fn too_old_era() { + let bob_message = new_validator_message(CHAIN_HEAD_ERA - 2); + let mut alice = new_topic(nonempty![bob_message.address()]); + + let err = alice + .inner_verify_validator_message(&bob_message) + .unwrap_err() + .unwrap_reject(); + assert_eq!( + err, + VerificationRejectReason::TooOldEra { + expected_era: CHAIN_HEAD_ERA, + received_era: CHAIN_HEAD_ERA - 2 + } + ); + + let bob_source = PeerId::random(); + let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); + assert_matches!(acceptance, MessageAcceptance::Reject); + assert_eq!(verified_msg, None); + assert_eq!(alice.cached_messages.len(), 0); + assert_eq!(alice.next_message(), None); + } + + #[test] + fn old_era() { + let bob_message = new_validator_message(CHAIN_HEAD_ERA - 1); + let mut alice = new_topic(nonempty![bob_message.address()]); + + let err = alice + .inner_verify_validator_message(&bob_message) + .unwrap_err() + .unwrap_ignore(); + assert_eq!( + err, + VerificationIgnoreReason::OldEra { + expected_era: CHAIN_HEAD_ERA, + received_era: CHAIN_HEAD_ERA - 1 + } + ); + + let bob_source = PeerId::random(); + let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); + assert_matches!(acceptance, MessageAcceptance::Ignore); + assert_eq!(verified_msg, None); + assert_eq!(alice.cached_messages.len(), 0); + assert_eq!(alice.next_message(), None); + } + + #[test] + fn too_new_era() { + let bob_message = new_validator_message(CHAIN_HEAD_ERA + 2); + let mut alice = new_topic(nonempty![bob_message.address()]); + + let err = alice + .inner_verify_validator_message(&bob_message) + .unwrap_err() + .unwrap_reject(); + assert_eq!( + err, + VerificationRejectReason::TooNewEra { + expected_era: CHAIN_HEAD_ERA, + received_era: CHAIN_HEAD_ERA + 2 + } + ); + + let bob_source = PeerId::random(); + let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); + assert_matches!(acceptance, MessageAcceptance::Reject); + assert_eq!(verified_msg, None); + assert_eq!(alice.cached_messages.len(), 0); + } + + #[test] + fn new_era() { + const BOB_BLOCK_ERA: u64 = CHAIN_HEAD_ERA + 1; + + let bob_message = new_validator_message(BOB_BLOCK_ERA); + let snapshot = ValidatorListSnapshot { + current_era_index: CHAIN_HEAD_ERA, + current_validators: Default::default(), + next_validators: Some(nonempty![bob_message.address()].into()), + }; + let mut alice = ValidatorTopic::new(peer_score::Handle::new_test(), Arc::new(snapshot)); + + let err = alice + .inner_verify_validator_message(&bob_message) + .unwrap_err() + .unwrap_cache(); + assert_eq!( + err, + VerificationCacheReason::NewEra { + expected_era: CHAIN_HEAD_ERA, + received_era: CHAIN_HEAD_ERA + 1 + } + ); + + let bob_source = PeerId::random(); + let (acceptance, verified_msg) = + alice.verify_validator_message(bob_source, bob_message.clone()); + assert_matches!(acceptance, MessageAcceptance::Ignore); + assert_eq!(verified_msg, None); + assert_eq!(alice.cached_messages.len(), 1); + + let snapshot = new_snapshot(BOB_BLOCK_ERA, nonempty![bob_message.address()]); + alice.on_new_snapshot(snapshot); + + assert_eq!(alice.next_message(), Some(bob_message)); + } + + #[test] + fn current_era_address_is_not_validator() { + let mut alice = new_topic(nonempty![Address::default()]); + let bob_message = new_validator_message(CHAIN_HEAD_ERA); + + let err = alice + .inner_verify_validator_message(&bob_message) + .unwrap_err() + .unwrap_reject(); + assert_eq!( + err, + VerificationRejectReason::AddressIsNotValidator { + address: bob_message.address() + } + ); + + let bob_source = PeerId::random(); + let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); + assert_matches!(acceptance, MessageAcceptance::Reject); + assert_eq!(verified_msg, None); + assert_eq!(alice.cached_messages.len(), 0); + assert_eq!(alice.next_message(), None); + } + + #[test] + fn next_era_address_is_not_validator() { + let mut alice = new_topic(nonempty![Address::default()]); + let bob_message = new_validator_message(CHAIN_HEAD_ERA + 1); + + let err = alice + .inner_verify_validator_message(&bob_message) + .unwrap_err() + .unwrap_reject(); + assert_eq!( + err, + VerificationRejectReason::AddressIsNotValidator { + address: bob_message.address() + } + ); + + let bob_source = PeerId::random(); + let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, bob_message); + assert_matches!(acceptance, MessageAcceptance::Reject); + assert_eq!(verified_msg, None); + assert_eq!(alice.cached_messages.len(), 0); + assert_eq!(alice.next_message(), None); + } + + #[test] + fn new_era_address_is_not_validator() { + let bob_message = new_validator_message(CHAIN_HEAD_ERA); + let charlie_message = new_validator_message(CHAIN_HEAD_ERA + 1); + + let mut alice = new_topic(nonempty![Default::default()]); + + for message in [bob_message, charlie_message] { + let err = alice + .inner_verify_validator_message(&message) + .unwrap_err() + .unwrap_reject(); + assert_eq!( + err, + VerificationRejectReason::AddressIsNotValidator { + address: message.address() + } + ); + + let bob_source = PeerId::random(); + let (acceptance, verified_msg) = alice.verify_validator_message(bob_source, message); + assert_matches!(acceptance, MessageAcceptance::Reject); + assert_eq!(verified_msg, None); + assert_eq!(alice.cached_messages.len(), 0); + assert_eq!(alice.next_message(), None); + } + } + + #[test] + fn success() { + let bob_message = new_validator_message(CHAIN_HEAD_ERA); + let mut alice = new_topic(nonempty![bob_message.address()]); + + alice.inner_verify_validator_message(&bob_message).unwrap(); + + let bob_source = PeerId::random(); + let (acceptance, verified_msg) = + alice.verify_validator_message(bob_source, bob_message.clone()); + assert_matches!(acceptance, MessageAcceptance::Accept); + assert_eq!(verified_msg, Some(bob_message)); + } + + #[test] + fn next_validators_arrive_later() { + const NEXT_ERA: u64 = CHAIN_HEAD_ERA + 1; + + // current set validators + let bob_message = new_validator_message(CHAIN_HEAD_ERA); + let charlie_message = new_validator_message(CHAIN_HEAD_ERA); + // new validator in next set + let dave_message = new_validator_message(NEXT_ERA); + + let bob_source = PeerId::random(); + let charlie_source = PeerId::random(); + let dave_source = PeerId::random(); + + let mut alice = new_topic(nonempty![bob_message.address(), charlie_message.address()]); + + let (bob_acceptance, bob_verified_msg) = + alice.verify_validator_message(bob_source, bob_message.clone()); + assert_matches!(bob_acceptance, MessageAcceptance::Accept); + assert_eq!(bob_verified_msg, Some(bob_message.clone())); + + let (charlie_acceptance, charlie_verified_msg) = + alice.verify_validator_message(charlie_source, charlie_message.clone()); + assert_matches!(charlie_acceptance, MessageAcceptance::Accept); + assert_eq!(charlie_verified_msg, Some(charlie_message.clone())); + + // we have no next validators yet, so the message should be rejected + let (dave_acceptance, dave_verified_msg) = + alice.verify_validator_message(dave_source, dave_message.clone()); + assert_matches!(dave_acceptance, MessageAcceptance::Reject); + assert!(dave_verified_msg.is_none()); + assert_eq!(alice.cached_messages.len(), 0); + + // Dave now is in the next validator set + let snapshot = ValidatorListSnapshot { + current_era_index: CHAIN_HEAD_ERA, + current_validators: Default::default(), + next_validators: Some(nonempty![dave_message.address()].into()), + }; + alice.on_new_snapshot(Arc::new(snapshot)); + + // Dave's message is cached + let (dave_acceptance, dave_verified_msg) = + alice.verify_validator_message(dave_source, dave_message.clone()); + assert_matches!(dave_acceptance, MessageAcceptance::Ignore); + assert!(dave_verified_msg.is_none()); + assert_eq!(alice.cached_messages.len(), 1); + + // Dave's message now is in the current validator set + let snapshot = ValidatorListSnapshot { + current_era_index: NEXT_ERA, + current_validators: nonempty![dave_message.address()].into(), + next_validators: None, + }; + alice.on_new_snapshot(Arc::new(snapshot)); + + let dave_verified_msg = alice.next_message().unwrap(); + assert_eq!(dave_verified_msg, dave_message); + } + + #[test] + fn verify_promise_unknown_validator() { + let topic = new_topic(nonempty![Address::default()]); + + let (pubkey, signer) = signer_with_pubkey(); + let promise = signed_promise(signer.clone(), pubkey); + let compact_promise = compact_signed_promise(&signer, pubkey, promise.clone().into_data()); + + let peer_id = PeerId::random(); + + let err = topic + .inner_verify_promise(peer_id, compact_promise.clone()) + .unwrap_err(); + + assert!(matches!(err, VerifyPromiseError::UnknownValidator { .. })); + if let VerifyPromiseError::UnknownValidator { address, tx_hash } = err { + assert_eq!(address, promise.address()); + assert_eq!(tx_hash, promise.data().tx_hash); + } + + let (acceptance, promise) = topic.verify_promise(peer_id, compact_promise); + assert_matches!(acceptance, MessageAcceptance::Ignore); + assert_eq!(promise, None); + } + + #[ignore = "TODO"] + #[tokio::test] + async fn verify_promise_ok() { + let (pubkey, signer) = signer_with_pubkey(); + let promise = signed_promise(signer.clone(), pubkey); + let compact_promise = compact_signed_promise(&signer, pubkey, promise.clone().into_data()); + + let topic = new_topic(nonempty![promise.address()]); + let peer_id = PeerId::random(); + + topic + .inner_verify_promise(peer_id, compact_promise.clone()) + .unwrap(); + + let (acceptance, returned_promise) = topic.verify_promise(peer_id, compact_promise.clone()); + assert_matches!(acceptance, MessageAcceptance::Accept); + assert_eq!(returned_promise, Some(compact_promise)); + } +} diff --git a/ethexe/rpc/Cargo.toml b/ethexe/rpc/Cargo.toml index 77617926545..47695019c55 100644 --- a/ethexe/rpc/Cargo.toml +++ b/ethexe/rpc/Cargo.toml @@ -29,6 +29,7 @@ serde = { workspace = true, features = ["std"] } tracing.workspace = true dashmap.workspace = true gear-workspace-hack.workspace = true +gsigner.workspace = true [dev-dependencies] jsonrpsee = { workspace = true, features = ["client"] } diff --git a/ethexe/rpc/src/apis/injected.rs b/ethexe/rpc/src/apis/injected.rs index cd167b2266e..309ef87086e 100644 --- a/ethexe/rpc/src/apis/injected.rs +++ b/ethexe/rpc/src/apis/injected.rs @@ -20,13 +20,15 @@ use crate::{RpcEvent, errors}; use dashmap::DashMap; use ethexe_common::{ HashOf, SignedMessage, - db::InjectedStorageRO, + db::{InjectedStorageRO, InjectedStorageRW}, injected::{ AddressedInjectedTransaction, CompactSignedPromise, InjectedTransaction, InjectedTransactionAcceptance, Promise, SignedPromise, }, }; + use ethexe_db::Database; +use gsigner::hash::Eip191Hash; use jsonrpsee::{ PendingSubscriptionSink, SubscriptionMessage, SubscriptionSink, core::{RpcResult, SubscriptionResult, async_trait}, @@ -76,7 +78,7 @@ pub struct InjectedApi { /// Map of promise waiters. promise_waiters: PromiseWaiters, - /// + /// The mapping from injected transaction hash to its promise signature. promises_computation_waiting: Arc, CompactSignedPromise>>, } @@ -133,11 +135,16 @@ impl InjectedServer for InjectedApi { return Ok(None); }; - let Some((signature, address)) = self.db.promise_signature(tx_hash) else { + let Some(signature) = self.db.promise_signature(tx_hash) else { return Ok(None); }; - match SignedMessage::try_from_parts(promise, signature, address) { + let promise_eip191_hash = Eip191Hash::new(&promise); + let Ok(public_key) = signature.recover_from_eip191_hash(promise_eip191_hash) else { + todo!() + }; + + match SignedMessage::try_from_parts(promise, signature, public_key.to_address()) { Ok(message) => Ok(Some(message)), Err(_err) => { tracing::trace!(""); @@ -177,16 +184,28 @@ impl InjectedApi { if let Some((_, compact_promise)) = self.promises_computation_waiting.remove(&promise.tx_hash) { + self.db + .set_promise_signature(promise.tx_hash, compact_promise.signature); self.send_promise(promise, compact_promise); } }) } pub fn send_promise(&self, promise: Promise, compact_promise: CompactSignedPromise) { + // Check the promise waiter firstly to avoid unnecessary computation. + let Some((_, promise_sender)) = self.promise_waiters.remove(&compact_promise.tx_hash) + else { + tracing::warn!( + ?promise, + "receive unregistered promise for injected transaction" + ); + return; + }; + let CompactSignedPromise { - tx_hash, signature, eip191_hash, + .. } = compact_promise; let Ok(public_key) = signature.recover_from_eip191_hash(eip191_hash) else { @@ -194,7 +213,6 @@ impl InjectedApi { }; let address = public_key.to_address(); - // TODO: remove clone here. let Ok(message) = SignedMessage::try_from_parts(promise.clone(), signature, address) else { tracing::trace!( ?promise, @@ -204,12 +222,6 @@ impl InjectedApi { todo!("handle invalid signature case"); }; - let Some((_, promise_sender)) = self.promise_waiters.remove(&compact_promise.tx_hash) - else { - tracing::warn!(?tx_hash, promise = ?promise, "receive unregistered promise for injected transaction"); - return; - }; - if let Err(promise) = promise_sender.send(message) { tracing::trace!(promise = ?promise, "rpc promise receiver dropped"); } From 9d788d3af423e597602b62e82f99a6b5c51e5383 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 9 Feb 2026 17:22:51 +0700 Subject: [PATCH 04/59] fix format --- ethexe/db/src/database.rs | 4 ++-- gsigner/src/hash.rs | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index 01de9522beb..9777d93f582 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -23,8 +23,8 @@ use crate::{ overlay::{CASOverlay, KVOverlay}, }; use ethexe_common::{ - Announce, BlockHeader, CodeBlobInfo, HashOf, ProgramStates, ProtocolTimelines, - Schedule, ValidatorsVec, + Announce, BlockHeader, CodeBlobInfo, HashOf, ProgramStates, ProtocolTimelines, Schedule, + ValidatorsVec, db::{ AnnounceMeta, AnnounceStorageRO, AnnounceStorageRW, BlockMeta, BlockMetaStorageRO, BlockMetaStorageRW, CodesStorageRO, CodesStorageRW, HashStorageRO, InjectedStorageRO, diff --git a/gsigner/src/hash.rs b/gsigner/src/hash.rs index 4dc9e305381..d98ec9c0a6b 100644 --- a/gsigner/src/hash.rs +++ b/gsigner/src/hash.rs @@ -23,7 +23,6 @@ use core::marker::PhantomData; use parity_scale_codec::{Decode, Encode}; use sha3::{Digest as _, Keccak256}; - /// Compute the Keccak-256 hash of a byte slice. #[inline] pub fn keccak256(data: &[u8]) -> [u8; 32] { From a8448558c2f9d1dd7df828a29424467bd95738de Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 11 Feb 2026 13:18:20 +0700 Subject: [PATCH 05/59] redisign CompactSignedPromise --- ethexe/common/src/db.rs | 11 +- ethexe/common/src/injected.rs | 142 +++++++++++--------- ethexe/consensus/src/validator/producer.rs | 9 +- ethexe/db/src/database.rs | 21 ++- ethexe/network/src/validator/topic.rs | 42 ++---- ethexe/rpc/src/apis/injected.rs | 48 +++---- ethexe/rpc/src/tests.rs | 5 +- ethexe/service/src/tests/mod.rs | 2 +- gsigner/src/schemes/secp256k1/signature.rs | 12 +- gsigner/src/schemes/secp256k1/signer_ext.rs | 33 +++-- 10 files changed, 168 insertions(+), 157 deletions(-) diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index 980ceba1482..53ff3c8fe1d 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -19,7 +19,7 @@ //! Common db types and traits. use crate::{ - Announce, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, ProtocolTimelines, + Address, Announce, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, ProtocolTimelines, Schedule, SimpleBlockData, ValidatorsVec, events::BlockEvent, gear::StateTransition, @@ -139,7 +139,7 @@ pub trait InjectedStorageRO { /// Returns the promise by its transaction hash. fn promise(&self, hash: HashOf) -> Option; - fn promise_signature(&self, hash: HashOf) -> Option; + fn promise_signature(&self, hash: HashOf) -> Option<(Signature, Address)>; } #[auto_impl::auto_impl(&)] @@ -148,7 +148,12 @@ pub trait InjectedStorageRW: InjectedStorageRO { fn set_promise(&self, promise: Promise); - fn set_promise_signature(&self, hash: HashOf, signature: Signature); + fn set_promise_signature( + &self, + hash: HashOf, + signature: Signature, + address: Address, + ); } #[derive(Debug, Clone, Default, Encode, Decode, PartialEq, Eq, Hash)] diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index c12420edc49..7ba76cab285 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -21,14 +21,8 @@ use alloc::string::{String, ToString}; use core::hash::Hash; use gear_core::rpc::ReplyInfo; use gprimitives::{ActorId, H256, MessageId}; -#[cfg(feature = "std")] -use gsigner::{ - PrivateKey, PublicKey, SignerError, - secp256k1::{Secp256k1SignerExt, Signer}, -}; -use gsigner::{Signature, hash::Eip191Hash}; use parity_scale_codec::{Decode, Encode}; -use sha3::{Digest, Keccak256}; +use sha3::{Digest as _, Keccak256}; use sp_core::Bytes; /// Recent block hashes window size used to check transaction mortality. @@ -139,83 +133,72 @@ pub struct Promise { /// It will be shared among other validators as a proof of promise. pub type SignedPromise = SignedMessage; -impl ToDigest for Promise { - fn update_hasher(&self, hasher: &mut sha3::Keccak256) { - let Self { tx_hash, reply } = self; +/// A wrapper on top of [`CompactPromiseHashes`]. +/// +/// [`CompactPromiseHashes`] is a lightweight version of [`SignedPromise`], that is +/// needed to reduce the amount of data transferred in network between validators. +pub type CompactSignedPromise = SignedMessage; - hasher.update(tx_hash.inner()); +impl Promise { + /// Calculates the `blake2b` hash from promise's reply. + pub fn reply_hash(&self) -> HashOf { let ReplyInfo { payload, code, value, - } = reply; + } = &self.reply; - hasher.update(payload); - hasher.update(code.to_bytes()); - hasher.update(value.to_be_bytes()); + let bytes = [ + payload.as_ref(), + code.to_bytes().as_ref(), + value.to_be_bytes().as_ref(), + ] + .concat(); + unsafe { HashOf::new(gear_core::utils::hash(&bytes).into()) } } } -// The bundle of signed [`Promise`]s was produced in concrete announce. -// #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -// pub struct PromisesNetworkBundle { -// /// The hash of [`Announce`] for which promises was created. -// pub announce: HashOf, -// /// The hashes of transactions with signatures -// pub promises: Vec, -// } - -// impl ToDigest for PromisesNetworkBundle { -// fn update_hasher(&self, hasher: &mut sha3::Keccak256) { -// todo!() -// } -// } - -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -pub struct CompactSignedPromise { - /// The hash of transaction, for which promise was created. - pub tx_hash: HashOf, - /// The [`Eip191Hash`] of the promise, was signed by block producer. - /// Important: this hash is needed to verify the signature correctness - /// without having a full promise body. - pub eip191_hash: Eip191Hash, - /// The signature over the [`Promise`] for `tx_hash`. - pub signature: Signature, +impl ToDigest for Promise { + fn update_hasher(&self, hasher: &mut sha3::Keccak256) { + // The hash of `Promise` equals to hash of `CompactPromiseHashes`. + CompactPromiseHashes::from(self).update_hasher(hasher); + } } -#[cfg(feature = "std")] -impl CompactSignedPromise { - pub fn create( - signer: &Signer, - public_key: PublicKey, - promise: Promise, - ) -> Result { - let eip191_hash = Eip191Hash::new(&promise); +/// The hashes of [`Promise`]. +#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)] +pub struct CompactPromiseHashes { + pub tx_hash: HashOf, + pub reply_hash: HashOf, +} - Ok(Self { +impl From<&Promise> for CompactPromiseHashes { + fn from(promise: &Promise) -> Self { + Self { tx_hash: promise.tx_hash, - signature: signer.sign_eip191_hash(public_key, eip191_hash, None)?, - eip191_hash, - }) + reply_hash: promise.reply_hash(), + } } +} - pub fn create_from_private_key( - private_key: &PrivateKey, - promise: Promise, - ) -> Result { - let eip191_hash = Eip191Hash::new(&promise); +impl ToDigest for CompactPromiseHashes { + fn update_hasher(&self, hasher: &mut sha3::Keccak256) { + let Self { + tx_hash, + reply_hash, + } = self; - Ok(Self { - tx_hash: promise.tx_hash, - eip191_hash, - signature: Signature::create_from_eip191_hash(private_key, eip191_hash)?, - }) + hasher.update(tx_hash.inner()); + hasher.update(reply_hash.inner()); } } -#[cfg(test)] +#[cfg(all(test, feature = "mock"))] mod tests { + use gsigner::PrivateKey; + use super::*; + use crate::mock::Mock; #[test] fn signed_message_and_injected_transactions() { @@ -254,4 +237,37 @@ mod tests { signed_tx.address() ); } + + #[test] + fn promise_hashes_digest_equal_to_promise_digest() { + let promise = { + let mut promise = Promise::mock(()); + promise.reply.value = 123; + promise.reply.payload = vec![1u8, 2u8, 42u8, 66u8]; + promise + }; + let promise_digest = promise.to_digest(); + let promise_hashes = CompactPromiseHashes::from(&promise); + + let promise_hashes_digest = promise_hashes.to_digest(); + assert_eq!(promise_digest, promise_hashes_digest); + } + + #[test] + fn compact_signature_valid_for_promise() { + let pk = PrivateKey::random(); + + let promise = Promise::mock(()); + let promise_hashes = CompactPromiseHashes::from(&promise); + let compact_signed_promise = CompactSignedPromise::create(pk, promise_hashes).unwrap(); + + let (signature, address) = ( + *compact_signed_promise.signature(), + compact_signed_promise.address(), + ); + + let signed_promise = SignedMessage::try_from_parts(promise.clone(), signature, address) + .expect("SignedMessage was correctly constructed from CompactSignedPromise"); + assert_eq!(signed_promise.into_data(), promise); + } } diff --git a/ethexe/consensus/src/validator/producer.rs b/ethexe/consensus/src/validator/producer.rs index 8b84153d5d9..c479b3ce17f 100644 --- a/ethexe/consensus/src/validator/producer.rs +++ b/ethexe/consensus/src/validator/producer.rs @@ -28,7 +28,7 @@ use anyhow::{Result, anyhow}; use derive_more::{Debug, Display}; use ethexe_common::{ Announce, ComputedAnnounce, HashOf, SimpleBlockData, ValidatorsVec, db::BlockMetaStorageRO, - gear::BatchCommitment, injected::CompactSignedPromise, network::ValidatorMessage, + gear::BatchCommitment, injected::CompactPromiseHashes, network::ValidatorMessage, }; use ethexe_service_utils::Timer; use futures::{FutureExt, future::BoxFuture}; @@ -86,10 +86,11 @@ impl StateHandler for Producer { .promises .into_iter() .map(|promise| { - CompactSignedPromise::create( - &self.ctx.core.signer, + let compact_hashes = CompactPromiseHashes::from(&promise); + self.ctx.core.signer.signed_message( self.ctx.core.pub_key, - promise.clone(), + compact_hashes, + None, ) }) .collect::>()?; diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index 9777d93f582..4d717b656f6 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -46,7 +46,7 @@ use gear_core::{ memory::PageBuf, }; use gprimitives::H256; -use gsigner::secp256k1::Signature; +use gsigner::{Address, secp256k1::Signature}; use parity_scale_codec::{Decode, Encode}; use std::collections::BTreeSet; @@ -631,11 +631,11 @@ impl InjectedStorageRO for Database { }) } - fn promise_signature(&self, tx_hash: HashOf) -> Option { + fn promise_signature(&self, hash: HashOf) -> Option<(Signature, Address)> { self.kv - .get(&Key::PromiseSignature(tx_hash).to_bytes()) + .get(&Key::PromiseSignature(hash).to_bytes()) .map(|data| { - Signature::decode(&mut data.as_slice()) + <(Signature, Address)>::decode(&mut data.as_slice()) .expect("Failed to decode data into `(Signature, Address)`") }) } @@ -657,11 +657,18 @@ impl InjectedStorageRW for Database { .put(&Key::Promise(promise.tx_hash).to_bytes(), promise.encode()) } - fn set_promise_signature(&self, hash: HashOf, signature: Signature) { + fn set_promise_signature( + &self, + hash: HashOf, + signature: Signature, + address: Address, + ) { tracing::trace!(tx_hash = ?hash, ?signature, "Set signature for injected transaction promise"); - self.kv - .put(&Key::PromiseSignature(hash).to_bytes(), signature.encode()); + self.kv.put( + &Key::PromiseSignature(hash).to_bytes(), + (signature, address).encode(), + ); } } diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index a4a805e7633..ee278b80011 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -28,7 +28,6 @@ use ethexe_common::{ injected::{CompactSignedPromise, InjectedTransaction}, network::VerifiedValidatorMessage, }; -use gsigner::{Signature, SignerError}; use lru::LruCache; use std::{cmp::Ordering, collections::VecDeque, mem, num::NonZeroUsize, sync::Arc}; @@ -91,10 +90,6 @@ enum VerifyPromiseError { address: Address, tx_hash: HashOf, }, - #[display("failed to recover validator's public key: signature={signature}")] - ValidatorPublicKeyRecover { signature: Signature }, - #[display("failed to verify promises signatures bundle: error={signer_error}")] - SignatureVerification { signer_error: SignerError }, } /// Tracks validator-signed messages and admits each one once the on-chain @@ -273,25 +268,15 @@ impl ValidatorTopic { _source: PeerId, compact_promise: CompactSignedPromise, ) -> Result { - let CompactSignedPromise { - tx_hash, - signature, - eip191_hash, - } = compact_promise.clone(); - - let public_key = signature - .recover_from_eip191_hash(eip191_hash) - .map_err(|_| VerifyPromiseError::ValidatorPublicKeyRecover { signature })?; - - let address = public_key.to_address(); + let address = compact_promise.address(); if !self.snapshot.contains(address) { - return Err(VerifyPromiseError::UnknownValidator { address, tx_hash }); + return Err(VerifyPromiseError::UnknownValidator { + address, + tx_hash: compact_promise.data().tx_hash, + }); } - match signature.verify_with_eip191_hash(public_key, eip191_hash) { - Ok(()) => Ok(compact_promise), - Err(signer_error) => Err(VerifyPromiseError::SignatureVerification { signer_error }), - } + Ok(compact_promise) } // FIXME: messages from previous era validators are ignored @@ -321,7 +306,7 @@ mod tests { use assert_matches::assert_matches; use ethexe_common::{ self, Announce, - injected::{Promise, SignedPromise}, + injected::{CompactPromiseHashes, Promise, SignedPromise}, mock::Mock, network::{SignedValidatorMessage, ValidatorMessage}, }; @@ -384,7 +369,10 @@ mod tests { public_key: PublicKey, promise: Promise, ) -> CompactSignedPromise { - CompactSignedPromise::create(signer, public_key, promise).unwrap() + let promise_hashes = CompactPromiseHashes::from(&promise); + signer + .signed_message(public_key, promise_hashes, None) + .unwrap() } #[test] @@ -662,11 +650,9 @@ mod tests { .inner_verify_promise(peer_id, compact_promise.clone()) .unwrap_err(); - assert!(matches!(err, VerifyPromiseError::UnknownValidator { .. })); - if let VerifyPromiseError::UnknownValidator { address, tx_hash } = err { - assert_eq!(address, promise.address()); - assert_eq!(tx_hash, promise.data().tx_hash); - } + let VerifyPromiseError::UnknownValidator { address, tx_hash } = err; + assert_eq!(address, promise.address()); + assert_eq!(tx_hash, promise.data().tx_hash); let (acceptance, promise) = topic.verify_promise(peer_id, compact_promise); assert_matches!(acceptance, MessageAcceptance::Ignore); diff --git a/ethexe/rpc/src/apis/injected.rs b/ethexe/rpc/src/apis/injected.rs index 309ef87086e..100180fb95e 100644 --- a/ethexe/rpc/src/apis/injected.rs +++ b/ethexe/rpc/src/apis/injected.rs @@ -28,7 +28,6 @@ use ethexe_common::{ }; use ethexe_db::Database; -use gsigner::hash::Eip191Hash; use jsonrpsee::{ PendingSubscriptionSink, SubscriptionMessage, SubscriptionSink, core::{RpcResult, SubscriptionResult, async_trait}, @@ -135,19 +134,22 @@ impl InjectedServer for InjectedApi { return Ok(None); }; - let Some(signature) = self.db.promise_signature(tx_hash) else { + let Some((signature, address)) = self.db.promise_signature(tx_hash) else { + tracing::trace!( + ?tx_hash, + "promise signature not found for injected transaction" + ); return Ok(None); }; - let promise_eip191_hash = Eip191Hash::new(&promise); - let Ok(public_key) = signature.recover_from_eip191_hash(promise_eip191_hash) else { - todo!() - }; - - match SignedMessage::try_from_parts(promise, signature, public_key.to_address()) { + match SignedMessage::try_from_parts(promise, signature, address) { Ok(message) => Ok(Some(message)), - Err(_err) => { - tracing::trace!(""); + Err(err) => { + tracing::trace!( + ?tx_hash, + ?err, + "failed to build signed promise from parts for injected transaction" + ); Ok(None) } } @@ -165,15 +167,15 @@ impl InjectedApi { } pub fn receive_compact_promise(&self, compact_promise: CompactSignedPromise) { - match self.db.promise(compact_promise.tx_hash) { + match self.db.promise(compact_promise.data().tx_hash) { Some(promise) => { tracing::trace!(tx_hash = ?promise.tx_hash, "Promise already computed, send to user"); self.send_promise(promise, compact_promise); } None => { - tracing::trace!(tx_hash = ?compact_promise.tx_hash, "Promise doesn't compute yet, waiting for producer's signature"); + tracing::trace!(tx_hash = ?compact_promise.data().tx_hash, "Promise doesn't compute yet, waiting for producer's signature"); self.promises_computation_waiting - .insert(compact_promise.tx_hash, compact_promise); + .insert(compact_promise.data().tx_hash, compact_promise); } } } @@ -184,8 +186,10 @@ impl InjectedApi { if let Some((_, compact_promise)) = self.promises_computation_waiting.remove(&promise.tx_hash) { + let (signature, address) = + (*compact_promise.signature(), compact_promise.address()); self.db - .set_promise_signature(promise.tx_hash, compact_promise.signature); + .set_promise_signature(promise.tx_hash, signature, address); self.send_promise(promise, compact_promise); } }) @@ -193,7 +197,8 @@ impl InjectedApi { pub fn send_promise(&self, promise: Promise, compact_promise: CompactSignedPromise) { // Check the promise waiter firstly to avoid unnecessary computation. - let Some((_, promise_sender)) = self.promise_waiters.remove(&compact_promise.tx_hash) + let Some((_, promise_sender)) = + self.promise_waiters.remove(&compact_promise.data().tx_hash) else { tracing::warn!( ?promise, @@ -202,16 +207,7 @@ impl InjectedApi { return; }; - let CompactSignedPromise { - signature, - eip191_hash, - .. - } = compact_promise; - - let Ok(public_key) = signature.recover_from_eip191_hash(eip191_hash) else { - todo!() - }; - let address = public_key.to_address(); + let (address, signature) = (compact_promise.address(), *compact_promise.signature()); let Ok(message) = SignedMessage::try_from_parts(promise.clone(), signature, address) else { tracing::trace!( @@ -219,7 +215,7 @@ impl InjectedApi { ?compact_promise, "failed to build `SignedMessage` from parts, invalid signature" ); - todo!("handle invalid signature case"); + return; }; if let Err(promise) = promise_sender.send(message) { diff --git a/ethexe/rpc/src/tests.rs b/ethexe/rpc/src/tests.rs index 0d6bc7d6dc0..3c1a983895e 100644 --- a/ethexe/rpc/src/tests.rs +++ b/ethexe/rpc/src/tests.rs @@ -24,7 +24,7 @@ use ethexe_common::{ db::InjectedStorageRW, ecdsa::PrivateKey, gear::MAX_BLOCK_GAS_LIMIT, - injected::{AddressedInjectedTransaction, CompactSignedPromise, Promise}, + injected::{AddressedInjectedTransaction, CompactPromiseHashes, CompactSignedPromise, Promise}, mock::Mock, }; use ethexe_db::Database; @@ -92,7 +92,8 @@ impl MockService { .map(|tx| { let promise = Promise::mock(tx.tx.data().to_hash()); self.db.set_promise(promise.clone()); - CompactSignedPromise::create_from_private_key(&pk, promise).unwrap() + let promise_hashes = CompactPromiseHashes::from(&promise); + CompactSignedPromise::create(pk.clone(), promise_hashes).unwrap() }) .collect() } diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index c00fc0d7cdc..b0bd620e424 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -2493,7 +2493,7 @@ async fn injected_tx_fungible_token() { if let TestingEvent::Consensus(ConsensusEvent::Promises(promises)) = event && !promises.is_empty() { - let promise_tx_hash = promises.first().unwrap().tx_hash; + let promise_tx_hash = promises.first().unwrap().data().tx_hash; let promise = node .db .promise(promise_tx_hash) diff --git a/gsigner/src/schemes/secp256k1/signature.rs b/gsigner/src/schemes/secp256k1/signature.rs index 88a559e0ebe..46f174932c9 100644 --- a/gsigner/src/schemes/secp256k1/signature.rs +++ b/gsigner/src/schemes/secp256k1/signature.rs @@ -108,12 +108,12 @@ impl Signature { .ok_or_else(|| SignerError::Crypto("Failed to recover public key".into())) } - pub fn recover_from_eip191_hash(&self, hash: Eip191Hash) -> SignResult { - self.0 - .recover_prehashed(hash.inner()) - .map(PublicKey::from) - .ok_or_else(|| SignerError::Crypto("Failed to recover public key".into())) - } + // pub fn recover_from_eip191_hash(&self, hash: Eip191Hash) -> SignResult { + // self.0 + // .recover_prehashed(hash.inner()) + // .map(PublicKey::from) + // .ok_or_else(|| SignerError::Crypto("Failed to recover public key".into())) + // } /// Recovers public key which was used to create the signature for the signed message /// according to EIP-191 standard. diff --git a/gsigner/src/schemes/secp256k1/signer_ext.rs b/gsigner/src/schemes/secp256k1/signer_ext.rs index 5f3314c13fc..89e6503198e 100644 --- a/gsigner/src/schemes/secp256k1/signer_ext.rs +++ b/gsigner/src/schemes/secp256k1/signer_ext.rs @@ -25,7 +25,6 @@ use super::{ use crate::{ Signer, error::{Result, SignerError}, - hash::Eip191Hash, }; /// Extension trait for Secp256k1 signers. @@ -49,12 +48,12 @@ pub trait Secp256k1SignerExt { password: Option<&str>, ) -> Result; - fn sign_eip191_hash( - &self, - public_key: PublicKey, - eip191_hash: Eip191Hash, - password: Option<&str>, - ) -> Result; + // fn sign_eip191_hash( + // &self, + // public_key: PublicKey, + // eip191_hash: Eip191Hash, + // password: Option<&str>, + // ) -> Result; /// Create signed data (signature + data). fn signed_data( @@ -125,16 +124,16 @@ impl Secp256k1SignerExt for Signer { .map_err(|e| SignerError::Crypto(format!("Signature creation failed: {e}"))) } - fn sign_eip191_hash( - &self, - public_key: PublicKey, - eip191_hash: Eip191Hash, - password: Option<&str>, - ) -> Result { - let private_key = self.get_private_key(public_key, password)?; - Signature::create_from_eip191_hash(&private_key, eip191_hash) - .map_err(|e| SignerError::Crypto(format!("Signature creation failed: {e}"))) - } + // fn sign_eip191_hash( + // &self, + // public_key: PublicKey, + // eip191_hash: Eip191Hash, + // password: Option<&str>, + // ) -> Result { + // let private_key = self.get_private_key(public_key, password)?; + // Signature::create_from_eip191_hash(&private_key, eip191_hash) + // .map_err(|e| SignerError::Crypto(format!("Signature creation failed: {e}"))) + // } fn signed_data( &self, From ac2519e8d2da60e51c07f4be71ed1365deed34b2 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 11 Feb 2026 14:26:35 +0700 Subject: [PATCH 06/59] remove Eip191Hash struct --- gsigner/src/hash.rs | 44 --------------------- gsigner/src/schemes/secp256k1/signature.rs | 34 +--------------- gsigner/src/schemes/secp256k1/signer_ext.rs | 18 --------- 3 files changed, 1 insertion(+), 95 deletions(-) diff --git a/gsigner/src/hash.rs b/gsigner/src/hash.rs index d98ec9c0a6b..6b3eebf8e42 100644 --- a/gsigner/src/hash.rs +++ b/gsigner/src/hash.rs @@ -18,9 +18,6 @@ //! Lightweight hashing helpers shared across schemes. -use crate::ToDigest; -use core::marker::PhantomData; -use parity_scale_codec::{Decode, Encode}; use sha3::{Digest as _, Keccak256}; /// Compute the Keccak-256 hash of a byte slice. @@ -44,44 +41,3 @@ where } hasher.finalize().into() } - -/// Representing the EIP-191 hash standard. -#[derive(Debug, PartialEq, Eq, Encode, Decode)] -pub struct Eip191Hash { - hash: [u8; 32], - _phantom: PhantomData, -} - -impl Copy for Eip191Hash {} - -impl Clone for Eip191Hash { - fn clone(&self) -> Self { - *self - } -} - -impl Eip191Hash { - pub fn inner(&self) -> &[u8; 32] { - &self.hash - } -} - -impl Eip191Hash -where - T: ToDigest, -{ - /// Constructs the [`Eip191Hash`] from [`Digest`]. - pub fn new(value: &T) -> Eip191Hash { - let digest = value.to_digest(); - let mut hasher = Keccak256::new(); - - hasher.update(b"\x19Ethereum Signed Message:\n"); - hasher.update(b"32"); - hasher.update(digest.0.as_ref()); - - Eip191Hash { - hash: hasher.finalize().into(), - _phantom: PhantomData, - } - } -} diff --git a/gsigner/src/schemes/secp256k1/signature.rs b/gsigner/src/schemes/secp256k1/signature.rs index 46f174932c9..68ccac363ed 100644 --- a/gsigner/src/schemes/secp256k1/signature.rs +++ b/gsigner/src/schemes/secp256k1/signature.rs @@ -19,10 +19,7 @@ //! Secp256k1 signature types and utilities backed by `sp_core` primitives. use super::{Address, Digest, PrivateKey, PublicKey}; -use crate::{ - error::SignerError, - hash::{Eip191Hash, keccak256_iter}, -}; +use crate::{error::SignerError, hash::keccak256_iter}; #[cfg(feature = "serde")] use alloc::{format, string::String}; use core::hash::{Hash, Hasher}; @@ -68,16 +65,6 @@ impl Signature { Ok(Self::new(private_key.as_pair().sign_prehashed(&digest.0))) } - /// Create a recoverable signature from a precomputed digest. - pub fn create_from_eip191_hash( - private_key: &PrivateKey, - eip191_hash: Eip191Hash, - ) -> SignResult { - Ok(Self::new( - private_key.as_pair().sign_prehashed(eip191_hash.inner()), - )) - } - /// Create a recoverable signature for the provided digest using the private key according to EIP-191. pub fn create_message(private_key: &PrivateKey, data: T) -> SignResult where @@ -108,13 +95,6 @@ impl Signature { .ok_or_else(|| SignerError::Crypto("Failed to recover public key".into())) } - // pub fn recover_from_eip191_hash(&self, hash: Eip191Hash) -> SignResult { - // self.0 - // .recover_prehashed(hash.inner()) - // .map(PublicKey::from) - // .ok_or_else(|| SignerError::Crypto("Failed to recover public key".into())) - // } - /// Recovers public key which was used to create the signature for the signed message /// according to EIP-191 standard. pub fn recover_message(&self, data: T) -> SignResult @@ -147,18 +127,6 @@ impl Signature { } } - pub fn verify_with_eip191_hash( - &self, - public_key: PublicKey, - eip191_hash: Eip191Hash, - ) -> SignResult<()> { - if SpPair::verify_prehashed(&self.0, eip191_hash.inner(), &SpPublic::from(public_key)) { - Ok(()) - } else { - Err(SignerError::Crypto("Verification failed".into())) - } - } - /// Verifies message using [`Self::verify`] method according to EIP-191 standard. pub fn verify_message(&self, public_key: PublicKey, data: T) -> SignResult<()> where diff --git a/gsigner/src/schemes/secp256k1/signer_ext.rs b/gsigner/src/schemes/secp256k1/signer_ext.rs index 89e6503198e..110abf0fe73 100644 --- a/gsigner/src/schemes/secp256k1/signer_ext.rs +++ b/gsigner/src/schemes/secp256k1/signer_ext.rs @@ -48,13 +48,6 @@ pub trait Secp256k1SignerExt { password: Option<&str>, ) -> Result; - // fn sign_eip191_hash( - // &self, - // public_key: PublicKey, - // eip191_hash: Eip191Hash, - // password: Option<&str>, - // ) -> Result; - /// Create signed data (signature + data). fn signed_data( &self, @@ -124,17 +117,6 @@ impl Secp256k1SignerExt for Signer { .map_err(|e| SignerError::Crypto(format!("Signature creation failed: {e}"))) } - // fn sign_eip191_hash( - // &self, - // public_key: PublicKey, - // eip191_hash: Eip191Hash, - // password: Option<&str>, - // ) -> Result { - // let private_key = self.get_private_key(public_key, password)?; - // Signature::create_from_eip191_hash(&private_key, eip191_hash) - // .map_err(|e| SignerError::Crypto(format!("Signature creation failed: {e}"))) - // } - fn signed_data( &self, public_key: PublicKey, From dfaa7d1a5435d4ff1e69e2cbc256005f09c9f1b5 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 11 Feb 2026 14:44:16 +0700 Subject: [PATCH 07/59] fix unused deps | small refactoring in consensus --- Cargo.lock | 2 -- ethexe/common/Cargo.toml | 1 - ethexe/consensus/src/utils.rs | 18 ++++++++++++++++++ ethexe/consensus/src/validator/producer.rs | 18 +++++------------- ethexe/rpc/Cargo.toml | 1 - 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ad91729fa19..0db052f0b98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5010,7 +5010,6 @@ dependencies = [ "sha3", "sp-core", "tap", - "thiserror 2.0.17", ] [[package]] @@ -5227,7 +5226,6 @@ dependencies = [ "gear-core", "gear-workspace-hack", "gprimitives", - "gsigner", "hyper 1.8.1", "jsonrpsee", "ntest", diff --git a/ethexe/common/Cargo.toml b/ethexe/common/Cargo.toml index 51dc7266730..93ff90d5dff 100644 --- a/ethexe/common/Cargo.toml +++ b/ethexe/common/Cargo.toml @@ -27,7 +27,6 @@ gsigner = { workspace = true, default-features = false, features = [ sha3.workspace = true k256 = { version = "0.13.4", features = ["ecdsa"], default-features = false } nonempty.workspace = true -thiserror.workspace = true # mock deps itertools = { workspace = true, optional = true } diff --git a/ethexe/consensus/src/utils.rs b/ethexe/consensus/src/utils.rs index 874ba2aa9a3..b0d9236ebc5 100644 --- a/ethexe/consensus/src/utils.rs +++ b/ethexe/consensus/src/utils.rs @@ -31,6 +31,7 @@ use ethexe_common::{ AggregatedPublicKey, BatchCommitment, ChainCommitment, CodeCommitment, RewardsCommitment, StateTransition, ValidatorsCommitment, }, + injected::{CompactPromiseHashes, CompactSignedPromise, Promise}, }; use gprimitives::{CodeId, H256, U256}; use gsigner::secp256k1::{Secp256k1SignerExt, Signer}; @@ -431,6 +432,23 @@ pub fn sort_transitions_by_value_to_receive(transitions: &mut [StateTransition]) }); } +/// A helper function that signs a bundle of promises to distribute signed promises in network. +pub fn sign_announce_promises( + signer: &Signer, + public_key: PublicKey, + promises: Vec, +) -> Result> { + promises + .into_iter() + .map(|promise| { + let promise_hashes = CompactPromiseHashes::from(&promise); + signer + .signed_message(public_key, promise_hashes, None) + .map_err(Into::into) + }) + .collect::>() +} + #[cfg(test)] mod tests { use super::*; diff --git a/ethexe/consensus/src/validator/producer.rs b/ethexe/consensus/src/validator/producer.rs index c479b3ce17f..4d711ce4cb0 100644 --- a/ethexe/consensus/src/validator/producer.rs +++ b/ethexe/consensus/src/validator/producer.rs @@ -22,13 +22,14 @@ use super::{ use crate::{ ConsensusEvent, announces::{self, DBAnnouncesExt}, + utils::sign_announce_promises, validator::DefaultProcessing, }; use anyhow::{Result, anyhow}; use derive_more::{Debug, Display}; use ethexe_common::{ Announce, ComputedAnnounce, HashOf, SimpleBlockData, ValidatorsVec, db::BlockMetaStorageRO, - gear::BatchCommitment, injected::CompactPromiseHashes, network::ValidatorMessage, + gear::BatchCommitment, network::ValidatorMessage, }; use ethexe_service_utils::Timer; use futures::{FutureExt, future::BoxFuture}; @@ -82,18 +83,9 @@ impl StateHandler for Producer { if *expected == computed_data.announce_hash => { if !computed_data.promises.is_empty() { - let signed_promises = computed_data - .promises - .into_iter() - .map(|promise| { - let compact_hashes = CompactPromiseHashes::from(&promise); - self.ctx.core.signer.signed_message( - self.ctx.core.pub_key, - compact_hashes, - None, - ) - }) - .collect::>()?; + let (signer, public_key) = (&self.ctx.core.signer, self.ctx.core.pub_key); + let signed_promises = + sign_announce_promises(signer, public_key, computed_data.promises)?; self.ctx.output(ConsensusEvent::Promises(signed_promises)); } diff --git a/ethexe/rpc/Cargo.toml b/ethexe/rpc/Cargo.toml index 47695019c55..77617926545 100644 --- a/ethexe/rpc/Cargo.toml +++ b/ethexe/rpc/Cargo.toml @@ -29,7 +29,6 @@ serde = { workspace = true, features = ["std"] } tracing.workspace = true dashmap.workspace = true gear-workspace-hack.workspace = true -gsigner.workspace = true [dev-dependencies] jsonrpsee = { workspace = true, features = ["client"] } From 8c7829cd881df06fb73be37f4b50579498410968 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 11 Feb 2026 15:08:02 +0700 Subject: [PATCH 08/59] self review fixes --- ethexe/compute/src/compute.rs | 1 - ethexe/db/src/database.rs | 4 ++-- ethexe/network/src/validator/topic.rs | 2 +- ethexe/rpc/src/apis/injected.rs | 7 ++++++- ethexe/rpc/src/lib.rs | 12 ------------ 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 7f84fcf9d66..7a3d6245be8 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -201,7 +201,6 @@ impl ComputeSubService

{ }) .ok_or(ComputeError::LatestDataNotFound)?; - // TODO: remove in this PR ComputedAnnounce struct. promises.clone().into_iter().for_each(|promise| { db.set_promise(promise); }); diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index 4d717b656f6..15f2ecf266c 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -651,7 +651,7 @@ impl InjectedStorageRW for Database { } fn set_promise(&self, promise: Promise) { - tracing::trace!(promise_tx_hash = ?promise.tx_hash, "Set promise for injected transaction"); + tracing::trace!(?promise, "Set promise for injected transaction"); self.kv .put(&Key::Promise(promise.tx_hash).to_bytes(), promise.encode()) @@ -663,7 +663,7 @@ impl InjectedStorageRW for Database { signature: Signature, address: Address, ) { - tracing::trace!(tx_hash = ?hash, ?signature, "Set signature for injected transaction promise"); + tracing::trace!(tx_hash = ?hash, ?signature, ?address, "Set signature for injected transaction promise"); self.kv.put( &Key::PromiseSignature(hash).to_bytes(), diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index ee278b80011..73cdafec87f 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -288,7 +288,7 @@ impl ValidatorTopic { match self.inner_verify_promise(source, compact_promise) { Ok(compact_promise) => (MessageAcceptance::Accept, Some(compact_promise)), Err(err) => { - log::trace!("failed to verify promises bundle: {err}"); + log::trace!("failed to verify compact promise: {err}"); (MessageAcceptance::Ignore, None) } } diff --git a/ethexe/rpc/src/apis/injected.rs b/ethexe/rpc/src/apis/injected.rs index 100180fb95e..b1556fcafb4 100644 --- a/ethexe/rpc/src/apis/injected.rs +++ b/ethexe/rpc/src/apis/injected.rs @@ -169,7 +169,12 @@ impl InjectedApi { pub fn receive_compact_promise(&self, compact_promise: CompactSignedPromise) { match self.db.promise(compact_promise.data().tx_hash) { Some(promise) => { - tracing::trace!(tx_hash = ?promise.tx_hash, "Promise already computed, send to user"); + tracing::trace!(tx_hash = ?promise.tx_hash, "Promise already computed, sending to user..."); + self.db.set_promise_signature( + promise.tx_hash, + *compact_promise.signature(), + compact_promise.address(), + ); self.send_promise(promise, compact_promise); } None => { diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index e67daf8d2e8..2341afc0431 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -163,18 +163,6 @@ impl RpcService { .into_iter() .for_each(|compact_promise| self.provide_compact_promise(compact_promise)); } - - // Provides a promise inside RPC service to be sent to subscribers. - // pub fn provide_promise(&self, promise: CompactSignedPromise) { - // self.injected_api.send_promise(promise); - // } - - // Provides a bundle of promises inside RPC service to be sent to subscribers. - // pub fn provide_promises(&self, promises: Vec) { - // promises.into_iter().for_each(|promise| { - // self.provide_promise(promise); - // }); - // } } impl Stream for RpcService { From 3afe82bfa5fba39ba4e2fb68a6080a93524c1ab3 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Thu, 12 Feb 2026 14:00:10 +0700 Subject: [PATCH 09/59] update submodules --- ethexe/contracts/lib/forge-std | 2 +- ethexe/contracts/lib/frost-secp256k1-evm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ethexe/contracts/lib/forge-std b/ethexe/contracts/lib/forge-std index ff47d4052a6..1801b0541f4 160000 --- a/ethexe/contracts/lib/forge-std +++ b/ethexe/contracts/lib/forge-std @@ -1 +1 @@ -Subproject commit ff47d4052a6018d9e5419e5cf013b16ff8006aae +Subproject commit 1801b0541f4fda118a10798fd3486bb7051c5dd6 diff --git a/ethexe/contracts/lib/frost-secp256k1-evm b/ethexe/contracts/lib/frost-secp256k1-evm index c7badcc70df..52b7473fea7 160000 --- a/ethexe/contracts/lib/frost-secp256k1-evm +++ b/ethexe/contracts/lib/frost-secp256k1-evm @@ -1 +1 @@ -Subproject commit c7badcc70df1eda47a051f6fc46ea3f226ac86b1 +Subproject commit 52b7473fea7c03d34dbb48d3ce9967e12c3ed574 From 9ace2a64d21ed74924eea5b2597a1d905515ca8a Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 23 Feb 2026 21:18:30 +0700 Subject: [PATCH 10/59] initial pull request | main functionality --- ethexe/common/src/gear.rs | 15 +++++- ethexe/processor/src/handling/overlaid.rs | 8 ++- ethexe/processor/src/handling/run.rs | 15 ++++++ ethexe/processor/src/host/api/mod.rs | 2 +- ethexe/processor/src/host/api/promise.rs | 56 ++++++++++++++++++++ ethexe/processor/src/host/mod.rs | 3 +- ethexe/processor/src/host/threads.rs | 5 +- ethexe/runtime/common/src/lib.rs | 29 +++++++++- ethexe/runtime/src/wasm/interface/mod.rs | 3 ++ ethexe/runtime/src/wasm/interface/promise.rs | 45 ++++++++++++++++ ethexe/runtime/src/wasm/storage.rs | 8 ++- 11 files changed, 178 insertions(+), 11 deletions(-) create mode 100644 ethexe/processor/src/host/api/promise.rs create mode 100644 ethexe/runtime/src/wasm/interface/promise.rs diff --git a/ethexe/common/src/gear.rs b/ethexe/common/src/gear.rs index 3745d7a6ac5..e4511cbf4c1 100644 --- a/ethexe/common/src/gear.rs +++ b/ethexe/common/src/gear.rs @@ -429,7 +429,20 @@ impl ToDigest for ValueClaim { } } -#[derive(Clone, Copy, Debug, Encode, Decode, PartialEq, Eq, Default, PartialOrd, Ord, Hash)] +#[derive( + Clone, + Copy, + Debug, + Encode, + Decode, + PartialEq, + Eq, + Default, + PartialOrd, + Ord, + Hash, + derive_more::IsVariant, +)] #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] pub enum MessageType { #[default] diff --git a/ethexe/processor/src/handling/overlaid.rs b/ethexe/processor/src/handling/overlaid.rs index e7fe15adedf..ab720c14a5d 100644 --- a/ethexe/processor/src/handling/overlaid.rs +++ b/ethexe/processor/src/handling/overlaid.rs @@ -25,7 +25,7 @@ use crate::{ host::InstanceCreator, }; use core_processor::common::JournalNote; -use ethexe_common::{BlockHeader, db::CodesStorageRO, gear::MessageType}; +use ethexe_common::{BlockHeader, db::CodesStorageRO, gear::MessageType, injected::Promise}; use ethexe_db::{CASDatabase, Database}; use ethexe_runtime_common::{InBlockTransitions, TransitionController}; use gear_core::{ @@ -34,7 +34,7 @@ use gear_core::{ message::ReplyDetails, }; use gprimitives::{ActorId, MessageId}; -use std::collections::HashSet; +use std::{collections::HashSet, sync::mpsc}; /// Overlay execution context. /// @@ -174,6 +174,10 @@ impl RunContext for OverlaidRunContext { &self.inner.instance_creator } + fn promise_sender(&self) -> &mpsc::Sender { + self.inner.promise_sender() + } + fn block_header(&self) -> BlockHeader { self.inner.block_header } diff --git a/ethexe/processor/src/handling/run.rs b/ethexe/processor/src/handling/run.rs index 5fdbcffc59d..6db89a93eba 100644 --- a/ethexe/processor/src/handling/run.rs +++ b/ethexe/processor/src/handling/run.rs @@ -106,6 +106,8 @@ // TODO: #5120 split to several files and move to separate module +use std::sync::mpsc; + use crate::{ ProcessorError, Result, handling::run::chunks_splitting::ExecutionChunks, host::InstanceCreator, }; @@ -116,6 +118,7 @@ use ethexe_common::{ BlockHeader, StateHashWithQueueSize, db::CodesStorageRO, gear::{CHUNK_PROCESSING_GAS_LIMIT, MessageType}, + injected::Promise, }; use ethexe_db::{CASDatabase, Database}; use ethexe_runtime_common::{ @@ -188,6 +191,9 @@ pub(super) trait RunContext { /// Get reference to instance creator. fn instance_creator(&self) -> &InstanceCreator; + /// Get the promises sender to main service. + fn promise_sender(&self) -> &mpsc::Sender; + /// Returns the header of the current block. fn block_header(&self) -> BlockHeader; @@ -263,6 +269,7 @@ pub(crate) struct CommonRunContext { pub(crate) gas_allowance_counter: GasAllowanceCounter, pub(crate) chunk_size: usize, pub(crate) block_header: BlockHeader, + pub(crate) promise_sender: mpsc::Sender, } impl CommonRunContext { @@ -273,6 +280,7 @@ impl CommonRunContext { gas_allowance: u64, chunk_size: usize, block_header: BlockHeader, + promise_sender: mpsc::Sender, ) -> Self { CommonRunContext { db, @@ -281,6 +289,7 @@ impl CommonRunContext { gas_allowance_counter: GasAllowanceCounter::new(gas_allowance), chunk_size, block_header, + promise_sender, } } @@ -302,6 +311,10 @@ impl RunContext for CommonRunContext { &self.instance_creator } + fn promise_sender(&self) -> &mpsc::Sender { + &self.promise_sender + } + fn block_header(&self) -> BlockHeader { self.block_header } @@ -548,6 +561,7 @@ mod chunk_execution_spawn { .collect::>>()?; let block_header = ctx.block_header(); + let promise_sender = ctx.promise_sender().clone(); let block_info = BlockInfo { height: block_header.height, timestamp: block_header.timestamp, @@ -579,6 +593,7 @@ mod chunk_execution_spawn { gas_allowance_for_chunk, ), block_info, + promise_sender: promise_sender.clone(), }, ) .expect("Some error occurs while running program in instance"); diff --git a/ethexe/processor/src/host/api/mod.rs b/ethexe/processor/src/host/api/mod.rs index 7361d65d35b..03d0fc95300 100644 --- a/ethexe/processor/src/host/api/mod.rs +++ b/ethexe/processor/src/host/api/mod.rs @@ -26,8 +26,8 @@ pub mod allocator; pub mod database; pub mod lazy_pages; pub mod logging; +pub mod promise; pub mod sandbox; - pub struct MemoryWrap(Memory); // TODO: return results for mem accesses. diff --git a/ethexe/processor/src/host/api/promise.rs b/ethexe/processor/src/host/api/promise.rs new file mode 100644 index 00000000000..0875776c6dd --- /dev/null +++ b/ethexe/processor/src/host/api/promise.rs @@ -0,0 +1,56 @@ +// This file is part of Gear. +// +// Copyright (C) 2024-2025 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use core::mem::size_of; +use ethexe_common::{HashOf, injected::Promise}; +use gear_core::rpc::ReplyInfo; +use gprimitives::MessageId; +use parity_scale_codec::{Decode, Error as CodecError}; +use sp_wasm_interface::StoreData; +use wasmtime::{Caller, Linker}; + +use crate::host::{api::MemoryWrap, threads}; + +pub fn link(linker: &mut Linker) -> Result<(), wasmtime::Error> { + linker.func_wrap("env", "ext_promise_send_to_service", send_promise); + + Ok(()) +} + +fn send_promise( + caller: Caller<'_, StoreData>, + reply_ptr: i32, + encoded_reply_len: i32, + message_id_ptr: i32, +) -> Result<(), CodecError> { + let memory = MemoryWrap(caller.data().memory()); + + let reply_slice = memory.slice_mut(&caller, reply_ptr as usize, encoded_reply_len as usize); + let reply = ReplyInfo::decode(reply_slice)?; + + let message_id_slice = + memory.slice_mut(&caller, message_id_ptr as usize, size_of::<[u8; 32]>()); + let message_id = MessageId::decode(message_id_slice)?; + + threads::with_params(|params| { + let tx_hash = unsafe { HashOf::new(message_id.into_bytes().into()) }; + let promise = Promise { tx_hash, reply }; + params.promise_sender.send(promise); + }); + Ok(()) +} diff --git a/ethexe/processor/src/host/mod.rs b/ethexe/processor/src/host/mod.rs index b22fb1e2bed..b286081cc75 100644 --- a/ethexe/processor/src/host/mod.rs +++ b/ethexe/processor/src/host/mod.rs @@ -110,6 +110,7 @@ impl InstanceCreator { api::lazy_pages::link(&mut linker)?; api::logging::link(&mut linker)?; api::sandbox::link(&mut linker)?; + api::promise::link(&mut linker)?; let instance_pre = linker.instantiate_pre(&module)?; let instance_pre = Arc::new(instance_pre); @@ -170,7 +171,7 @@ impl InstanceWrapper { db: Box, ctx: ProcessQueueContext, ) -> Result<(ProgramJournals, H256, u64)> { - threads::set(db, ctx.state_root); + threads::set(db, ctx.state_root, ctx.promise_sender.clone()); // Pieces of resulting journal. Hack to avoid single allocation limit. let (ptr_lens, gas_spent): (Vec, i64) = self.call("run", ctx.encode())?; diff --git a/ethexe/processor/src/host/threads.rs b/ethexe/processor/src/host/threads.rs index b41bd615207..e96d265db0d 100644 --- a/ethexe/processor/src/host/threads.rs +++ b/ethexe/processor/src/host/threads.rs @@ -29,7 +29,7 @@ use gear_core::{ids::ActorId, memory::PageBuf, pages::GearPage}; use gear_lazy_pages::LazyPagesStorage; use gprimitives::H256; use parity_scale_codec::{Decode, DecodeAll}; -use std::{cell::RefCell, collections::BTreeMap}; +use std::{cell::RefCell, collections::BTreeMap, sync::mpsc}; const UNSET_PANIC: &str = "params should be set before query"; const UNKNOWN_STATE: &str = "state should always be valid (must exist)"; @@ -41,6 +41,7 @@ thread_local! { pub struct ThreadParams { pub db: Box, pub state_hash: H256, + pub promise_sender: mpsc::Sender, pages_registry_cache: Option, pages_regions_cache: Option>, } @@ -102,7 +103,7 @@ impl PageKey { } } -pub fn set(db: Box, state_hash: H256) { +pub fn set(db: Box, state_hash: H256, promise_sender: mpsc::Sender) { PARAMS.set(Some(ThreadParams { db, state_hash, diff --git a/ethexe/runtime/common/src/lib.rs b/ethexe/runtime/common/src/lib.rs index 1be408d45bb..3cbe14dc778 100644 --- a/ethexe/runtime/common/src/lib.rs +++ b/ethexe/runtime/common/src/lib.rs @@ -22,22 +22,28 @@ extern crate alloc; +use std::sync::mpsc; + use alloc::vec::Vec; use core_processor::{ ContextCharged, Ext, ProcessExecutionContext, common::{ExecutableActorData, JournalNote}, configs::{BlockConfig, SyscallName}, }; -use ethexe_common::gear::{CHUNK_PROCESSING_GAS_LIMIT, MessageType}; +use ethexe_common::{ + gear::{CHUNK_PROCESSING_GAS_LIMIT, MessageType}, + injected::Promise, +}; use gear_core::{ code::{CodeMetadata, InstrumentedCode, MAX_WASM_PAGES_AMOUNT}, gas::GasAllowanceCounter, gas_metering::Schedule, ids::ActorId, message::{DispatchKind, IncomingDispatch, IncomingMessage}, + rpc::ReplyInfo, }; use gear_lazy_pages_common::LazyPagesInterface; -use gprimitives::H256; +use gprimitives::{H256, MessageId}; use gsys::{GasMultiplier, Percent}; use journal::RuntimeJournalHandler; use state::{Dispatch, ProgramState, Storage}; @@ -73,6 +79,7 @@ pub struct ProcessQueueContext { pub code_metadata: CodeMetadata, pub gas_allowance: GasAllowanceCounter, pub block_info: BlockInfo, + pub promise_sender: mpsc::Sender, } pub trait RuntimeInterface: Storage { @@ -81,6 +88,8 @@ pub trait RuntimeInterface: Storage { fn init_lazy_pages(&self); fn random_data(&self) -> (Vec, u32); fn update_state_hash(&self, state_hash: &H256); + // TODO: create more meaningful function name + fn send_promise(&self, reply: &ReplyInfo, message_id: &MessageId); } /// A main low-level interface to perform state changes @@ -261,6 +270,7 @@ where value, details, context, + message_type, .. } = dispatch; @@ -320,6 +330,21 @@ where return core_processor::process_uninitialized(context); } + if active_state.initialized && kind.is_reply() && message_type.is_injected() { + let code = details + .and_then(|d| d.to_reply_details()) + .map(|d| d.to_reply_code()) + .expect("reply details must exists for reply dispatch"); + + let reply = ReplyInfo { + value, + code, + payload: payload.to_vec(), + }; + + ri.send_promise(&reply, &dispatch_id); + } + let context = match context.charge_for_code_metadata(block_config) { Ok(context) => context, Err(journal) => return journal, diff --git a/ethexe/runtime/src/wasm/interface/mod.rs b/ethexe/runtime/src/wasm/interface/mod.rs index 8090a75b029..77d716341c6 100644 --- a/ethexe/runtime/src/wasm/interface/mod.rs +++ b/ethexe/runtime/src/wasm/interface/mod.rs @@ -25,6 +25,9 @@ pub(crate) mod database_ri; #[path = "logging.rs"] pub(crate) mod logging_ri; +#[path = "promise.rs"] +pub(crate) mod promise_ri; + pub(crate) mod utils { use ethexe_runtime_common::pack_u32_to_i64; diff --git a/ethexe/runtime/src/wasm/interface/promise.rs b/ethexe/runtime/src/wasm/interface/promise.rs new file mode 100644 index 00000000000..2b284c22ac7 --- /dev/null +++ b/ethexe/runtime/src/wasm/interface/promise.rs @@ -0,0 +1,45 @@ +// This file is part of Gear. +// +// Copyright (C) 2024-2025 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::wasm::interface; +use gear_core::rpc::ReplyInfo; +use gprimitives::MessageId; +use parity_scale_codec::Encode; + +interface::declare!( + pub(super) fn send_promise( + reply_ptr: *const ReplyInfo, + encoded_reply_len: i32, + message_id_ptr: *const MessageId, + ); +); + +pub fn send_promise(reply: &ReplyInfo, message_id: &MessageId) { + unsafe { + // TODO: implement `as_ptr` for `ReplyInfo` + let reply_ptr = 0; + let reply_encoded_size = reply.encoded_size(); + let message_id_ptr = message_id.as_ref().as_ptr(); + + sys::send_promise( + reply_ptr as _, + reply_encoded_size as i32, + message_id_ptr as _, + ); + } +} diff --git a/ethexe/runtime/src/wasm/storage.rs b/ethexe/runtime/src/wasm/storage.rs index db800d57bd3..7c617029853 100644 --- a/ethexe/runtime/src/wasm/storage.rs +++ b/ethexe/runtime/src/wasm/storage.rs @@ -26,9 +26,9 @@ use ethexe_runtime_common::{ ProgramState, Storage, UserMailbox, Waitlist, }, }; -use gear_core::{buffer::Payload, memory::PageBuf}; +use gear_core::{buffer::Payload, memory::PageBuf, rpc::ReplyInfo}; use gear_lazy_pages_interface::{LazyPagesInterface, LazyPagesRuntimeInterface}; -use gprimitives::H256; +use gprimitives::{H256, MessageId}; #[derive(Debug, Clone)] pub struct NativeRuntimeInterface; @@ -150,4 +150,8 @@ impl RuntimeInterface for NativeRuntimeInterface { fn update_state_hash(&self, hash: &H256) { database_ri::update_state_hash(hash); } + + fn send_promise(&self, reply: &ReplyInfo, message_id: &MessageId) { + todo!() + } } From 5a974ec505ac968d19a53d7d962f6e72866f19d3 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 24 Feb 2026 19:04:46 +0300 Subject: [PATCH 11/59] complete functionality in processor --- Cargo.lock | 1 + ethexe/cli/src/commands/check.rs | 2 +- ethexe/compute/src/codes.rs | 94 ++++---- ethexe/compute/src/compute.rs | 116 +++++----- ethexe/compute/src/service.rs | 214 +++++++++---------- ethexe/compute/src/tests.rs | 9 +- ethexe/processor/Cargo.toml | 1 + ethexe/processor/src/handling/overlaid.rs | 11 +- ethexe/processor/src/handling/run.rs | 38 ++-- ethexe/processor/src/host/api/promise.rs | 44 ++-- ethexe/processor/src/host/mod.rs | 7 +- ethexe/processor/src/host/threads.rs | 12 +- ethexe/processor/src/lib.rs | 19 +- ethexe/processor/src/tests.rs | 94 ++++++-- ethexe/rpc/src/apis/program.rs | 7 +- ethexe/rpc/src/lib.rs | 4 +- ethexe/runtime/common/src/lib.rs | 61 +++--- ethexe/runtime/common/src/transitions.rs | 2 +- ethexe/runtime/src/wasm/interface/promise.rs | 26 ++- ethexe/runtime/src/wasm/storage.rs | 4 +- ethexe/service/src/lib.rs | 8 +- ethexe/service/src/tests/utils/env.rs | 8 +- 22 files changed, 443 insertions(+), 339 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84955b177c6..799fa9f4eca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5194,6 +5194,7 @@ dependencies = [ "ethexe-runtime", "ethexe-runtime-common", "gear-core", + "gear-core-errors", "gear-core-processor", "gear-lazy-pages", "gear-runtime-interface", diff --git a/ethexe/cli/src/commands/check.rs b/ethexe/cli/src/commands/check.rs index af5310719df..14d53419b1b 100644 --- a/ethexe/cli/src/commands/check.rs +++ b/ethexe/cli/src/commands/check.rs @@ -227,7 +227,7 @@ impl Checker { None }; - let processor = Processor::with_config(ProcessorConfig { chunk_size }, db.clone()) + let processor = Processor::with_config(ProcessorConfig { chunk_size }, db.clone(), None) .context("failed to create processor")?; // Iterate back: from `head` announce to `bottom` announce diff --git a/ethexe/compute/src/codes.rs b/ethexe/compute/src/codes.rs index 02320254739..4ef134ef768 100644 --- a/ethexe/compute/src/codes.rs +++ b/ethexe/compute/src/codes.rs @@ -107,50 +107,50 @@ impl SubService for CodesSubService

{ } } -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::*; - use ethexe_common::CodeAndId; - use gear_core::code::{InstantiatedSectionSizes, InstrumentedCode}; - - #[tokio::test] - #[ntest::timeout(3000)] - async fn process_code() { - let db = Database::memory(); - let mut service = CodesSubService::new(db.clone(), MockProcessor); - - let code_and_id = CodeAndId::new(vec![1, 2, 3, 4]); - - service.receive_code_to_process(code_and_id.clone().into_unchecked()); - assert_eq!(service.next().await.unwrap(), code_and_id.code_id()); - } - - #[tokio::test] - #[ntest::timeout(3000)] - async fn process_already_validated_code() { - let db = Database::memory(); - let mut service = CodesSubService::new(db.clone(), MockProcessor); - - let code_and_id = CodeAndId::new(vec![1, 2, 3, 4]); - let code_id = code_and_id.code_id(); - db.set_code_valid(code_id, true); - db.set_original_code(code_and_id.code()); - db.set_instrumented_code( - ethexe_runtime_common::VERSION, - code_id, - InstrumentedCode::new( - vec![5, 6, 7, 8], - InstantiatedSectionSizes::new(1, 1, 1, 1, 1, 1), - ), - ); - service.receive_code_to_process(code_and_id.into_unchecked()); - assert_eq!(service.next().await.unwrap(), code_id); - - let code_and_id = CodeAndId::new(vec![100, 101, 102, 103]); - let code_id = code_and_id.code_id(); - db.set_code_valid(code_id, false); - service.receive_code_to_process(code_and_id.into_unchecked()); - assert_eq!(service.next().await.unwrap(), code_id); - } -} +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::tests::*; +// use ethexe_common::CodeAndId; +// use gear_core::code::{InstantiatedSectionSizes, InstrumentedCode}; + +// #[tokio::test] +// #[ntest::timeout(3000)] +// async fn process_code() { +// let db = Database::memory(); +// let mut service = CodesSubService::new(db.clone(), MockProcessor); + +// let code_and_id = CodeAndId::new(vec![1, 2, 3, 4]); + +// service.receive_code_to_process(code_and_id.clone().into_unchecked()); +// assert_eq!(service.next().await.unwrap(), code_and_id.code_id()); +// } + +// #[tokio::test] +// #[ntest::timeout(3000)] +// async fn process_already_validated_code() { +// let db = Database::memory(); +// let mut service = CodesSubService::new(db.clone(), MockProcessor); + +// let code_and_id = CodeAndId::new(vec![1, 2, 3, 4]); +// let code_id = code_and_id.code_id(); +// db.set_code_valid(code_id, true); +// db.set_original_code(code_and_id.code()); +// db.set_instrumented_code( +// ethexe_runtime_common::VERSION, +// code_id, +// InstrumentedCode::new( +// vec![5, 6, 7, 8], +// InstantiatedSectionSizes::new(1, 1, 1, 1, 1, 1), +// ), +// ); +// service.receive_code_to_process(code_and_id.into_unchecked()); +// assert_eq!(service.next().await.unwrap(), code_id); + +// let code_and_id = CodeAndId::new(vec![100, 101, 102, 103]); +// let code_id = code_and_id.code_id(); +// db.set_code_valid(code_id, false); +// service.receive_code_to_process(code_and_id.into_unchecked()); +// assert_eq!(service.next().await.unwrap(), code_id); +// } +// } diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 9a6fa073a75..96d93f77912 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -272,61 +272,61 @@ fn find_canonical_events_post_quarantine( .ok_or(ComputeError::BlockEventsNotFound(block_hash)) } -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::{MockProcessor, PROCESSOR_RESULT}; - use ethexe_common::{gear::StateTransition, mock::*}; - use gprimitives::{ActorId, H256}; - - #[tokio::test] - #[ntest::timeout(3000)] - async fn test_compute() { - gear_utils::init_default_logger(); - - let db = Database::memory(); - let block_hash = BlockChain::mock(1).setup(&db).blocks[1].hash; - let config = ComputeConfig::without_quarantine(); - let mut service = ComputeSubService::new(config, db.clone(), MockProcessor); - - let announce = Announce { - block_hash, - parent: db.latest_data().unwrap().genesis_announce_hash, - gas_allowance: Some(100), - injected_transactions: vec![], - }; - let announce_hash = announce.to_hash(); - - // Create non-empty processor result with transitions - let non_empty_result = FinalizedBlockTransitions { - transitions: vec![StateTransition { - actor_id: ActorId::from([1; 32]), - new_state_hash: H256::from([2; 32]), - value_to_receive: 100, - ..Default::default() - }], - ..Default::default() - }; - - // Set the PROCESSOR_RESULT to return non-empty result - PROCESSOR_RESULT.with_borrow_mut(|r| *r = non_empty_result.clone()); - service.receive_announce_to_compute(announce); - - assert_eq!(service.next().await.unwrap().announce_hash, announce_hash); - - // Verify block was marked as computed - assert!(db.announce_meta(announce_hash).computed); - - // Verify transitions were stored in DB - let stored_transitions = db.announce_outcome(announce_hash).unwrap(); - assert_eq!(stored_transitions.len(), 1); - assert_eq!(stored_transitions[0].actor_id, ActorId::from([1; 32])); - assert_eq!(stored_transitions[0].new_state_hash, H256::from([2; 32])); - - // Verify latest announce - assert_eq!( - db.latest_data().unwrap().computed_announce_hash, - announce_hash - ); - } -} +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::tests::{MockProcessor, PROCESSOR_RESULT}; +// use ethexe_common::{gear::StateTransition, mock::*}; +// use gprimitives::{ActorId, H256}; + +// #[tokio::test] +// #[ntest::timeout(3000)] +// async fn test_compute() { +// gear_utils::init_default_logger(); + +// let db = Database::memory(); +// let block_hash = BlockChain::mock(1).setup(&db).blocks[1].hash; +// let config = ComputeConfig::without_quarantine(); +// let mut service = ComputeSubService::new(config, db.clone(), MockProcessor); + +// let announce = Announce { +// block_hash, +// parent: db.latest_data().unwrap().genesis_announce_hash, +// gas_allowance: Some(100), +// injected_transactions: vec![], +// }; +// let announce_hash = announce.to_hash(); + +// // Create non-empty processor result with transitions +// let non_empty_result = FinalizedBlockTransitions { +// transitions: vec![StateTransition { +// actor_id: ActorId::from([1; 32]), +// new_state_hash: H256::from([2; 32]), +// value_to_receive: 100, +// ..Default::default() +// }], +// ..Default::default() +// }; + +// // Set the PROCESSOR_RESULT to return non-empty result +// PROCESSOR_RESULT.with_borrow_mut(|r| *r = non_empty_result.clone()); +// service.receive_announce_to_compute(announce); + +// assert_eq!(service.next().await.unwrap().announce_hash, announce_hash); + +// // Verify block was marked as computed +// assert!(db.announce_meta(announce_hash).computed); + +// // Verify transitions were stored in DB +// let stored_transitions = db.announce_outcome(announce_hash).unwrap(); +// assert_eq!(stored_transitions.len(), 1); +// assert_eq!(stored_transitions[0].actor_id, ActorId::from([1; 32])); +// assert_eq!(stored_transitions[0].new_state_hash, H256::from([2; 32])); + +// // Verify latest announce +// assert_eq!( +// db.latest_data().unwrap().computed_announce_hash, +// announce_hash +// ); +// } +// } diff --git a/ethexe/compute/src/service.rs b/ethexe/compute/src/service.rs index 5be9707c60b..251325d0563 100644 --- a/ethexe/compute/src/service.rs +++ b/ethexe/compute/src/service.rs @@ -158,110 +158,110 @@ pub(crate) trait SubService: Unpin + Send + 'static { } } -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::MockProcessor; - use ethexe_common::{CodeAndIdUnchecked, ComputedAnnounce, db::*, mock::*}; - use ethexe_db::Database as DB; - use futures::StreamExt; - use gear_core::ids::prelude::CodeIdExt; - use gprimitives::CodeId; - - /// Test ComputeService block preparation functionality - #[tokio::test] - async fn prepare_block() { - gear_utils::init_default_logger(); - - let db = DB::memory(); - let processor = MockProcessor; - let config = ComputeConfig::without_quarantine(); - let mut service = ComputeService::new(config, db.clone(), processor); - - let chain = BlockChain::mock(1).setup(&db); - let block = chain.blocks[1].to_simple().next_block().setup(&db); - - // Request block preparation - service.prepare_block(block.hash); - - // Poll service to process the preparation request - let event = service.next().await.unwrap().unwrap(); - assert_eq!(event, ComputeEvent::BlockPrepared(block.hash)); - - // Verify block is marked as prepared in DB - assert!(db.block_meta(block.hash).prepared); - } - - /// Test ComputeService block processing functionality - #[tokio::test] - async fn compute_announce() { - gear_utils::init_default_logger(); - - let db = DB::memory(); - let processor = MockProcessor; - - let config = ComputeConfig::without_quarantine(); - let mut service = ComputeService::new(config, db.clone(), processor); - let chain = BlockChain::mock(1).setup(&db); - - let block = chain.blocks[1].to_simple().next_block().setup(&db); - - service.prepare_block(block.hash); - let event = service.next().await.unwrap().unwrap(); - assert_eq!(event, ComputeEvent::BlockPrepared(block.hash)); - - // Request computation - let announce = Announce { - block_hash: block.hash, - parent: chain.block_top_announce_hash(1), - gas_allowance: Some(42), - injected_transactions: vec![], - }; - let announce_hash = announce.to_hash(); - service.compute_announce(announce); - - // Poll service to process the block - let event = service.next().await.unwrap().unwrap(); - assert_eq!( - event, - ComputeEvent::AnnounceComputed(ComputedAnnounce::mock(announce_hash)) - ); - - // Verify block is marked as computed in DB - assert!(db.announce_meta(announce_hash).computed); - } - - /// Test ComputeService code processing functionality - #[tokio::test] - async fn process_code() { - gear_utils::init_default_logger(); - - let db = DB::memory(); - let processor = MockProcessor; - let config = ComputeConfig::without_quarantine(); - let mut service = ComputeService::new(config, db.clone(), processor); - - // Create test code - let code = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]; // Simple WASM header - let code_id = CodeId::generate(&code); - - let code_and_id = CodeAndIdUnchecked { code, code_id }; - - // Verify code is not yet in DB - assert!(db.code_valid(code_id).is_none()); - - // Request code processing - service.process_code(code_and_id); - - // Poll service to process the code - let event = service.next().await.unwrap().unwrap(); - - // Should receive CodeProcessed event with correct code_id - match event { - ComputeEvent::CodeProcessed(processed_code_id) => { - assert_eq!(processed_code_id, code_id); - } - _ => panic!("Expected CodeProcessed event"), - } - } -} +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::tests::MockProcessor; +// use ethexe_common::{CodeAndIdUnchecked, ComputedAnnounce, db::*, mock::*}; +// use ethexe_db::Database as DB; +// use futures::StreamExt; +// use gear_core::ids::prelude::CodeIdExt; +// use gprimitives::CodeId; + +// /// Test ComputeService block preparation functionality +// #[tokio::test] +// async fn prepare_block() { +// gear_utils::init_default_logger(); + +// let db = DB::memory(); +// let processor = MockProcessor; +// let config = ComputeConfig::without_quarantine(); +// let mut service = ComputeService::new(config, db.clone(), processor); + +// let chain = BlockChain::mock(1).setup(&db); +// let block = chain.blocks[1].to_simple().next_block().setup(&db); + +// // Request block preparation +// service.prepare_block(block.hash); + +// // Poll service to process the preparation request +// let event = service.next().await.unwrap().unwrap(); +// assert_eq!(event, ComputeEvent::BlockPrepared(block.hash)); + +// // Verify block is marked as prepared in DB +// assert!(db.block_meta(block.hash).prepared); +// } + +// /// Test ComputeService block processing functionality +// #[tokio::test] +// async fn compute_announce() { +// gear_utils::init_default_logger(); + +// let db = DB::memory(); +// let processor = MockProcessor; + +// let config = ComputeConfig::without_quarantine(); +// let mut service = ComputeService::new(config, db.clone(), processor); +// let chain = BlockChain::mock(1).setup(&db); + +// let block = chain.blocks[1].to_simple().next_block().setup(&db); + +// service.prepare_block(block.hash); +// let event = service.next().await.unwrap().unwrap(); +// assert_eq!(event, ComputeEvent::BlockPrepared(block.hash)); + +// // Request computation +// let announce = Announce { +// block_hash: block.hash, +// parent: chain.block_top_announce_hash(1), +// gas_allowance: Some(42), +// injected_transactions: vec![], +// }; +// let announce_hash = announce.to_hash(); +// service.compute_announce(announce); + +// // Poll service to process the block +// let event = service.next().await.unwrap().unwrap(); +// assert_eq!( +// event, +// ComputeEvent::AnnounceComputed(ComputedAnnounce::mock(announce_hash)) +// ); + +// // Verify block is marked as computed in DB +// assert!(db.announce_meta(announce_hash).computed); +// } + +// /// Test ComputeService code processing functionality +// #[tokio::test] +// async fn process_code() { +// gear_utils::init_default_logger(); + +// let db = DB::memory(); +// let processor = MockProcessor; +// let config = ComputeConfig::without_quarantine(); +// let mut service = ComputeService::new(config, db.clone(), processor); + +// // Create test code +// let code = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]; // Simple WASM header +// let code_id = CodeId::generate(&code); + +// let code_and_id = CodeAndIdUnchecked { code, code_id }; + +// // Verify code is not yet in DB +// assert!(db.code_valid(code_id).is_none()); + +// // Request code processing +// service.process_code(code_and_id); + +// // Poll service to process the code +// let event = service.next().await.unwrap().unwrap(); + +// // Should receive CodeProcessed event with correct code_id +// match event { +// ComputeEvent::CodeProcessed(processed_code_id) => { +// assert_eq!(processed_code_id, code_id); +// } +// _ => panic!("Expected CodeProcessed event"), +// } +// } +// } diff --git a/ethexe/compute/src/tests.rs b/ethexe/compute/src/tests.rs index 892e512f500..7973bdfa64b 100644 --- a/ethexe/compute/src/tests.rs +++ b/ethexe/compute/src/tests.rs @@ -24,6 +24,7 @@ use ethexe_common::{ BlockEvent, RouterEvent, router::{CodeGotValidatedEvent, CodeValidationRequestedEvent}, }, + injected::Promise, mock::*, }; use ethexe_db::Database; @@ -33,7 +34,7 @@ use gear_core::{ code::{CodeMetadata, InstantiatedSectionSizes, InstrumentedCode}, ids::prelude::CodeIdExt, }; -use std::{cell::RefCell, collections::BTreeMap}; +use std::{cell::RefCell, collections::BTreeMap, sync::mpsc}; thread_local! { pub(crate) static PROCESSOR_RESULT: RefCell = const { RefCell::new( @@ -170,7 +171,11 @@ impl TestEnv { chain = chain.setup(&db); let config = ComputeConfig::without_quarantine(); - let compute = ComputeService::new(config, db.clone(), Processor::new(db.clone()).unwrap()); + let compute = ComputeService::new( + config, + db.clone(), + Processor::new(db.clone(), promise_sender, None).unwrap(), + ); TestEnv { db, compute, chain } } diff --git a/ethexe/processor/Cargo.toml b/ethexe/processor/Cargo.toml index 46f9af7eaf7..3e4a3fe516e 100644 --- a/ethexe/processor/Cargo.toml +++ b/ethexe/processor/Cargo.toml @@ -41,3 +41,4 @@ demo-async = { workspace = true, features = ["debug", "ethexe"] } demo-panic-payload = { workspace = true, features = ["debug", "ethexe"] } wat.workspace = true ethexe-common = { workspace = true, features = ["mock"] } +gear-core-errors.workspace = true diff --git a/ethexe/processor/src/handling/overlaid.rs b/ethexe/processor/src/handling/overlaid.rs index ab720c14a5d..a1d2f0e31ed 100644 --- a/ethexe/processor/src/handling/overlaid.rs +++ b/ethexe/processor/src/handling/overlaid.rs @@ -91,8 +91,11 @@ impl OverlaidRunContext { } } - pub(crate) async fn run(mut self) -> Result { - let _ = run::run_for_queue_type(&mut self, MessageType::Canonical).await?; + pub(crate) async fn run( + mut self, + promise_sender: Option>, + ) -> Result { + let _ = run::run_for_queue_type(&mut self, MessageType::Canonical, promise_sender).await?; Ok(self.inner.transitions) } @@ -174,10 +177,6 @@ impl RunContext for OverlaidRunContext { &self.inner.instance_creator } - fn promise_sender(&self) -> &mpsc::Sender { - self.inner.promise_sender() - } - fn block_header(&self) -> BlockHeader { self.inner.block_header } diff --git a/ethexe/processor/src/handling/run.rs b/ethexe/processor/src/handling/run.rs index 6db89a93eba..8c2741ee45d 100644 --- a/ethexe/processor/src/handling/run.rs +++ b/ethexe/processor/src/handling/run.rs @@ -136,6 +136,7 @@ use itertools::Itertools; pub(super) async fn run_for_queue_type( ctx: &mut impl RunContext, queue_type: MessageType, + promise_sender: Option>, ) -> Result { let mut is_out_of_gas_for_block = false; @@ -150,8 +151,13 @@ pub(super) async fn run_for_queue_type( for chunk in chunks { // Spawn on a separate thread an execution of each program (it's queue) in the chunk. - let chunk_outputs = - chunk_execution_spawn::spawn_chunk_execution(ctx, chunk, queue_type).await?; + let chunk_outputs = chunk_execution_spawn::spawn_chunk_execution( + ctx, + chunk, + queue_type, + promise_sender.clone(), + ) + .await?; // Collect journals from all executed programs in the chunk. let (chunk_journals, max_gas_spent_in_chunk) = @@ -191,9 +197,6 @@ pub(super) trait RunContext { /// Get reference to instance creator. fn instance_creator(&self) -> &InstanceCreator; - /// Get the promises sender to main service. - fn promise_sender(&self) -> &mpsc::Sender; - /// Returns the header of the current block. fn block_header(&self) -> BlockHeader; @@ -269,7 +272,7 @@ pub(crate) struct CommonRunContext { pub(crate) gas_allowance_counter: GasAllowanceCounter, pub(crate) chunk_size: usize, pub(crate) block_header: BlockHeader, - pub(crate) promise_sender: mpsc::Sender, + // pub(crate) promise_sender: mpsc::Sender, } impl CommonRunContext { @@ -280,7 +283,7 @@ impl CommonRunContext { gas_allowance: u64, chunk_size: usize, block_header: BlockHeader, - promise_sender: mpsc::Sender, + // promise_sender: mpsc::Sender, ) -> Self { CommonRunContext { db, @@ -289,17 +292,22 @@ impl CommonRunContext { gas_allowance_counter: GasAllowanceCounter::new(gas_allowance), chunk_size, block_header, - promise_sender, + // promise_sender, } } - pub(crate) async fn run(mut self) -> Result { + pub(crate) async fn run( + mut self, + promise_sender: Option>, + ) -> Result { // Start with injected queues processing. - let can_continue = run_for_queue_type(&mut self, MessageType::Injected).await?; + let can_continue = + run_for_queue_type(&mut self, MessageType::Injected, promise_sender.clone()).await?; if can_continue { // If gas is still left in block, process canonical (Ethereum) queues - let _ = run_for_queue_type(&mut self, MessageType::Canonical).await?; + let _ = run_for_queue_type(&mut self, MessageType::Canonical, promise_sender.clone()) + .await?; } Ok(self.transitions) @@ -311,10 +319,6 @@ impl RunContext for CommonRunContext { &self.instance_creator } - fn promise_sender(&self) -> &mpsc::Sender { - &self.promise_sender - } - fn block_header(&self) -> BlockHeader { self.block_header } @@ -523,6 +527,7 @@ mod chunk_execution_spawn { ctx: &mut impl RunContext, chunk: Vec<(ActorId, H256)>, queue_type: MessageType, + promise_sender: Option>, ) -> Result> { struct Executable { program_id: ActorId, @@ -561,7 +566,6 @@ mod chunk_execution_spawn { .collect::>>()?; let block_header = ctx.block_header(); - let promise_sender = ctx.promise_sender().clone(); let block_info = BlockInfo { height: block_header.height, timestamp: block_header.timestamp, @@ -593,8 +597,8 @@ mod chunk_execution_spawn { gas_allowance_for_chunk, ), block_info, - promise_sender: promise_sender.clone(), }, + promise_sender.clone(), ) .expect("Some error occurs while running program in instance"); diff --git a/ethexe/processor/src/host/api/promise.rs b/ethexe/processor/src/host/api/promise.rs index 0875776c6dd..edd4e3de26b 100644 --- a/ethexe/processor/src/host/api/promise.rs +++ b/ethexe/processor/src/host/api/promise.rs @@ -16,41 +16,47 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use core::mem::size_of; use ethexe_common::{HashOf, injected::Promise}; -use gear_core::rpc::ReplyInfo; use gprimitives::MessageId; -use parity_scale_codec::{Decode, Error as CodecError}; use sp_wasm_interface::StoreData; use wasmtime::{Caller, Linker}; use crate::host::{api::MemoryWrap, threads}; pub fn link(linker: &mut Linker) -> Result<(), wasmtime::Error> { - linker.func_wrap("env", "ext_promise_send_to_service", send_promise); + linker.func_wrap("env", "ext_forward_promise_to_service", forward_promise)?; Ok(()) } -fn send_promise( +// TODO: it is a raw implementation, should be fixed +fn forward_promise( caller: Caller<'_, StoreData>, - reply_ptr: i32, - encoded_reply_len: i32, - message_id_ptr: i32, -) -> Result<(), CodecError> { + encoded_reply_ptr_len: i64, + message_id_ptr_len: i64, +) { let memory = MemoryWrap(caller.data().memory()); - let reply_slice = memory.slice_mut(&caller, reply_ptr as usize, encoded_reply_len as usize); - let reply = ReplyInfo::decode(reply_slice)?; - - let message_id_slice = - memory.slice_mut(&caller, message_id_ptr as usize, size_of::<[u8; 32]>()); - let message_id = MessageId::decode(message_id_slice)?; + let reply = memory.decode_by_val(&caller, encoded_reply_ptr_len); + let message_id: MessageId = memory.decode_by_val(&caller, message_id_ptr_len); threads::with_params(|params| { - let tx_hash = unsafe { HashOf::new(message_id.into_bytes().into()) }; - let promise = Promise { tx_hash, reply }; - params.promise_sender.send(promise); + if let Some(ref sender) = params.promise_sender { + log::error!("calling `forward_promise` reply={reply:?}"); + + let tx_hash = unsafe { HashOf::new(message_id.into_bytes().into()) }; + let promise = Promise { tx_hash, reply }; + + match sender.send(promise) { + Ok(()) => { + // log::trace!( + // "successfully send promise to outer service: reply_ptr_len={reply_ptr_len}, message_id_ptr_len={message_id_ptr_len}" + // ); + } + Err(err) => { + log::trace!("failed to send promise to outer service: error={err}"); + } + } + } }); - Ok(()) } diff --git a/ethexe/processor/src/host/mod.rs b/ethexe/processor/src/host/mod.rs index b286081cc75..62972c5058e 100644 --- a/ethexe/processor/src/host/mod.rs +++ b/ethexe/processor/src/host/mod.rs @@ -17,7 +17,7 @@ // along with this program. If not, see . use core_processor::common::JournalNote; -use ethexe_common::gear::MessageType; +use ethexe_common::{gear::MessageType, injected::Promise}; use ethexe_db::CASDatabase; use ethexe_runtime_common::{ProcessQueueContext, ProgramJournals, unpack_i64_to_u32}; use gear_core::code::{CodeMetadata, InstrumentedCode}; @@ -25,7 +25,7 @@ use gprimitives::H256; use parity_scale_codec::{Decode, Encode}; use sp_allocator::{AllocationStats, FreeingBumpHeapAllocator}; use sp_wasm_interface::{HostState, IntoValue, MemoryWrapper, StoreData}; -use std::sync::Arc; +use std::sync::{Arc, mpsc}; pub mod api; pub mod runtime; @@ -170,8 +170,9 @@ impl InstanceWrapper { &mut self, db: Box, ctx: ProcessQueueContext, + promise_sender: Option>, ) -> Result<(ProgramJournals, H256, u64)> { - threads::set(db, ctx.state_root, ctx.promise_sender.clone()); + threads::set(db, ctx.state_root, promise_sender.clone()); // Pieces of resulting journal. Hack to avoid single allocation limit. let (ptr_lens, gas_spent): (Vec, i64) = self.call("run", ctx.encode())?; diff --git a/ethexe/processor/src/host/threads.rs b/ethexe/processor/src/host/threads.rs index e96d265db0d..ab334aa3ca1 100644 --- a/ethexe/processor/src/host/threads.rs +++ b/ethexe/processor/src/host/threads.rs @@ -19,7 +19,7 @@ // TODO: for each panic here place log::error, otherwise it won't be printed. use core::fmt; -use ethexe_common::HashOf; +use ethexe_common::{HashOf, injected::Promise}; use ethexe_db::CASDatabase; use ethexe_runtime_common::state::{ ActiveProgram, MemoryPages, MemoryPagesRegionInner, Program, ProgramState, QueryableStorage, @@ -41,7 +41,8 @@ thread_local! { pub struct ThreadParams { pub db: Box, pub state_hash: H256, - pub promise_sender: mpsc::Sender, + /// TODO: think about using [`mpsc::sync_channel`] instead of [`mpsc::channel`]. + pub promise_sender: Option>, pages_registry_cache: Option, pages_regions_cache: Option>, } @@ -103,12 +104,17 @@ impl PageKey { } } -pub fn set(db: Box, state_hash: H256, promise_sender: mpsc::Sender) { +pub fn set( + db: Box, + state_hash: H256, + promise_sender: Option>, +) { PARAMS.set(Some(ThreadParams { db, state_hash, pages_registry_cache: None, pages_regions_cache: None, + promise_sender, })) } diff --git a/ethexe/processor/src/lib.rs b/ethexe/processor/src/lib.rs index b6841ce842e..46567692e1f 100644 --- a/ethexe/processor/src/lib.rs +++ b/ethexe/processor/src/lib.rs @@ -23,7 +23,7 @@ use ethexe_common::{ CodeAndIdUnchecked, ProgramStates, Schedule, SimpleBlockData, ecdsa::VerifiedData, events::{BlockRequestEvent, MirrorRequestEvent, mirror::MessageQueueingRequestedEvent}, - injected::InjectedTransaction, + injected::{InjectedTransaction, Promise}, }; use ethexe_db::Database; use ethexe_runtime_common::{ @@ -38,6 +38,7 @@ use gear_core::{ use gprimitives::{ActorId, CodeId, H256, MessageId}; use handling::{ProcessingHandler, overlaid::OverlaidRunContext, run::CommonRunContext}; use host::InstanceCreator; +use std::sync::mpsc; pub use host::InstanceError; @@ -107,22 +108,28 @@ pub struct Processor { config: ProcessorConfig, db: Database, creator: InstanceCreator, + promise_sender: Option>, } /// TODO: consider avoiding re-instantiations on processing events. /// Maybe impl `struct EventProcessor`. impl Processor { /// Creates processor with default config. - pub fn new(db: Database) -> Result { - Self::with_config(Default::default(), db) + pub fn new(db: Database, promise_sender: Option>) -> Result { + Self::with_config(Default::default(), db, promise_sender) } - pub fn with_config(config: ProcessorConfig, db: Database) -> Result { + pub fn with_config( + config: ProcessorConfig, + db: Database, + promise_sender: Option>, + ) -> Result { let creator = InstanceCreator::new(host::runtime())?; Ok(Self { config, db, creator, + promise_sender, }) } @@ -252,7 +259,7 @@ impl Processor { self.config.chunk_size, block.header, ) - .run() + .run(self.promise_sender.clone()) .await } @@ -402,7 +409,7 @@ impl OverlaidProcessor { self.0.creator.clone(), block.header, ) - .run() + .run(self.0.promise_sender.clone()) .await?; let res = transitions diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index eb519921103..ce1bfd62136 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -29,7 +29,11 @@ use ethexe_common::{ mock::*, }; use ethexe_runtime_common::{RUNTIME_ID, state::MessageQueue}; -use gear_core::ids::prelude::CodeIdExt; +use gear_core::{ + ids::prelude::CodeIdExt, + message::{ErrorReplyReason, ReplyCode, SuccessReplyReason}, +}; +use gear_core_errors::SimpleExecutionError; use gprimitives::{ActorId, MessageId}; use parity_scale_codec::Encode; use utils::*; @@ -94,9 +98,10 @@ mod utils { pub fn setup_test_env_and_load_codes( codes: [&[u8]; N], + promise_sender: Option>, ) -> (Processor, BlockChain, [CodeId; N]) { let db = Database::memory(); - let mut processor = Processor::new(db.clone()).unwrap(); + let mut processor = Processor::new(db.clone(), promise_sender).unwrap(); let chain = BlockChain::mock(20).setup(&db); let mut code_ids = Vec::new(); @@ -137,7 +142,8 @@ mod utils { async fn ping_init() { init_logger(); - let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY]); + let (mut processor, chain, [code_id]) = + setup_test_env_and_load_codes([demo_ping::WASM_BINARY], None); // Empty processing for block1 let executable = ExecutableData { @@ -231,7 +237,8 @@ async fn ping_init() { fn handle_new_code_valid() { init_logger(); - let mut processor = Processor::new(Database::memory()).expect("failed to create processor"); + let mut processor = + Processor::new(Database::memory(), None).expect("failed to create processor"); let (code_id, code) = utils::wat_to_wasm(utils::VALID_PROGRAM); @@ -257,7 +264,8 @@ fn handle_new_code_valid() { fn handle_new_code_invalid() { init_logger(); - let mut processor = Processor::new(Database::memory()).expect("failed to create processor"); + let mut processor = + Processor::new(Database::memory(), None).expect("failed to create processor"); let (code_id, code) = utils::wat_to_wasm(utils::INVALID_PROGRAM); @@ -275,7 +283,7 @@ async fn ping_pong() { init_logger(); let (mut processor, chain, [code_id, ..]) = - setup_test_env_and_load_codes([demo_ping::WASM_BINARY, demo_async::WASM_BINARY]); + setup_test_env_and_load_codes([demo_ping::WASM_BINARY, demo_async::WASM_BINARY], None); let block1 = chain.blocks[1].to_simple(); let user_id = ActorId::from(10); @@ -355,7 +363,7 @@ async fn async_and_ping() { }; let (mut processor, chain, [ping_code_id, upload_code_id, ..]) = - setup_test_env_and_load_codes([demo_ping::WASM_BINARY, demo_async::WASM_BINARY]); + setup_test_env_and_load_codes([demo_ping::WASM_BINARY, demo_async::WASM_BINARY], None); let block1 = chain.blocks[1].to_simple(); let mut handler = setup_handler(processor.db.clone(), block1); @@ -494,7 +502,7 @@ async fn many_waits() { let (_, code) = wat_to_wasm(wat.as_str()); - let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([code.as_slice()]); + let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([code.as_slice()], None); let block1 = chain.blocks[1].to_simple(); let wake_block = chain.blocks[1 + blocks_to_wait].to_simple(); @@ -620,7 +628,7 @@ async fn overlay_execution() { }; let (mut processor, chain, [ping_code_id, async_code_id]) = - setup_test_env_and_load_codes([demo_ping::WASM_BINARY, demo_async::WASM_BINARY]); + setup_test_env_and_load_codes([demo_ping::WASM_BINARY, demo_async::WASM_BINARY], None); let block1 = chain.blocks[1].to_simple(); // ----------------------------------------------------------------------------- @@ -849,7 +857,9 @@ async fn overlay_execution() { async fn injected_ping_pong() { init_logger(); - let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY]); + let (promise_sender, promise_receiver) = mpsc::channel(); + let (mut processor, chain, [code_id]) = + setup_test_env_and_load_codes([demo_ping::WASM_BINARY], Some(promise_sender)); let block1 = chain.blocks[1].to_simple(); let user_1 = ActorId::from(10); @@ -907,8 +917,10 @@ async fn injected_ping_pong() { ) .expect("failed to send message"); + let injected_tx = injected(actor_id, b"PING", 0); + log::error!("injected tx hash: {:?}", injected_tx.to_hash()); handler - .handle_injected_transaction(user_2, injected(actor_id, b"PING", 0)) + .handle_injected_transaction(user_2, injected_tx.clone()) .expect("failed to send message"); handler.transitions = processor @@ -916,6 +928,18 @@ async fn injected_ping_pong() { .await .unwrap(); + let promise = promise_receiver + .recv() + .expect("promise must be sent after processing"); + + assert_eq!(promise.tx_hash, injected_tx.to_hash()); + assert_eq!(promise.reply.payload, b"PONG"); + assert_eq!(promise.reply.value, 0); + assert_eq!( + promise.reply.code, + ReplyCode::Success(SuccessReplyReason::Manual) + ); + let to_users = handler.transitions.current_messages(); assert_eq!(to_users.len(), 3); @@ -934,12 +958,15 @@ async fn injected_ping_pong() { #[cfg(debug_assertions)] // FIXME: test fails in release mode #[tokio::test(flavor = "multi_thread")] +// TODO: add here testing the promises async fn injected_prioritized_over_canonical() { const MSG_NUM: usize = 100; init_logger(); - let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY]); + let (promise_sender, promise_receiver) = mpsc::channel(); + let (mut processor, chain, [code_id]) = + setup_test_env_and_load_codes([demo_ping::WASM_BINARY], Some(promise_sender)); let block1 = chain.blocks[1].to_simple(); let canonical_user = ActorId::from(10); @@ -1001,9 +1028,12 @@ async fn injected_prioritized_over_canonical() { } // Send injected messages + let mut tx_hahes = vec![]; for _ in 0..MSG_NUM { + let tx = injected(actor_id, b"PING", 0); + tx_hahes.push(tx.to_hash()); handler - .handle_injected_transaction(injected_user, injected(actor_id, b"PING", 0)) + .handle_injected_transaction(injected_user, tx) .expect("failed to send message"); } @@ -1012,6 +1042,20 @@ async fn injected_prioritized_over_canonical() { .await .unwrap(); + tx_hahes.into_iter().for_each(|tx_hash| { + let promise = promise_receiver + .recv() + .expect("promise for injected transaction"); + + assert_eq!(promise.tx_hash, tx_hash); + assert_eq!(promise.reply.value, 0); + assert_eq!( + promise.reply.code, + ReplyCode::Success(SuccessReplyReason::Manual) + ); + assert_eq!(promise.reply.payload, b"PONG"); + }); + // Verify that injected messages were processed first // skip the first message which is INIT reply let mut is_canonical_found = false; @@ -1028,7 +1072,8 @@ async fn injected_prioritized_over_canonical() { async fn executable_balance_charged() { init_logger(); - let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY]); + let (mut processor, chain, [code_id]) = + setup_test_env_and_load_codes([demo_ping::WASM_BINARY], None); let block1 = chain.blocks[1].to_simple(); let mut handler = setup_handler(processor.db.clone(), block1); @@ -1113,8 +1158,9 @@ async fn executable_balance_injected_panic_not_charged() { init_logger(); + let (promise_sender, promise_receiver) = mpsc::channel(); let (mut processor, chain, [code_id]) = - setup_test_env_and_load_codes([demo_panic_payload::WASM_BINARY]); + setup_test_env_and_load_codes([demo_panic_payload::WASM_BINARY], Some(promise_sender)); let block1 = chain.blocks[1].to_simple(); let user_id = ActorId::from(10); @@ -1164,14 +1210,27 @@ async fn executable_balance_injected_panic_not_charged() { // We know for sure handling this message is cost less than the threshold. // This message will cause panic in the program. + let panic_tx = injected(actor_id, b"", 0); handler - .handle_injected_transaction(user_id, injected(actor_id, b"", 0)) + .handle_injected_transaction(user_id, panic_tx.clone()) .unwrap(); handler.transitions = processor .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT) .await .unwrap(); + let panic_promise = promise_receiver + .recv() + .expect("promise for injected transaction"); + assert_eq!(panic_promise.tx_hash, panic_tx.to_hash()); + assert_eq!(panic_promise.reply.value, 0); + assert_eq!( + panic_promise.reply.code, + ReplyCode::Error(ErrorReplyReason::Execution( + SimpleExecutionError::UserspacePanic + )) + ); + let to_users = handler.transitions.current_messages(); assert_eq!(to_users.len(), 2); @@ -1224,7 +1283,8 @@ async fn insufficient_executable_balance_still_charged() { init_logger(); - let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY]); + let (mut processor, chain, [code_id]) = + setup_test_env_and_load_codes([demo_ping::WASM_BINARY], None); let block1 = chain.blocks[1].to_simple(); let mut handler = setup_handler(processor.db.clone(), block1); diff --git a/ethexe/rpc/src/apis/program.rs b/ethexe/rpc/src/apis/program.rs index 568cec5d32d..ab27dc48caf 100644 --- a/ethexe/rpc/src/apis/program.rs +++ b/ethexe/rpc/src/apis/program.rs @@ -22,7 +22,7 @@ use ethexe_common::{ db::{AnnounceStorageRO, CodesStorageRO, OnChainStorageRO}, }; use ethexe_db::Database; -use ethexe_processor::{ExecutableDataForReply, Processor}; +use ethexe_processor::{ExecutableDataForReply, OverlaidProcessor}; use ethexe_runtime_common::state::{ DispatchStash, Mailbox, MemoryPages, MessageQueue, Program, ProgramState, QueryableStorage, Storage, Waitlist, @@ -95,12 +95,12 @@ pub trait Program { pub struct ProgramApi { db: Database, - processor: Processor, + processor: OverlaidProcessor, gas_allowance: u64, } impl ProgramApi { - pub fn new(db: Database, processor: Processor, gas_allowance: u64) -> Self { + pub fn new(db: Database, processor: OverlaidProcessor, gas_allowance: u64) -> Self { Self { db, processor, @@ -167,7 +167,6 @@ impl ProgramServer for ProgramApi { self.processor .clone() - .overlaid() .execute_for_reply(executable) .await .map_err(errors::runtime) diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index 6c69db952b3..ec952d347c3 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -106,7 +106,9 @@ impl RpcServer { chunk_size: self.config.chunk_size, }, self.db.clone(), - )?; + None, + )? + .overlaid(); let server_apis = RpcServerApis { code: CodeApi::new(self.db.clone()), diff --git a/ethexe/runtime/common/src/lib.rs b/ethexe/runtime/common/src/lib.rs index 3cbe14dc778..49058a02cdf 100644 --- a/ethexe/runtime/common/src/lib.rs +++ b/ethexe/runtime/common/src/lib.rs @@ -22,18 +22,13 @@ extern crate alloc; -use std::sync::mpsc; - use alloc::vec::Vec; use core_processor::{ ContextCharged, Ext, ProcessExecutionContext, common::{ExecutableActorData, JournalNote}, configs::{BlockConfig, SyscallName}, }; -use ethexe_common::{ - gear::{CHUNK_PROCESSING_GAS_LIMIT, MessageType}, - injected::Promise, -}; +use ethexe_common::gear::{CHUNK_PROCESSING_GAS_LIMIT, MessageType}; use gear_core::{ code::{CodeMetadata, InstrumentedCode, MAX_WASM_PAGES_AMOUNT}, gas::GasAllowanceCounter, @@ -79,7 +74,7 @@ pub struct ProcessQueueContext { pub code_metadata: CodeMetadata, pub gas_allowance: GasAllowanceCounter, pub block_info: BlockInfo, - pub promise_sender: mpsc::Sender, + // pub promise_sender: mpsc::Sender, } pub trait RuntimeInterface: Storage { @@ -216,9 +211,11 @@ where ri.init_lazy_pages(); for dispatch in queue { - let origin = dispatch.message_type; + let dispatch_id = dispatch.id; + let message_type = dispatch.message_type; let call_reply = dispatch.call; let is_first_execution = dispatch.context.is_none(); + let is_promise_required = dispatch.kind.is_handle() && dispatch.message_type.is_injected(); let journal = process_dispatch(dispatch, &block_config, &program_state, &ctx, ri); let mut handler = RuntimeJournalHandler { @@ -230,8 +227,38 @@ where is_first_execution, stop_processing: false, }; + + log::error!("processing dispatch: message_type={message_type:?}"); + if is_promise_required { + log::error!("promise required, notes={journal:?}"); + for note in journal.iter() { + if let JournalNote::SendDispatch { + message_id, + dispatch, + .. + } = note + && *message_id == dispatch_id + && dispatch.kind().is_reply() + { + let code = dispatch + .reply_details() + .map(|d| d.to_reply_code()) + .expect("reply details must exists for reply dispatch"); + + let reply = ReplyInfo { + value: dispatch.value(), + code, + payload: dispatch.message().payload_bytes().to_vec(), + }; + + log::error!("sending promise to user"); + ri.send_promise(&reply, &dispatch_id); + break; + } + } + } let (unhandled_journal_notes, new_state_hash) = handler.handle_journal(journal); - mega_journal.push((unhandled_journal_notes, origin, call_reply)); + mega_journal.push((unhandled_journal_notes, message_type, call_reply)); // Update state hash if it was changed. if let Some(new_state_hash) = new_state_hash { @@ -270,7 +297,6 @@ where value, details, context, - message_type, .. } = dispatch; @@ -330,21 +356,6 @@ where return core_processor::process_uninitialized(context); } - if active_state.initialized && kind.is_reply() && message_type.is_injected() { - let code = details - .and_then(|d| d.to_reply_details()) - .map(|d| d.to_reply_code()) - .expect("reply details must exists for reply dispatch"); - - let reply = ReplyInfo { - value, - code, - payload: payload.to_vec(), - }; - - ri.send_promise(&reply, &dispatch_id); - } - let context = match context.charge_for_code_metadata(block_config) { Ok(context) => context, Err(journal) => return journal, diff --git a/ethexe/runtime/common/src/transitions.rs b/ethexe/runtime/common/src/transitions.rs index 3d0774e704b..5b0d85b4060 100644 --- a/ethexe/runtime/common/src/transitions.rs +++ b/ethexe/runtime/common/src/transitions.rs @@ -148,7 +148,7 @@ impl InBlockTransitions { &self.program_creations } - /// Handles new reply for injected transaction. + // TODO: remove this in current pull request pub fn maybe_store_injected_reply(&mut self, message_id: MessageId, reply: ReplyInfo) { if self.injected_messages.contains(&message_id) { self.injected_replies.push((message_id, reply)); diff --git a/ethexe/runtime/src/wasm/interface/promise.rs b/ethexe/runtime/src/wasm/interface/promise.rs index 2b284c22ac7..76f85a97ddd 100644 --- a/ethexe/runtime/src/wasm/interface/promise.rs +++ b/ethexe/runtime/src/wasm/interface/promise.rs @@ -17,29 +17,27 @@ // along with this program. If not, see . use crate::wasm::interface; +use ethexe_runtime_common::pack_u32_to_i64; use gear_core::rpc::ReplyInfo; use gprimitives::MessageId; use parity_scale_codec::Encode; interface::declare!( - pub(super) fn send_promise( - reply_ptr: *const ReplyInfo, - encoded_reply_len: i32, - message_id_ptr: *const MessageId, + pub(super) fn ext_forward_promise_to_service( + encoded_reply_ptr_len: i64, + message_id_ptr_len: i64, ); ); - pub fn send_promise(reply: &ReplyInfo, message_id: &MessageId) { unsafe { - // TODO: implement `as_ptr` for `ReplyInfo` - let reply_ptr = 0; - let reply_encoded_size = reply.encoded_size(); - let message_id_ptr = message_id.as_ref().as_ptr(); - - sys::send_promise( - reply_ptr as _, - reply_encoded_size as i32, - message_id_ptr as _, + let message_id_ptr_len = pack_u32_to_i64( + message_id.as_ref().as_ptr() as _, + message_id.encoded_size() as _, ); + let encoded_reply = reply.encode(); + let encoded_reply_ptr_len = + pack_u32_to_i64(encoded_reply.as_ptr() as _, reply.encoded_size() as _); + + sys::ext_forward_promise_to_service(encoded_reply_ptr_len, message_id_ptr_len); } } diff --git a/ethexe/runtime/src/wasm/storage.rs b/ethexe/runtime/src/wasm/storage.rs index 7c617029853..c73040360a3 100644 --- a/ethexe/runtime/src/wasm/storage.rs +++ b/ethexe/runtime/src/wasm/storage.rs @@ -16,6 +16,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use crate::wasm::interface::promise_ri; + use super::interface::database_ri; use alloc::vec::Vec; use ethexe_common::HashOf; @@ -152,6 +154,6 @@ impl RuntimeInterface for NativeRuntimeInterface { } fn send_promise(&self, reply: &ReplyInfo, message_id: &MessageId) { - todo!() + promise_ri::send_promise(reply, message_id); } } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 9cdff36290d..57b39090b93 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -47,11 +47,7 @@ use futures::{StreamExt, stream::FuturesUnordered}; use gprimitives::{ActorId, CodeId, H256}; use gsigner::secp256k1::{Address, PrivateKey, PublicKey, Signer}; use std::{ - collections::{BTreeSet, HashMap}, - num::NonZero, - path::PathBuf, - pin::Pin, - time::Duration, + collections::{BTreeSet, HashMap}, num::NonZero, path::PathBuf, pin::Pin, sync::mpsc, time::Duration }; use tokio::sync::oneshot; @@ -272,12 +268,14 @@ impl Service { .await .with_context(|| "failed to query validators threshold")?; log::info!("🔒 Multisig threshold: {threshold} / {}", validators.len()); + let (promise_sender, _promise_receiver) = mpsc::channel(); let processor = Processor::with_config( ProcessorConfig { chunk_size: config.node.chunk_processing_threads, }, db.clone(), + Some(promise_sender) ) .with_context(|| "failed to create processor")?; diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index 47dd4ed75ea..47dee1620f9 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -76,7 +76,10 @@ use std::{ net::SocketAddr, num::NonZero, pin::Pin, - sync::atomic::{AtomicUsize, Ordering}, + sync::{ + atomic::{AtomicUsize, Ordering}, + mpsc, + }, time::Duration, }; use tokio::{task, task::JoinHandle}; @@ -886,7 +889,8 @@ impl Node { "Service is already running" ); - let processor = Processor::new(self.db.clone()).unwrap(); + let (promise_sender, _promise_receiver) = mpsc::channel(); + let processor = Processor::new(self.db.clone(), Some(promise_sender)).unwrap(); let compute = ComputeService::new(self.compute_config, self.db.clone(), processor); let observer = ObserverService::new(&self.eth_cfg, u32::MAX, self.db.clone()) From 26a48e95b55e0d3b09e6cd5e465cb7019e609cb8 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 24 Feb 2026 19:09:27 +0300 Subject: [PATCH 12/59] fix small compile error --- ethexe/compute/src/tests.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ethexe/compute/src/tests.rs b/ethexe/compute/src/tests.rs index 7973bdfa64b..4553938fa9d 100644 --- a/ethexe/compute/src/tests.rs +++ b/ethexe/compute/src/tests.rs @@ -24,7 +24,6 @@ use ethexe_common::{ BlockEvent, RouterEvent, router::{CodeGotValidatedEvent, CodeValidationRequestedEvent}, }, - injected::Promise, mock::*, }; use ethexe_db::Database; @@ -34,7 +33,7 @@ use gear_core::{ code::{CodeMetadata, InstantiatedSectionSizes, InstrumentedCode}, ids::prelude::CodeIdExt, }; -use std::{cell::RefCell, collections::BTreeMap, sync::mpsc}; +use std::{cell::RefCell, collections::BTreeMap}; thread_local! { pub(crate) static PROCESSOR_RESULT: RefCell = const { RefCell::new( @@ -174,7 +173,7 @@ impl TestEnv { let compute = ComputeService::new( config, db.clone(), - Processor::new(db.clone(), promise_sender, None).unwrap(), + Processor::new(db.clone(), None).unwrap(), ); TestEnv { db, compute, chain } From 94b27eabc3b0eb31c707bcb1cdf4c2d3fecee59b Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 25 Feb 2026 13:38:59 +0300 Subject: [PATCH 13/59] all tests run correctly --- ethexe/common/src/mock.rs | 22 +- ethexe/common/src/primitives.rs | 24 +- ethexe/compute/src/compute.rs | 25 +-- ethexe/compute/src/lib.rs | 4 +- ethexe/compute/src/service.rs | 211 +++++++++--------- ethexe/compute/src/tests.rs | 6 +- ethexe/consensus/src/connect/mod.rs | 4 +- ethexe/consensus/src/lib.rs | 9 +- ethexe/consensus/src/validator/mod.rs | 32 +-- ethexe/consensus/src/validator/producer.rs | 38 +--- ethexe/consensus/src/validator/subordinate.rs | 21 +- ethexe/processor/src/handling/overlaid.rs | 5 +- ethexe/processor/src/handling/run.rs | 17 +- ethexe/processor/src/host/api/promise.rs | 10 +- ethexe/processor/src/host/mod.rs | 5 +- ethexe/processor/src/host/threads.rs | 7 +- ethexe/processor/src/lib.rs | 33 ++- ethexe/processor/src/tests.rs | 27 ++- ethexe/runtime/common/src/journal.rs | 19 +- ethexe/runtime/common/src/lib.rs | 4 - ethexe/runtime/common/src/transitions.rs | 38 +--- ethexe/service/src/lib.rs | 69 ++++-- ethexe/service/src/tests/mod.rs | 38 +++- ethexe/service/src/tests/utils/env.rs | 20 +- ethexe/service/src/tests/utils/events.rs | 8 +- 25 files changed, 300 insertions(+), 396 deletions(-) diff --git a/ethexe/common/src/mock.rs b/ethexe/common/src/mock.rs index a133ecde58c..3734b5a0ce8 100644 --- a/ethexe/common/src/mock.rs +++ b/ethexe/common/src/mock.rs @@ -19,8 +19,8 @@ pub use tap::Tap; use crate::{ - Announce, BlockData, BlockHeader, CodeBlobInfo, ComputedAnnounce, Digest, HashOf, - ProgramStates, ProtocolTimelines, Schedule, SimpleBlockData, ValidatorsVec, + Announce, BlockData, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, + ProtocolTimelines, Schedule, SimpleBlockData, ValidatorsVec, consensus::BatchCommitmentValidationRequest, db::*, ecdsa::{PrivateKey, SignedMessage}, @@ -632,21 +632,3 @@ impl BlockData { self } } - -impl Mock for ComputedAnnounce { - fn mock(_: ()) -> Self { - Self { - announce_hash: HashOf::random(), - promises: Default::default(), - } - } -} - -impl Mock> for ComputedAnnounce { - fn mock(announce_hash: HashOf) -> Self { - Self { - announce_hash, - promises: Default::default(), - } - } -} diff --git a/ethexe/common/src/primitives.rs b/ethexe/common/src/primitives.rs index 38f3ea50a16..f5d120f2ba2 100644 --- a/ethexe/common/src/primitives.rs +++ b/ethexe/common/src/primitives.rs @@ -17,9 +17,8 @@ // along with this program. If not, see . use crate::{ - DEFAULT_BLOCK_GAS_LIMIT, HashOf, ToDigest, - events::BlockEvent, - injected::{Promise, SignedInjectedTransaction}, + DEFAULT_BLOCK_GAS_LIMIT, HashOf, ToDigest, events::BlockEvent, + injected::SignedInjectedTransaction, }; use alloc::{ collections::{btree_map::BTreeMap, btree_set::BTreeSet}, @@ -127,25 +126,6 @@ impl ToDigest for Announce { } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ComputedAnnounce { - pub announce_hash: HashOf, - pub promises: Vec, -} - -impl ComputedAnnounce { - pub fn from_announce_hash(announce_hash: HashOf) -> Self { - Self { - announce_hash, - promises: Default::default(), - } - } - - pub fn merge_promises(&mut self, other: ComputedAnnounce) { - self.promises.extend(other.promises); - } -} - #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, Default, Encode, Decode)] #[cfg_attr(feature = "std", derive(serde::Serialize))] pub struct StateHashWithQueueSize { diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 96d93f77912..892cfe08971 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -18,7 +18,7 @@ use crate::{ComputeError, ProcessorExt, Result, service::SubService}; use ethexe_common::{ - Announce, ComputedAnnounce, HashOf, SimpleBlockData, + Announce, HashOf, SimpleBlockData, db::{ AnnounceStorageRO, AnnounceStorageRW, BlockMetaStorageRO, CodesStorageRW, LatestDataStorageRO, LatestDataStorageRW, OnChainStorageRO, @@ -68,7 +68,7 @@ pub struct ComputeSubService { config: ComputeConfig, input: VecDeque, - computation: Option>>, + computation: Option>>>, } impl ComputeSubService

{ @@ -91,7 +91,7 @@ impl ComputeSubService

{ config: ComputeConfig, mut processor: P, announce: Announce, - ) -> Result { + ) -> Result> { let announce_hash = announce.to_hash(); let block_hash = announce.block_hash; @@ -116,19 +116,16 @@ impl ComputeSubService

{ parent_hash = next_parent_hash; } - let mut computed_announce = ComputedAnnounce::from_announce_hash(announce_hash); if announces_chain.is_empty() { log::trace!("All announces are already computed"); - return Ok(computed_announce); + return Ok(announce_hash); } for (announce_hash, announce) in announces_chain { - computed_announce.merge_promises( - Self::compute_one(&db, &mut processor, announce_hash, announce, config).await?, - ); + Self::compute_one(&db, &mut processor, announce_hash, announce, config).await?; } - Ok(computed_announce) + Ok(announce_hash) } async fn compute_one( @@ -137,7 +134,7 @@ impl ComputeSubService

{ announce_hash: HashOf, announce: Announce, config: ComputeConfig, - ) -> Result { + ) -> Result> { let executable = prepare_executable_for_announce(db, announce, config.canonical_quarantine())?; let processing_result = processor.process_announce(executable).await?; @@ -146,7 +143,6 @@ impl ComputeSubService

{ transitions, states, schedule, - promises, program_creations, } = processing_result; @@ -168,15 +164,12 @@ impl ComputeSubService

{ }) .ok_or(ComputeError::LatestDataNotFound)?; - Ok(ComputedAnnounce { - announce_hash, - promises, - }) + Ok(announce_hash) } } impl SubService for ComputeSubService

{ - type Output = ComputedAnnounce; + type Output = HashOf; fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll> { if self.computation.is_none() diff --git a/ethexe/compute/src/lib.rs b/ethexe/compute/src/lib.rs index beb30deef4a..110146260ef 100644 --- a/ethexe/compute/src/lib.rs +++ b/ethexe/compute/src/lib.rs @@ -16,7 +16,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use ethexe_common::{Announce, CodeAndIdUnchecked, ComputedAnnounce, HashOf}; +use ethexe_common::{Announce, CodeAndIdUnchecked, HashOf}; use ethexe_processor::{ExecutableData, ProcessedCodeInfo, Processor, ProcessorError}; use ethexe_runtime_common::FinalizedBlockTransitions; use gprimitives::{CodeId, H256}; @@ -43,7 +43,7 @@ pub enum ComputeEvent { RequestLoadCodes(HashSet), CodeProcessed(CodeId), BlockPrepared(H256), - AnnounceComputed(ComputedAnnounce), + AnnounceComputed(HashOf), } #[derive(thiserror::Error, Debug)] diff --git a/ethexe/compute/src/service.rs b/ethexe/compute/src/service.rs index 251325d0563..95c95b38702 100644 --- a/ethexe/compute/src/service.rs +++ b/ethexe/compute/src/service.rs @@ -158,110 +158,107 @@ pub(crate) trait SubService: Unpin + Send + 'static { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::tests::MockProcessor; -// use ethexe_common::{CodeAndIdUnchecked, ComputedAnnounce, db::*, mock::*}; -// use ethexe_db::Database as DB; -// use futures::StreamExt; -// use gear_core::ids::prelude::CodeIdExt; -// use gprimitives::CodeId; - -// /// Test ComputeService block preparation functionality -// #[tokio::test] -// async fn prepare_block() { -// gear_utils::init_default_logger(); - -// let db = DB::memory(); -// let processor = MockProcessor; -// let config = ComputeConfig::without_quarantine(); -// let mut service = ComputeService::new(config, db.clone(), processor); - -// let chain = BlockChain::mock(1).setup(&db); -// let block = chain.blocks[1].to_simple().next_block().setup(&db); - -// // Request block preparation -// service.prepare_block(block.hash); - -// // Poll service to process the preparation request -// let event = service.next().await.unwrap().unwrap(); -// assert_eq!(event, ComputeEvent::BlockPrepared(block.hash)); - -// // Verify block is marked as prepared in DB -// assert!(db.block_meta(block.hash).prepared); -// } - -// /// Test ComputeService block processing functionality -// #[tokio::test] -// async fn compute_announce() { -// gear_utils::init_default_logger(); - -// let db = DB::memory(); -// let processor = MockProcessor; - -// let config = ComputeConfig::without_quarantine(); -// let mut service = ComputeService::new(config, db.clone(), processor); -// let chain = BlockChain::mock(1).setup(&db); - -// let block = chain.blocks[1].to_simple().next_block().setup(&db); - -// service.prepare_block(block.hash); -// let event = service.next().await.unwrap().unwrap(); -// assert_eq!(event, ComputeEvent::BlockPrepared(block.hash)); - -// // Request computation -// let announce = Announce { -// block_hash: block.hash, -// parent: chain.block_top_announce_hash(1), -// gas_allowance: Some(42), -// injected_transactions: vec![], -// }; -// let announce_hash = announce.to_hash(); -// service.compute_announce(announce); - -// // Poll service to process the block -// let event = service.next().await.unwrap().unwrap(); -// assert_eq!( -// event, -// ComputeEvent::AnnounceComputed(ComputedAnnounce::mock(announce_hash)) -// ); - -// // Verify block is marked as computed in DB -// assert!(db.announce_meta(announce_hash).computed); -// } - -// /// Test ComputeService code processing functionality -// #[tokio::test] -// async fn process_code() { -// gear_utils::init_default_logger(); - -// let db = DB::memory(); -// let processor = MockProcessor; -// let config = ComputeConfig::without_quarantine(); -// let mut service = ComputeService::new(config, db.clone(), processor); - -// // Create test code -// let code = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]; // Simple WASM header -// let code_id = CodeId::generate(&code); - -// let code_and_id = CodeAndIdUnchecked { code, code_id }; - -// // Verify code is not yet in DB -// assert!(db.code_valid(code_id).is_none()); - -// // Request code processing -// service.process_code(code_and_id); - -// // Poll service to process the code -// let event = service.next().await.unwrap().unwrap(); - -// // Should receive CodeProcessed event with correct code_id -// match event { -// ComputeEvent::CodeProcessed(processed_code_id) => { -// assert_eq!(processed_code_id, code_id); -// } -// _ => panic!("Expected CodeProcessed event"), -// } -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::MockProcessor; + use ethexe_common::{CodeAndIdUnchecked, db::*, mock::*}; + use ethexe_db::Database as DB; + use futures::StreamExt; + use gear_core::ids::prelude::CodeIdExt; + use gprimitives::CodeId; + + /// Test ComputeService block preparation functionality + #[tokio::test] + async fn prepare_block() { + gear_utils::init_default_logger(); + + let db = DB::memory(); + let processor = MockProcessor; + let config = ComputeConfig::without_quarantine(); + let mut service = ComputeService::new(config, db.clone(), processor); + + let chain = BlockChain::mock(1).setup(&db); + let block = chain.blocks[1].to_simple().next_block().setup(&db); + + // Request block preparation + service.prepare_block(block.hash); + + // Poll service to process the preparation request + let event = service.next().await.unwrap().unwrap(); + assert_eq!(event, ComputeEvent::BlockPrepared(block.hash)); + + // Verify block is marked as prepared in DB + assert!(db.block_meta(block.hash).prepared); + } + + /// Test ComputeService block processing functionality + #[tokio::test] + async fn compute_announce() { + gear_utils::init_default_logger(); + + let db = DB::memory(); + let processor = MockProcessor; + + let config = ComputeConfig::without_quarantine(); + let mut service = ComputeService::new(config, db.clone(), processor); + let chain = BlockChain::mock(1).setup(&db); + + let block = chain.blocks[1].to_simple().next_block().setup(&db); + + service.prepare_block(block.hash); + let event = service.next().await.unwrap().unwrap(); + assert_eq!(event, ComputeEvent::BlockPrepared(block.hash)); + + // Request computation + let announce = Announce { + block_hash: block.hash, + parent: chain.block_top_announce_hash(1), + gas_allowance: Some(42), + injected_transactions: vec![], + }; + let announce_hash = announce.to_hash(); + service.compute_announce(announce); + + // Poll service to process the block + let event = service.next().await.unwrap().unwrap(); + assert_eq!(event, ComputeEvent::AnnounceComputed(announce_hash)); + + // Verify block is marked as computed in DB + assert!(db.announce_meta(announce_hash).computed); + } + + /// Test ComputeService code processing functionality + #[tokio::test] + async fn process_code() { + gear_utils::init_default_logger(); + + let db = DB::memory(); + let processor = MockProcessor; + let config = ComputeConfig::without_quarantine(); + let mut service = ComputeService::new(config, db.clone(), processor); + + // Create test code + let code = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]; // Simple WASM header + let code_id = CodeId::generate(&code); + + let code_and_id = CodeAndIdUnchecked { code, code_id }; + + // Verify code is not yet in DB + assert!(db.code_valid(code_id).is_none()); + + // Request code processing + service.process_code(code_and_id); + + // Poll service to process the code + let event = service.next().await.unwrap().unwrap(); + + // Should receive CodeProcessed event with correct code_id + match event { + ComputeEvent::CodeProcessed(processed_code_id) => { + assert_eq!(processed_code_id, code_id); + } + _ => panic!("Expected CodeProcessed event"), + } + } +} diff --git a/ethexe/compute/src/tests.rs b/ethexe/compute/src/tests.rs index 4553938fa9d..3d36eed20ef 100644 --- a/ethexe/compute/src/tests.rs +++ b/ethexe/compute/src/tests.rs @@ -41,7 +41,6 @@ thread_local! { transitions: Vec::new(), states: BTreeMap::new(), schedule: BTreeMap::new(), - promises: Vec::new(), program_creations: Vec::new(), } ) }; @@ -62,7 +61,6 @@ impl ProcessorExt for MockProcessor { transitions: vec![], states: BTreeMap::new(), schedule: BTreeMap::new(), - promises: vec![], program_creations: vec![], } }); @@ -234,8 +232,8 @@ impl TestEnv { .unwrap() .expect("expect block will be processing"); - let computed_data = event.unwrap_announce_computed(); - assert_eq!(computed_data.announce_hash, announce_hash); + let computed_announce = event.unwrap_announce_computed(); + assert_eq!(computed_announce, announce_hash); self.db.mutate_block_meta(announce.block_hash, |meta| { meta.announces.get_or_insert_default().insert(announce_hash); diff --git a/ethexe/consensus/src/connect/mod.rs b/ethexe/consensus/src/connect/mod.rs index 2b3e57b127b..fc672c8be64 100644 --- a/ethexe/consensus/src/connect/mod.rs +++ b/ethexe/consensus/src/connect/mod.rs @@ -27,7 +27,7 @@ use crate::{ }; use anyhow::{Result, anyhow}; use ethexe_common::{ - Address, Announce, ComputedAnnounce, SimpleBlockData, + Address, Announce, HashOf, SimpleBlockData, consensus::{VerifiedAnnounce, VerifiedValidationRequest}, db::OnChainStorageRO, injected::SignedInjectedTransaction, @@ -262,7 +262,7 @@ impl ConsensusService for ConnectService { Ok(()) } - fn receive_computed_announce(&mut self, _computed_data: ComputedAnnounce) -> Result<()> { + fn receive_computed_announce(&mut self, _announce_hash: HashOf) -> Result<()> { Ok(()) } diff --git a/ethexe/consensus/src/lib.rs b/ethexe/consensus/src/lib.rs index b9c4e7eef8a..b204b196537 100644 --- a/ethexe/consensus/src/lib.rs +++ b/ethexe/consensus/src/lib.rs @@ -34,9 +34,9 @@ use anyhow::Result; use ethexe_common::{ - Announce, ComputedAnnounce, Digest, HashOf, SimpleBlockData, + Announce, Digest, HashOf, SimpleBlockData, consensus::{BatchCommitmentValidationReply, VerifiedAnnounce, VerifiedValidationRequest}, - injected::{SignedInjectedTransaction, SignedPromise}, + injected::SignedInjectedTransaction, network::{AnnouncesRequest, AnnouncesResponse, SignedValidatorMessage}, }; use futures::{Stream, stream::FusedStream}; @@ -71,7 +71,7 @@ pub trait ConsensusService: fn receive_prepared_block(&mut self, block: H256) -> Result<()>; /// Process a computed block received - fn receive_computed_announce(&mut self, computed_data: ComputedAnnounce) -> Result<()>; + fn receive_computed_announce(&mut self, computed_data: HashOf) -> Result<()>; /// Process a received producer announce fn receive_announce(&mut self, announce: VerifiedAnnounce) -> Result<()>; @@ -122,7 +122,4 @@ pub enum ConsensusEvent { CommitmentSubmitted(CommitmentSubmitted), /// Informational event: during service processing, a warning situation was detected Warning(String), - /// Promises for [`ethexe_common::injected::InjectedTransaction`]s execution in some announce. - #[from] - Promises(Vec), } diff --git a/ethexe/consensus/src/validator/mod.rs b/ethexe/consensus/src/validator/mod.rs index 2c303ae7e74..3a8f207a9cc 100644 --- a/ethexe/consensus/src/validator/mod.rs +++ b/ethexe/consensus/src/validator/mod.rs @@ -54,10 +54,10 @@ use anyhow::{Result, anyhow}; pub use core::BatchCommitter; use derive_more::{Debug, From}; use ethexe_common::{ - Address, ComputedAnnounce, SimpleBlockData, ToDigest, + Address, Announce, HashOf, SimpleBlockData, consensus::{VerifiedAnnounce, VerifiedValidationRequest}, db::OnChainStorageRO, - ecdsa::{PublicKey, SignedMessage}, + ecdsa::PublicKey, injected::SignedInjectedTransaction, network::AnnouncesResponse, }; @@ -69,7 +69,7 @@ use futures::{ stream::{FusedStream, FuturesUnordered}, }; use gprimitives::H256; -use gsigner::secp256k1::{Secp256k1SignerExt, Signer}; +use gsigner::secp256k1::Signer; use initial::Initial; use std::{ collections::VecDeque, @@ -210,8 +210,8 @@ impl ConsensusService for ValidatorService { self.update_inner(|inner| inner.process_prepared_block(block)) } - fn receive_computed_announce(&mut self, computed_data: ComputedAnnounce) -> Result<()> { - self.update_inner(|inner| inner.process_computed_announce(computed_data)) + fn receive_computed_announce(&mut self, announce_hash: HashOf) -> Result<()> { + self.update_inner(|inner| inner.process_computed_announce(announce_hash)) } fn receive_announce(&mut self, announce: VerifiedAnnounce) -> Result<()> { @@ -314,8 +314,8 @@ where DefaultProcessing::prepared_block(self.into(), block) } - fn process_computed_announce(self, computed_data: ComputedAnnounce) -> Result { - DefaultProcessing::computed_announce(self.into(), computed_data) + fn process_computed_announce(self, announce_hash: HashOf) -> Result { + DefaultProcessing::computed_announce(self.into(), announce_hash) } fn process_announce(self, announce: VerifiedAnnounce) -> Result { @@ -402,8 +402,8 @@ impl StateHandler for ValidatorState { delegate_call!(self => process_prepared_block(block)) } - fn process_computed_announce(self, computed_data: ComputedAnnounce) -> Result { - delegate_call!(self => process_computed_announce(computed_data)) + fn process_computed_announce(self, announce_hash: HashOf) -> Result { + delegate_call!(self => process_computed_announce(announce_hash)) } fn process_announce(self, verified_announce: VerifiedAnnounce) -> Result { @@ -458,13 +458,10 @@ impl DefaultProcessing { fn computed_announce( s: impl Into, - computed_data: ComputedAnnounce, + announce_hash: HashOf, ) -> Result { let mut s = s.into(); - s.warning(format!( - "unexpected computed announce: {}", - computed_data.announce_hash - )); + s.warning(format!("unexpected computed announce: {}", announce_hash)); Ok(s) } @@ -547,11 +544,4 @@ impl ValidatorContext { pub fn pending(&mut self, event: impl Into) { self.pending_events.push_front(event.into()); } - - pub fn sign_message(&self, data: T) -> Result> { - Ok(self - .core - .signer - .signed_message(self.core.pub_key, data, None)?) - } } diff --git a/ethexe/consensus/src/validator/producer.rs b/ethexe/consensus/src/validator/producer.rs index 34091430069..7a822cb1a65 100644 --- a/ethexe/consensus/src/validator/producer.rs +++ b/ethexe/consensus/src/validator/producer.rs @@ -24,10 +24,10 @@ use crate::{ announces::{self, DBAnnouncesExt}, validator::DefaultProcessing, }; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Result, anyhow}; use derive_more::{Debug, Display}; use ethexe_common::{ - Announce, ComputedAnnounce, HashOf, SimpleBlockData, ValidatorsVec, db::BlockMetaStorageRO, + Announce, HashOf, SimpleBlockData, ValidatorsVec, db::BlockMetaStorageRO, gear::BatchCommitment, network::ValidatorMessage, }; use ethexe_service_utils::Timer; @@ -75,26 +75,10 @@ impl StateHandler for Producer { fn process_computed_announce( mut self, - computed_data: ComputedAnnounce, + announce_hash: HashOf, ) -> Result { match &self.state { - State::WaitingAnnounceComputed(expected) - if *expected == computed_data.announce_hash => - { - if !computed_data.promises.is_empty() { - let signed_promises = computed_data - .promises - .into_iter() - .map(|promise| { - self.ctx - .sign_message(promise) - .context("producer: failed to sign promise") - }) - .collect::>()?; - - self.ctx.output(ConsensusEvent::Promises(signed_promises)); - } - + State::WaitingAnnounceComputed(expected) if *expected == announce_hash => { // Aggregate commitment for the block and use `announce_hash` as head for chain commitment. // `announce_hash` is computed and included in the db already, so it's safe to use it. self.state = State::AggregateBatchCommitment { @@ -102,7 +86,7 @@ impl StateHandler for Producer { .ctx .core .clone() - .aggregate_batch_commitment(self.block, computed_data.announce_hash) + .aggregate_batch_commitment(self.block, announce_hash) .boxed(), }; @@ -111,12 +95,12 @@ impl StateHandler for Producer { State::WaitingAnnounceComputed(expected) => { self.warning(format!( "Computed announce {} is not expected, expected {expected}", - computed_data.announce_hash + announce_hash )); Ok(self.into()) } - _ => DefaultProcessing::computed_announce(self, computed_data), + _ => DefaultProcessing::computed_announce(self, announce_hash), } } @@ -293,7 +277,7 @@ mod tests { .setup(&state.context().core.db); let state = state - .process_computed_announce(ComputedAnnounce::mock(announce_hash)) + .process_computed_announce(announce_hash) .unwrap() .wait_for_state(|state| state.is_initial()) .await @@ -340,7 +324,7 @@ mod tests { .setup(&state.context().core.db); let mut state = state - .process_computed_announce(ComputedAnnounce::mock(announce_hash)) + .process_computed_announce(announce_hash) .unwrap() .wait_for_state(|state| matches!(state, ValidatorState::Initial(_))) .await @@ -385,7 +369,7 @@ mod tests { .setup(&state.context().core.db); let (state, event) = state - .process_computed_announce(ComputedAnnounce::mock(announce_hash)) + .process_computed_announce(announce_hash) .unwrap() .wait_for_event() .await @@ -429,7 +413,7 @@ mod tests { .setup(&state.context().core.db); let mut state = state - .process_computed_announce(ComputedAnnounce::mock(announce_hash)) + .process_computed_announce(announce_hash) .unwrap() .wait_for_state(|state| matches!(state, ValidatorState::Initial(_))) .await diff --git a/ethexe/consensus/src/validator/subordinate.rs b/ethexe/consensus/src/validator/subordinate.rs index 3a5b7961ec8..696a981b929 100644 --- a/ethexe/consensus/src/validator/subordinate.rs +++ b/ethexe/consensus/src/validator/subordinate.rs @@ -28,7 +28,7 @@ use crate::{ use anyhow::Result; use derive_more::{Debug, Display}; use ethexe_common::{ - Address, Announce, ComputedAnnounce, HashOf, SimpleBlockData, + Address, Announce, HashOf, SimpleBlockData, consensus::{VerifiedAnnounce, VerifiedValidationRequest}, }; use std::mem; @@ -70,10 +70,13 @@ impl StateHandler for Subordinate { self.ctx } - fn process_computed_announce(self, computed_data: ComputedAnnounce) -> Result { + fn process_computed_announce( + self, + computed_announce_hash: HashOf, + ) -> Result { match &self.state { State::WaitingAnnounceComputed { announce_hash } - if *announce_hash == computed_data.announce_hash => + if *announce_hash == computed_announce_hash => { if self.is_validator { Participant::create(self.ctx, self.block, self.producer) @@ -81,7 +84,7 @@ impl StateHandler for Subordinate { Initial::create(self.ctx) } } - _ => DefaultProcessing::computed_announce(self, computed_data), + _ => DefaultProcessing::computed_announce(self, computed_announce_hash), } } @@ -192,7 +195,7 @@ impl Subordinate { mod tests { use super::*; use crate::{mock::*, validator::mock::*}; - use ethexe_common::{ComputedAnnounce, mock::*}; + use ethexe_common::mock::*; #[test] fn create_empty() { @@ -326,7 +329,7 @@ mod tests { // After announce is computed, subordinate switches to participant state. let s = s - .process_computed_announce(ComputedAnnounce::mock(announce.data().to_hash())) + .process_computed_announce(announce.data().to_hash()) .unwrap(); assert!(s.is_participant(), "got {s:?}"); assert_eq!( @@ -368,7 +371,7 @@ mod tests { // After announce is computed, not-validator subordinate switches to initial state. let s = s - .process_computed_announce(ComputedAnnounce::mock(announce.data().to_hash())) + .process_computed_announce(announce.data().to_hash()) .unwrap(); assert!(s.is_initial(), "got {s:?}"); } @@ -428,9 +431,7 @@ mod tests { let s = Subordinate::create(ctx, block, producer.to_address(), true).unwrap(); - let s = s - .process_computed_announce(ComputedAnnounce::mock(())) - .unwrap(); + let s = s.process_computed_announce(HashOf::random()).unwrap(); assert_eq!(s.context().output.len(), 1); assert!(matches!(s.context().output[0], ConsensusEvent::Warning(_))); } diff --git a/ethexe/processor/src/handling/overlaid.rs b/ethexe/processor/src/handling/overlaid.rs index a1d2f0e31ed..ab8d60571ac 100644 --- a/ethexe/processor/src/handling/overlaid.rs +++ b/ethexe/processor/src/handling/overlaid.rs @@ -34,7 +34,8 @@ use gear_core::{ message::ReplyDetails, }; use gprimitives::{ActorId, MessageId}; -use std::{collections::HashSet, sync::mpsc}; +use std::collections::HashSet; +use tokio::sync::mpsc; /// Overlay execution context. /// @@ -93,7 +94,7 @@ impl OverlaidRunContext { pub(crate) async fn run( mut self, - promise_sender: Option>, + promise_sender: Option>, ) -> Result { let _ = run::run_for_queue_type(&mut self, MessageType::Canonical, promise_sender).await?; Ok(self.inner.transitions) diff --git a/ethexe/processor/src/handling/run.rs b/ethexe/processor/src/handling/run.rs index 8c2741ee45d..c8a5cb38c29 100644 --- a/ethexe/processor/src/handling/run.rs +++ b/ethexe/processor/src/handling/run.rs @@ -106,8 +106,6 @@ // TODO: #5120 split to several files and move to separate module -use std::sync::mpsc; - use crate::{ ProcessorError, Result, handling::run::chunks_splitting::ExecutionChunks, host::InstanceCreator, }; @@ -130,13 +128,14 @@ use gear_core::{ }; use gprimitives::{ActorId, CodeId, H256}; use itertools::Itertools; +use tokio::sync::mpsc; // Process chosen queue type in chunks // Returns whether execution is NOT run out of gas (execution can be continued) pub(super) async fn run_for_queue_type( ctx: &mut impl RunContext, queue_type: MessageType, - promise_sender: Option>, + promise_sender: Option>, ) -> Result { let mut is_out_of_gas_for_block = false; @@ -272,7 +271,6 @@ pub(crate) struct CommonRunContext { pub(crate) gas_allowance_counter: GasAllowanceCounter, pub(crate) chunk_size: usize, pub(crate) block_header: BlockHeader, - // pub(crate) promise_sender: mpsc::Sender, } impl CommonRunContext { @@ -283,7 +281,6 @@ impl CommonRunContext { gas_allowance: u64, chunk_size: usize, block_header: BlockHeader, - // promise_sender: mpsc::Sender, ) -> Self { CommonRunContext { db, @@ -292,13 +289,12 @@ impl CommonRunContext { gas_allowance_counter: GasAllowanceCounter::new(gas_allowance), chunk_size, block_header, - // promise_sender, } } pub(crate) async fn run( mut self, - promise_sender: Option>, + promise_sender: Option>, ) -> Result { // Start with injected queues processing. let can_continue = @@ -527,7 +523,7 @@ mod chunk_execution_spawn { ctx: &mut impl RunContext, chunk: Vec<(ActorId, H256)>, queue_type: MessageType, - promise_sender: Option>, + promise_sender: Option>, ) -> Result> { struct Executable { program_id: ActorId, @@ -752,7 +748,7 @@ mod tests { .take(STATE_SIZE) .collect(); - let transitions = InBlockTransitions::new(0, states, Default::default(), vec![]); + let transitions = InBlockTransitions::new(0, states, Default::default()); let mut ctx = CommonRunContext { db: Database::memory(), @@ -887,8 +883,7 @@ mod tests { (pid2, pid2_state_hash_with_queue_size), ]); - let mut in_block_transitions = - InBlockTransitions::new(3, states, Default::default(), vec![]); + let mut in_block_transitions = InBlockTransitions::new(3, states, Default::default()); let base_program = pid2; diff --git a/ethexe/processor/src/host/api/promise.rs b/ethexe/processor/src/host/api/promise.rs index edd4e3de26b..fe49d0ed511 100644 --- a/ethexe/processor/src/host/api/promise.rs +++ b/ethexe/processor/src/host/api/promise.rs @@ -38,20 +38,18 @@ fn forward_promise( let memory = MemoryWrap(caller.data().memory()); let reply = memory.decode_by_val(&caller, encoded_reply_ptr_len); - let message_id: MessageId = memory.decode_by_val(&caller, message_id_ptr_len); + let message_id = memory.decode_by_val::<_, MessageId>(&caller, message_id_ptr_len); threads::with_params(|params| { if let Some(ref sender) = params.promise_sender { - log::error!("calling `forward_promise` reply={reply:?}"); - let tx_hash = unsafe { HashOf::new(message_id.into_bytes().into()) }; let promise = Promise { tx_hash, reply }; match sender.send(promise) { Ok(()) => { - // log::trace!( - // "successfully send promise to outer service: reply_ptr_len={reply_ptr_len}, message_id_ptr_len={message_id_ptr_len}" - // ); + log::trace!( + "successfully send promise to outer service: encoded_reply_ptr_len={encoded_reply_ptr_len}, message_id_ptr_len={message_id_ptr_len}" + ); } Err(err) => { log::trace!("failed to send promise to outer service: error={err}"); diff --git a/ethexe/processor/src/host/mod.rs b/ethexe/processor/src/host/mod.rs index 62972c5058e..20e42aa57ef 100644 --- a/ethexe/processor/src/host/mod.rs +++ b/ethexe/processor/src/host/mod.rs @@ -25,7 +25,8 @@ use gprimitives::H256; use parity_scale_codec::{Decode, Encode}; use sp_allocator::{AllocationStats, FreeingBumpHeapAllocator}; use sp_wasm_interface::{HostState, IntoValue, MemoryWrapper, StoreData}; -use std::sync::{Arc, mpsc}; +use std::sync::Arc; +use tokio::sync::mpsc; pub mod api; pub mod runtime; @@ -170,7 +171,7 @@ impl InstanceWrapper { &mut self, db: Box, ctx: ProcessQueueContext, - promise_sender: Option>, + promise_sender: Option>, ) -> Result<(ProgramJournals, H256, u64)> { threads::set(db, ctx.state_root, promise_sender.clone()); diff --git a/ethexe/processor/src/host/threads.rs b/ethexe/processor/src/host/threads.rs index ab334aa3ca1..b30aa2ac5d1 100644 --- a/ethexe/processor/src/host/threads.rs +++ b/ethexe/processor/src/host/threads.rs @@ -29,7 +29,8 @@ use gear_core::{ids::ActorId, memory::PageBuf, pages::GearPage}; use gear_lazy_pages::LazyPagesStorage; use gprimitives::H256; use parity_scale_codec::{Decode, DecodeAll}; -use std::{cell::RefCell, collections::BTreeMap, sync::mpsc}; +use std::{cell::RefCell, collections::BTreeMap}; +use tokio::sync::mpsc; const UNSET_PANIC: &str = "params should be set before query"; const UNKNOWN_STATE: &str = "state should always be valid (must exist)"; @@ -42,7 +43,7 @@ pub struct ThreadParams { pub db: Box, pub state_hash: H256, /// TODO: think about using [`mpsc::sync_channel`] instead of [`mpsc::channel`]. - pub promise_sender: Option>, + pub promise_sender: Option>, pages_registry_cache: Option, pages_regions_cache: Option>, } @@ -107,7 +108,7 @@ impl PageKey { pub fn set( db: Box, state_hash: H256, - promise_sender: Option>, + promise_sender: Option>, ) { PARAMS.set(Some(ThreadParams { db, diff --git a/ethexe/processor/src/lib.rs b/ethexe/processor/src/lib.rs index 46567692e1f..e7e43416762 100644 --- a/ethexe/processor/src/lib.rs +++ b/ethexe/processor/src/lib.rs @@ -38,7 +38,7 @@ use gear_core::{ use gprimitives::{ActorId, CodeId, H256, MessageId}; use handling::{ProcessingHandler, overlaid::OverlaidRunContext, run::CommonRunContext}; use host::InstanceCreator; -use std::sync::mpsc; +use tokio::sync::mpsc; pub use host::InstanceError; @@ -108,21 +108,24 @@ pub struct Processor { config: ProcessorConfig, db: Database, creator: InstanceCreator, - promise_sender: Option>, + promise_sender: Option>, } /// TODO: consider avoiding re-instantiations on processing events. /// Maybe impl `struct EventProcessor`. impl Processor { /// Creates processor with default config. - pub fn new(db: Database, promise_sender: Option>) -> Result { + pub fn new( + db: Database, + promise_sender: Option>, + ) -> Result { Self::with_config(Default::default(), db, promise_sender) } pub fn with_config( config: ProcessorConfig, db: Database, - promise_sender: Option>, + promise_sender: Option>, ) -> Result { let creator = InstanceCreator::new(host::runtime())?; Ok(Self { @@ -189,24 +192,18 @@ impl Processor { block, program_states, schedule, + // TODO: remove injected_transactions, gas_allowance, events, } = executable; - let injected_messages = injected_transactions - .iter() - .map(|tx| tx.data().to_message_id()); - - let mut transitions = InBlockTransitions::new( - block.header.height, - program_states, - schedule, - injected_messages, - ); + let mut transitions = + InBlockTransitions::new(block.header.height, program_states, schedule); transitions = self.process_injected_and_events(transitions, injected_transactions, events)?; + if let Some(gas_allowance) = gas_allowance { transitions = self .process_queues(transitions, block, gas_allowance) @@ -376,12 +373,8 @@ impl OverlaidProcessor { return Err(ExecuteForReplyError::ProgramNotInitialized(program_id)); } - let transitions = InBlockTransitions::new( - block.header.height, - program_states, - Schedule::default(), - vec![], - ); + let transitions = + InBlockTransitions::new(block.header.height, program_states, Schedule::default()); let transitions = self.0.process_injected_and_events( transitions, diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index ce1bfd62136..f2a2e8398fb 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -98,7 +98,7 @@ mod utils { pub fn setup_test_env_and_load_codes( codes: [&[u8]; N], - promise_sender: Option>, + promise_sender: Option>, ) -> (Processor, BlockChain, [CodeId; N]) { let db = Database::memory(); let mut processor = Processor::new(db.clone(), promise_sender).unwrap(); @@ -113,12 +113,8 @@ mod utils { } pub fn setup_handler(db: Database, block: SimpleBlockData) -> ProcessingHandler { - let transitions = InBlockTransitions::new( - block.header.height, - Default::default(), - Default::default(), - vec![], - ); + let transitions = + InBlockTransitions::new(block.header.height, Default::default(), Default::default()); ProcessingHandler::new(db, transitions) } @@ -590,7 +586,7 @@ async fn many_waits() { .for_each(|(pid, cid)| processor.db.set_program_code_id(pid, cid)); // Check all messages wake up and reply with "Hello, world!" - let transitions = InBlockTransitions::new(wake_block.header.height, states, schedule, vec![]); + let transitions = InBlockTransitions::new(wake_block.header.height, states, schedule); let transitions = processor.process_tasks(transitions); let transitions = processor .process_queues(transitions, wake_block, DEFAULT_BLOCK_GAS_LIMIT) @@ -726,7 +722,7 @@ async fn overlay_execution() { let block2 = chain.blocks[2].to_simple(); let mut handler = ProcessingHandler::new( processor.db.clone(), - InBlockTransitions::new(block2.header.height, states, schedule, vec![]), + InBlockTransitions::new(block2.header.height, states, schedule), ); // Manually add messages to programs queues @@ -857,7 +853,7 @@ async fn overlay_execution() { async fn injected_ping_pong() { init_logger(); - let (promise_sender, promise_receiver) = mpsc::channel(); + let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY], Some(promise_sender)); let block1 = chain.blocks[1].to_simple(); @@ -930,6 +926,7 @@ async fn injected_ping_pong() { let promise = promise_receiver .recv() + .await .expect("promise must be sent after processing"); assert_eq!(promise.tx_hash, injected_tx.to_hash()); @@ -964,7 +961,7 @@ async fn injected_prioritized_over_canonical() { init_logger(); - let (promise_sender, promise_receiver) = mpsc::channel(); + let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY], Some(promise_sender)); let block1 = chain.blocks[1].to_simple(); @@ -1042,9 +1039,10 @@ async fn injected_prioritized_over_canonical() { .await .unwrap(); - tx_hahes.into_iter().for_each(|tx_hash| { + for tx_hash in tx_hahes { let promise = promise_receiver .recv() + .await .expect("promise for injected transaction"); assert_eq!(promise.tx_hash, tx_hash); @@ -1054,7 +1052,7 @@ async fn injected_prioritized_over_canonical() { ReplyCode::Success(SuccessReplyReason::Manual) ); assert_eq!(promise.reply.payload, b"PONG"); - }); + } // Verify that injected messages were processed first // skip the first message which is INIT reply @@ -1158,7 +1156,7 @@ async fn executable_balance_injected_panic_not_charged() { init_logger(); - let (promise_sender, promise_receiver) = mpsc::channel(); + let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_panic_payload::WASM_BINARY], Some(promise_sender)); let block1 = chain.blocks[1].to_simple(); @@ -1221,6 +1219,7 @@ async fn executable_balance_injected_panic_not_charged() { let panic_promise = promise_receiver .recv() + .await .expect("promise for injected transaction"); assert_eq!(panic_promise.tx_hash, panic_tx.to_hash()); assert_eq!(panic_promise.reply.value, 0); diff --git a/ethexe/runtime/common/src/journal.rs b/ethexe/runtime/common/src/journal.rs index 74aff63c908..c69e2565152 100644 --- a/ethexe/runtime/common/src/journal.rs +++ b/ethexe/runtime/common/src/journal.rs @@ -16,10 +16,9 @@ use gear_core::{ env::MessageWaitedType, gas::GasAllowanceCounter, memory::PageBuf, - message::{Dispatch as CoreDispatch, DispatchKind, StoredDispatch}, + message::{Dispatch as CoreDispatch, StoredDispatch}, pages::{GearPage, WasmPage, num_traits::Zero as _, numerated::tree::IntervalsTree}, reservation::GasReserver, - rpc::ReplyInfo, }; use gear_core_errors::SignalCode; use gprimitives::{ActorId, CodeId, H256, MessageId, ReservationId}; @@ -252,20 +251,6 @@ impl JournalHandler for NativeJournalHandler<'_, S> { let destination = dispatch.destination(); let dispatch = dispatch.into_stored(); - if self.message_type == MessageType::Injected && dispatch.kind() == DispatchKind::Reply { - let reply_info = ReplyInfo { - payload: dispatch.payload_bytes().to_vec(), - code: dispatch - .reply_code() - .expect("expect reply_code in dispatch with DispatchKind::Reply"), - value: dispatch.value(), - }; - - self.controller - .transitions - .maybe_store_injected_reply(message_id, reply_info); - } - if self.controller.transitions.is_program(&destination) { let dispatch = Dispatch::from_core_stored( self.controller.storage, @@ -672,7 +657,7 @@ where #[cfg(test)] mod tests { - use gear_core::message::{Message as CoreMessage, StoredMessage}; + use gear_core::message::{DispatchKind, Message as CoreMessage, StoredMessage}; use super::*; diff --git a/ethexe/runtime/common/src/lib.rs b/ethexe/runtime/common/src/lib.rs index 49058a02cdf..fda22458ebb 100644 --- a/ethexe/runtime/common/src/lib.rs +++ b/ethexe/runtime/common/src/lib.rs @@ -74,7 +74,6 @@ pub struct ProcessQueueContext { pub code_metadata: CodeMetadata, pub gas_allowance: GasAllowanceCounter, pub block_info: BlockInfo, - // pub promise_sender: mpsc::Sender, } pub trait RuntimeInterface: Storage { @@ -228,9 +227,7 @@ where stop_processing: false, }; - log::error!("processing dispatch: message_type={message_type:?}"); if is_promise_required { - log::error!("promise required, notes={journal:?}"); for note in journal.iter() { if let JournalNote::SendDispatch { message_id, @@ -251,7 +248,6 @@ where payload: dispatch.message().payload_bytes().to_vec(), }; - log::error!("sending promise to user"); ri.send_promise(&reply, &dispatch_id); break; } diff --git a/ethexe/runtime/common/src/transitions.rs b/ethexe/runtime/common/src/transitions.rs index 5b0d85b4060..fb00898864d 100644 --- a/ethexe/runtime/common/src/transitions.rs +++ b/ethexe/runtime/common/src/transitions.rs @@ -23,12 +23,10 @@ use alloc::{ use anyhow::{Result, anyhow}; use core::num::NonZero; use ethexe_common::{ - HashOf, ProgramStates, Schedule, ScheduledTask, StateHashWithQueueSize, + ProgramStates, Schedule, ScheduledTask, StateHashWithQueueSize, gear::{Message, StateTransition, ValueClaim}, - injected::Promise, }; -use gear_core::rpc::ReplyInfo; -use gprimitives::{ActorId, CodeId, H256, MessageId}; +use gprimitives::{ActorId, CodeId, H256}; /// In-memory store for the state transitions /// that are going to be applied in the current block. @@ -46,11 +44,6 @@ pub struct InBlockTransitions { schedule: Schedule, modifications: BTreeMap, program_creations: BTreeMap, - - /// The set of injected messages to track replies for. - injected_messages: BTreeSet, - /// Replies for injected messages, in the order of processing. - injected_replies: Vec<(MessageId, ReplyInfo)>, } #[derive(Debug, Clone, Default)] @@ -58,22 +51,15 @@ pub struct FinalizedBlockTransitions { pub transitions: Vec, pub states: ProgramStates, pub schedule: Schedule, - pub promises: Vec, pub program_creations: Vec<(ActorId, CodeId)>, } impl InBlockTransitions { - pub fn new( - block_height: u32, - states: ProgramStates, - schedule: Schedule, - injected_messages: impl IntoIterator, - ) -> Self { + pub fn new(block_height: u32, states: ProgramStates, schedule: Schedule) -> Self { Self { block_height, states, schedule, - injected_messages: injected_messages.into_iter().collect(), ..Default::default() } } @@ -148,13 +134,6 @@ impl InBlockTransitions { &self.program_creations } - // TODO: remove this in current pull request - pub fn maybe_store_injected_reply(&mut self, message_id: MessageId, reply: ReplyInfo) { - if self.injected_messages.contains(&message_id) { - self.injected_replies.push((message_id, reply)); - } - } - pub fn modify_state( &mut self, actor_id: ActorId, @@ -216,20 +195,10 @@ impl InBlockTransitions { states, schedule, modifications, - injected_replies, program_creations, .. } = self; - let promises = injected_replies - .into_iter() - .map(|(message_id, reply)| { - // SAFETY: message_id for injected transaction is created from its hash bytes. - let tx_hash = unsafe { HashOf::new(message_id.into_bytes().into()) }; - Promise { tx_hash, reply } - }) - .collect(); - let mut transitions = Vec::with_capacity(modifications.len()); for (actor_id, modification) in modifications { @@ -256,7 +225,6 @@ impl InBlockTransitions { transitions, states, schedule, - promises, program_creations: program_creations.into_iter().collect(), } } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 57b39090b93..0534fd618cd 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -24,7 +24,9 @@ use alloy::{ use anyhow::{Context, Result, bail}; use async_trait::async_trait; use ethexe_blob_loader::{BlobLoader, BlobLoaderEvent, BlobLoaderService, ConsensusLayerConfig}; -use ethexe_common::{COMMITMENT_DELAY_LIMIT, gear::CodeState, network::VerifiedValidatorMessage}; +use ethexe_common::{ + COMMITMENT_DELAY_LIMIT, gear::CodeState, injected::Promise, network::VerifiedValidatorMessage, +}; use ethexe_compute::{ComputeConfig, ComputeEvent, ComputeService}; use ethexe_consensus::{ ConnectService, ConsensusEvent, ConsensusService, ValidatorConfig, ValidatorService, @@ -45,11 +47,15 @@ use ethexe_rpc::{RpcEvent, RpcServer}; use ethexe_service_utils::{OptionFuture as _, OptionStreamNext as _}; use futures::{StreamExt, stream::FuturesUnordered}; use gprimitives::{ActorId, CodeId, H256}; -use gsigner::secp256k1::{Address, PrivateKey, PublicKey, Signer}; +use gsigner::secp256k1::{Address, PrivateKey, PublicKey, Secp256k1SignerExt, Signer}; use std::{ - collections::{BTreeSet, HashMap}, num::NonZero, path::PathBuf, pin::Pin, sync::mpsc, time::Duration + collections::{BTreeSet, HashMap}, + num::NonZero, + path::PathBuf, + pin::Pin, + time::Duration, }; -use tokio::sync::oneshot; +use tokio::sync::{mpsc, oneshot}; pub mod config; @@ -67,6 +73,8 @@ pub enum Event { Prometheus(PrometheusEvent), Rpc(RpcEvent), Fetching(db_sync::HandleResult), + // TODO: i don't like this naming, rename in future + PromiseProcessed(Promise), } #[derive(Clone)] @@ -109,8 +117,11 @@ pub struct Service { prometheus: Option, rpc: Option, + /// Promises receiver from [`Processor`]. + promise_receiver: mpsc::UnboundedReceiver, + fast_sync: bool, - validator_address: Option

, + validator_pub_key: Option, #[cfg(test)] sender: tests::utils::TestingEventSender, @@ -268,14 +279,14 @@ impl Service { .await .with_context(|| "failed to query validators threshold")?; log::info!("🔒 Multisig threshold: {threshold} / {}", validators.len()); - let (promise_sender, _promise_receiver) = mpsc::channel(); + let (promise_sender, promise_receiver) = mpsc::unbounded_channel(); let processor = Processor::with_config( ProcessorConfig { chunk_size: config.node.chunk_processing_threads, }, db.clone(), - Some(promise_sender) + Some(promise_sender), ) .with_context(|| "failed to create processor")?; @@ -288,7 +299,6 @@ impl Service { let validator_pub_key = Self::get_config_public_key(config.node.validator, &signer) .with_context(|| "failed to get validator private key")?; - let validator_address = validator_pub_key.map(|key| key.to_address()); // TODO #4642: use validator session key let _validator_pub_key_session = @@ -392,8 +402,9 @@ impl Service { signer, prometheus, rpc, + promise_receiver, fast_sync, - validator_address, + validator_pub_key, #[cfg(test)] sender: unreachable!(), }) @@ -419,9 +430,10 @@ impl Service { network: Option, prometheus: Option, rpc: Option, + promise_receiver: mpsc::UnboundedReceiver, sender: tests::utils::TestingEventSender, fast_sync: bool, - validator_address: Option
, + validator_pub_key: Option, ) -> Self { Self { db, @@ -433,9 +445,10 @@ impl Service { network, prometheus, rpc, + promise_receiver, sender, fast_sync, - validator_address, + validator_pub_key, } } @@ -457,14 +470,16 @@ impl Service { mut blob_loader, mut compute, mut consensus, - signer: _signer, + signer, mut prometheus, rpc, + mut promise_receiver, fast_sync: _, - validator_address, + validator_pub_key, #[cfg(test)] sender, } = self; + let validator_address = validator_pub_key.map(|key| key.to_address()); let (mut rpc_handle, mut rpc) = if let Some(rpc) = rpc { log::info!("🌐 Rpc server starting at: {}", rpc.port()); @@ -496,6 +511,16 @@ impl Service { event = blob_loader.select_next_some() => event?.into(), event = prometheus.maybe_next_some() => event.into(), event = rpc.maybe_next_some() => event.into(), + maybe_promise = promise_receiver.recv() => { + // TODO: think about re-design this behaviour + match maybe_promise { + Some(promise) => promise.into(), + None => { + tracing::error!("Stopping service: the promises receiver from processor is closed"); + break Ok(()); + } + } + } fetching_result = network_fetcher.maybe_next_some() => Event::Fetching(fetching_result), _ = rpc_handle.as_mut().maybe() => { log::info!("`RPCWorker` has terminated, shutting down..."); @@ -542,8 +567,8 @@ impl Service { ComputeEvent::RequestLoadCodes(codes) => { blob_loader.load_codes(codes)?; } - ComputeEvent::AnnounceComputed(computed_data) => { - consensus.receive_computed_announce(computed_data)? + ComputeEvent::AnnounceComputed(announce_hash) => { + consensus.receive_computed_announce(announce_hash)? } ComputeEvent::BlockPrepared(block_hash) => { consensus.receive_prepared_block(block_hash)? @@ -700,22 +725,24 @@ impl Service { ConsensusEvent::AnnounceAccepted(_) | ConsensusEvent::AnnounceRejected(_) => { // TODO #4940: consider to publish network message } - ConsensusEvent::Promises(promises) => { + }, + Event::PromiseProcessed(promise) => { + if let Some(pub_key) = validator_pub_key { + let signed_promise = signer.signed_message(pub_key, promise, None)?; + if rpc.is_none() && network.is_none() { panic!("Promise without network or rpc"); } if let Some(rpc) = &rpc { - rpc.provide_promises(promises.clone()); + rpc.provide_promise(signed_promise.clone()); } if let Some(network) = &mut network { - for promise in promises { - network.publish_promise(promise); - } + network.publish_promise(signed_promise); } } - }, + } Event::Fetching(result) => { let Some(network) = network.as_mut() else { unreachable!("Fetching event is impossible without network service"); diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index c6f26879271..4ac4f671de9 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -1545,13 +1545,14 @@ async fn send_injected_tx() { }; // Send request - log::info!("Sending tx pool request to node-1"); - let _r = node1 + log::info!("Sending transaction to node-1"); + let acceptance = node1 .rpc_http_client() .unwrap() .send_transaction(tx_for_node1.clone()) .await .expect("rpc server is set"); + assert_eq!(acceptance, InjectedTransactionAcceptance::Accept); // Tx executable validation takes time, so wait for event. node1 @@ -2403,7 +2404,10 @@ async fn injected_tx_fungible_token() { .validator(env.validators[0]), ); node.start_service().await; - let rpc_client = node.rpc_http_client().expect("RPC client provide by node"); + let rpc_client = node + .rpc_ws_client() + .await + .expect("RPC client provide by node"); // 1. Create Fungible token config let token_config = demo_fungible_token::InitConfig { @@ -2475,11 +2479,10 @@ async fn injected_tx_fungible_token() { .unwrap(), }; - let acceptance = rpc_client - .send_transaction(rpc_tx) + let mut subscription = rpc_client + .send_transaction_and_watch(rpc_tx) .await .expect("successfully send transaction to RPC"); - assert!(matches!(acceptance, InjectedTransactionAcceptance::Accept)); let expected_event = demo_fungible_token::FTEvent::Transfer { from: ActorId::new([0u8; 32]), @@ -2490,10 +2493,7 @@ async fn injected_tx_fungible_token() { // Listen for inclusion and check the expected payload. node.events() .find(|event| { - if let TestingEvent::Consensus(ConsensusEvent::Promises(promises)) = event - && !promises.is_empty() - { - let promise = promises.first().unwrap().data(); + if let TestingEvent::PromiseProcessed(promise) = event { assert_eq!(promise.reply.payload, expected_event.encode()); assert_eq!( promise.reply.code, @@ -2509,6 +2509,22 @@ async fn injected_tx_fungible_token() { .await; tracing::info!("✅ Tokens mint successfully"); + let subscription_promise = subscription + .next() + .await + .expect("subscription produce value") + .expect("no errors for correct injected transaction"); + assert_eq!(subscription_promise.data().tx_hash, mint_tx.to_hash()); + assert_eq!(subscription_promise.data().reply.value, 0); + assert_eq!( + subscription_promise.data().reply.code, + ReplyCode::Success(SuccessReplyReason::Manual) + ); + assert_eq!( + subscription_promise.into_data().reply.payload, + expected_event.encode() + ); + let db = node.db.clone(); node.events() .find(|event| { @@ -3363,3 +3379,5 @@ async fn catch_up_test_case(commitment_delay_limit: u32) { unreachable!(); } } + +// TODO: implement test checks that promises produced only by validator diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index 47dee1620f9..aee6bcd530e 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -76,13 +76,13 @@ use std::{ net::SocketAddr, num::NonZero, pin::Pin, - sync::{ - atomic::{AtomicUsize, Ordering}, - mpsc, - }, + sync::atomic::{AtomicUsize, Ordering}, time::Duration, }; -use tokio::{task, task::JoinHandle}; +use tokio::{ + sync::mpsc, + task::{self, JoinHandle}, +}; use tracing::Instrument; /// Max network services which can be created by one test environment. @@ -889,7 +889,7 @@ impl Node { "Service is already running" ); - let (promise_sender, _promise_receiver) = mpsc::channel(); + let (promise_sender, promise_receiver) = mpsc::unbounded_channel(); let processor = Processor::new(self.db.clone(), Some(promise_sender)).unwrap(); let compute = ComputeService::new(self.compute_config, self.db.clone(), processor); @@ -952,10 +952,7 @@ impl Node { } }; - let validator_address = self - .validator_config - .as_ref() - .map(|c| c.public_key.to_address()); + let validator_pub_key = self.validator_config.as_ref().map(|c| c.public_key); let (sender, receiver) = events::channel(self.db.clone()); @@ -995,9 +992,10 @@ impl Node { network, None, rpc, + promise_receiver, sender, self.fast_sync, - validator_address, + validator_pub_key, ); let name = self.name.clone(); diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index eb1bd74d8b7..314b349ee1f 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -26,7 +26,7 @@ use ethexe_common::{ db::*, events::BlockEvent, injected::{ - AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, + AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, Promise, SignedInjectedTransaction, SignedPromise, }, network::VerifiedValidatorMessage, @@ -144,6 +144,7 @@ pub enum TestingEvent { Prometheus(PrometheusEvent), Rpc(TestingRpcEvent), Fetching, + PromiseProcessed(Promise), } impl TestingEvent { @@ -157,6 +158,7 @@ impl TestingEvent { Event::Prometheus(event) => Self::Prometheus(event.clone()), Event::Rpc(event) => Self::Rpc(TestingRpcEvent::new(event)), Event::Fetching(_) => Self::Fetching, + Event::PromiseProcessed(promise) => Self::PromiseProcessed(promise.clone()), } } } @@ -277,8 +279,8 @@ impl TestingEventReceiver { let id = id.into(); log::info!("📗 waiting for announce computed: {id:?}"); self.find_announce(id, |event| { - if let TestingEvent::Compute(ComputeEvent::AnnounceComputed(computed_data)) = event { - Some(computed_data.announce_hash) + if let TestingEvent::Compute(ComputeEvent::AnnounceComputed(announce_hash)) = event { + Some(announce_hash) } else { None } From b182951796fa1ae1ddefeae7e122099946a08b44 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 25 Feb 2026 15:33:40 +0300 Subject: [PATCH 14/59] compute service produce event with promise --- ethexe/compute/src/lib.rs | 7 ++- ethexe/compute/src/service.rs | 30 ++++++++--- ethexe/compute/src/tests.rs | 1 + ethexe/service/src/lib.rs | 65 +++++++++--------------- ethexe/service/src/tests/mod.rs | 4 +- ethexe/service/src/tests/utils/env.rs | 8 ++- ethexe/service/src/tests/utils/events.rs | 4 +- 7 files changed, 64 insertions(+), 55 deletions(-) diff --git a/ethexe/compute/src/lib.rs b/ethexe/compute/src/lib.rs index 110146260ef..911a89c40bd 100644 --- a/ethexe/compute/src/lib.rs +++ b/ethexe/compute/src/lib.rs @@ -16,7 +16,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use ethexe_common::{Announce, CodeAndIdUnchecked, HashOf}; +use ethexe_common::{Announce, CodeAndIdUnchecked, HashOf, injected::Promise}; use ethexe_processor::{ExecutableData, ProcessedCodeInfo, Processor, ProcessorError}; use ethexe_runtime_common::FinalizedBlockTransitions; use gprimitives::{CodeId, H256}; @@ -38,12 +38,13 @@ pub struct BlockProcessed { pub block_hash: H256, } -#[derive(Debug, Clone, Eq, PartialEq, derive_more::Unwrap)] +#[derive(Debug, Clone, Eq, PartialEq, derive_more::Unwrap, derive_more::From)] pub enum ComputeEvent { RequestLoadCodes(HashSet), CodeProcessed(CodeId), BlockPrepared(H256), AnnounceComputed(HashOf), + Promise(Promise), } #[derive(thiserror::Error, Debug)] @@ -83,6 +84,8 @@ pub enum ComputeError { ProgramStatesNotFound(HashOf), #[error("Schedule not found for computed Announce {0:?}")] ScheduleNotFound(HashOf), + #[error("Promise sender dropped")] + PromiseSenderDropped, #[error(transparent)] Processor(#[from] ProcessorError), diff --git a/ethexe/compute/src/service.rs b/ethexe/compute/src/service.rs index d829e953f88..ac61068cfcd 100644 --- a/ethexe/compute/src/service.rs +++ b/ethexe/compute/src/service.rs @@ -17,12 +17,12 @@ // along with this program. If not, see . use crate::{ - ComputeEvent, ProcessorExt, Result, + ComputeError, ComputeEvent, ProcessorExt, Result, codes::CodesSubService, compute::{ComputeConfig, ComputeSubService}, prepare::PrepareSubService, }; -use ethexe_common::{Announce, CodeAndIdUnchecked}; +use ethexe_common::{Announce, CodeAndIdUnchecked, injected::Promise}; use ethexe_db::Database; use ethexe_processor::Processor; use futures::{Stream, stream::FusedStream}; @@ -31,20 +31,28 @@ use std::{ pin::Pin, task::{Context, Poll}, }; +use tokio::sync::mpsc; pub struct ComputeService { codes_sub_service: CodesSubService

, prepare_sub_service: PrepareSubService, compute_sub_service: ComputeSubService

, + promise_receiver: Option>, } impl ComputeService

{ // TODO #4550: consider to create Processor inside ComputeService - pub fn new(config: ComputeConfig, db: Database, processor: P) -> Self { + pub fn new( + config: ComputeConfig, + db: Database, + processor: P, + promise_receiver: Option>, + ) -> Self { Self { prepare_sub_service: PrepareSubService::new(db.clone()), compute_sub_service: ComputeSubService::new(config, db.clone(), processor.clone()), codes_sub_service: CodesSubService::new(db, processor), + promise_receiver, } } @@ -86,6 +94,16 @@ impl Stream for ComputeService

{ return Poll::Ready(Some(result.map(ComputeEvent::AnnounceComputed))); }; + if let Some(ref mut receiver) = self.promise_receiver + && let Poll::Ready(maybe_promise) = receiver.poll_recv(cx) + { + return Poll::Ready(Some( + maybe_promise + .map(Into::into) + .ok_or(ComputeError::PromiseSenderDropped), + )); + } + Poll::Pending } } @@ -125,7 +143,7 @@ mod tests { let db = DB::memory(); let processor = MockProcessor; let config = ComputeConfig::without_quarantine(); - let mut service = ComputeService::new(config, db.clone(), processor); + let mut service = ComputeService::new(config, db.clone(), processor, None); let chain = BlockChain::mock(1).setup(&db); let block = chain.blocks[1].to_simple().next_block().setup(&db); @@ -150,7 +168,7 @@ mod tests { let processor = MockProcessor; let config = ComputeConfig::without_quarantine(); - let mut service = ComputeService::new(config, db.clone(), processor); + let mut service = ComputeService::new(config, db.clone(), processor, None); let chain = BlockChain::mock(1).setup(&db); let block = chain.blocks[1].to_simple().next_block().setup(&db); @@ -185,7 +203,7 @@ mod tests { let db = DB::memory(); let processor = MockProcessor; let config = ComputeConfig::without_quarantine(); - let mut service = ComputeService::new(config, db.clone(), processor); + let mut service = ComputeService::new(config, db.clone(), processor, None); // Create test code let code = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]; // Simple WASM header diff --git a/ethexe/compute/src/tests.rs b/ethexe/compute/src/tests.rs index 3d36eed20ef..5170651bd50 100644 --- a/ethexe/compute/src/tests.rs +++ b/ethexe/compute/src/tests.rs @@ -172,6 +172,7 @@ impl TestEnv { config, db.clone(), Processor::new(db.clone(), None).unwrap(), + None, ); TestEnv { db, compute, chain } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 29e79f5f3bd..ff1ef133ff0 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -24,9 +24,7 @@ use alloy::{ use anyhow::{Context, Result, bail}; use async_trait::async_trait; use ethexe_blob_loader::{BlobLoader, BlobLoaderEvent, BlobLoaderService, ConsensusLayerConfig}; -use ethexe_common::{ - COMMITMENT_DELAY_LIMIT, gear::CodeState, injected::Promise, network::VerifiedValidatorMessage, -}; +use ethexe_common::{COMMITMENT_DELAY_LIMIT, gear::CodeState, network::VerifiedValidatorMessage}; use ethexe_compute::{ComputeConfig, ComputeEvent, ComputeService}; use ethexe_consensus::{ ConnectService, ConsensusEvent, ConsensusService, ValidatorConfig, ValidatorService, @@ -72,8 +70,6 @@ pub enum Event { BlobLoader(BlobLoaderEvent), Rpc(RpcEvent), Fetching(db_sync::HandleResult), - // TODO: i don't like this naming, rename in future - PromiseProcessed(Promise), } #[derive(Clone)] @@ -116,9 +112,6 @@ pub struct Service { prometheus: Option, rpc: Option, - /// Promises receiver from [`Processor`]. - promise_receiver: mpsc::UnboundedReceiver, - fast_sync: bool, validator_pub_key: Option, @@ -386,7 +379,12 @@ impl Service { .map(|config| RpcServer::new(config.clone(), db.clone())); let compute_config = ComputeConfig::new(config.node.canonical_quarantine); - let compute = ComputeService::new(compute_config, db.clone(), processor); + let compute = ComputeService::new( + compute_config, + db.clone(), + processor, + Some(promise_receiver), + ); let fast_sync = config.node.fast_sync; @@ -401,7 +399,6 @@ impl Service { signer, prometheus, rpc, - promise_receiver, fast_sync, validator_pub_key, #[cfg(test)] @@ -429,7 +426,6 @@ impl Service { network: Option, prometheus: Option, rpc: Option, - promise_receiver: mpsc::UnboundedReceiver, sender: tests::utils::TestingEventSender, fast_sync: bool, validator_pub_key: Option, @@ -444,7 +440,6 @@ impl Service { network, prometheus, rpc, - promise_receiver, sender, fast_sync, validator_pub_key, @@ -472,7 +467,6 @@ impl Service { signer, mut prometheus, rpc, - mut promise_receiver, fast_sync: _, validator_pub_key, #[cfg(test)] @@ -509,16 +503,6 @@ impl Service { event = observer.select_next_some() => event?.into(), event = blob_loader.select_next_some() => event?.into(), event = rpc.maybe_next_some() => event.into(), - maybe_promise = promise_receiver.recv() => { - // TODO: think about re-design this behaviour - match maybe_promise { - Some(promise) => promise.into(), - None => { - tracing::error!("Stopping service: the promises receiver from processor is closed"); - break Ok(()); - } - } - } fetching_result = network_fetcher.maybe_next_some() => Event::Fetching(fetching_result), _ = prometheus.maybe_next_some() => { anyhow::bail!("Prometheus server handle has terminated"); @@ -576,6 +560,24 @@ impl Service { ComputeEvent::CodeProcessed(_) => { // Nothing } + ComputeEvent::Promise(promise) => { + // TODO: think to send the promise to consensus service + if let Some(pub_key) = validator_pub_key { + let signed_promise = signer.signed_message(pub_key, promise, None)?; + + if rpc.is_none() && network.is_none() { + panic!("Promise without network or rpc"); + } + + if let Some(rpc) = &rpc { + rpc.provide_promise(signed_promise.clone()); + } + + if let Some(network) = &mut network { + network.publish_promise(signed_promise); + } + } + } }, Event::Network(event) => { let Some(_) = network.as_mut() else { @@ -692,23 +694,6 @@ impl Service { // TODO #4940: consider to publish network message } }, - Event::PromiseProcessed(promise) => { - if let Some(pub_key) = validator_pub_key { - let signed_promise = signer.signed_message(pub_key, promise, None)?; - - if rpc.is_none() && network.is_none() { - panic!("Promise without network or rpc"); - } - - if let Some(rpc) = &rpc { - rpc.provide_promise(signed_promise.clone()); - } - - if let Some(network) = &mut network { - network.publish_promise(signed_promise); - } - } - } Event::Fetching(result) => { let Some(network) = network.as_mut() else { unreachable!("Fetching event is impossible without network service"); diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 221576c1fa5..7a37f563d29 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -49,7 +49,7 @@ use ethexe_common::{ mock::*, network::ValidatorMessage, }; -use ethexe_compute::ComputeConfig; +use ethexe_compute::{ComputeConfig, ComputeEvent}; use ethexe_consensus::{BatchCommitter, ConsensusEvent}; use ethexe_db::{Database, verifier::IntegrityVerifier}; use ethexe_ethereum::{TryGetReceipt, deploy::ContractsDeploymentParams, router::Router}; @@ -2493,7 +2493,7 @@ async fn injected_tx_fungible_token() { // Listen for inclusion and check the expected payload. node.events() .find(|event| { - if let TestingEvent::PromiseProcessed(promise) = event { + if let TestingEvent::Compute(ComputeEvent::Promise(promise)) = event { assert_eq!(promise.reply.payload, expected_event.encode()); assert_eq!( promise.reply.code, diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index aee6bcd530e..66ead2a2b77 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -891,7 +891,12 @@ impl Node { let (promise_sender, promise_receiver) = mpsc::unbounded_channel(); let processor = Processor::new(self.db.clone(), Some(promise_sender)).unwrap(); - let compute = ComputeService::new(self.compute_config, self.db.clone(), processor); + let compute = ComputeService::new( + self.compute_config, + self.db.clone(), + processor, + Some(promise_receiver), + ); let observer = ObserverService::new(&self.eth_cfg, u32::MAX, self.db.clone()) .await @@ -992,7 +997,6 @@ impl Node { network, None, rpc, - promise_receiver, sender, self.fast_sync, validator_pub_key, diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index 3f4f2c1510e..2d7dbc93c4b 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -26,7 +26,7 @@ use ethexe_common::{ db::*, events::BlockEvent, injected::{ - AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, Promise, + AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, SignedInjectedTransaction, SignedPromise, }, network::VerifiedValidatorMessage, @@ -142,7 +142,6 @@ pub enum TestingEvent { BlobLoader(BlobLoaderEvent), Rpc(TestingRpcEvent), Fetching, - PromiseProcessed(Promise), } impl TestingEvent { @@ -155,7 +154,6 @@ impl TestingEvent { Event::BlobLoader(event) => Self::BlobLoader(event.clone()), Event::Rpc(event) => Self::Rpc(TestingRpcEvent::new(event)), Event::Fetching(_) => Self::Fetching, - Event::PromiseProcessed(promise) => Self::PromiseProcessed(promise.clone()), } } } From 3b9affb5922af7d917171ae607d3330f8bc1bfa5 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 25 Feb 2026 20:17:28 +0300 Subject: [PATCH 15/59] only producer provides promises from compute service --- ethexe/cli/src/commands/check.rs | 10 +++-- ethexe/compute/src/compute.rs | 37 +++++++++++++++---- ethexe/compute/src/service.rs | 8 ++-- ethexe/compute/src/tests.rs | 2 +- ethexe/consensus/src/connect/mod.rs | 2 +- ethexe/consensus/src/lib.rs | 3 +- ethexe/consensus/src/validator/producer.rs | 5 ++- ethexe/consensus/src/validator/subordinate.rs | 15 ++++---- ethexe/processor/src/handling/overlaid.rs | 1 + ethexe/processor/src/handling/run.rs | 17 +++++++++ ethexe/processor/src/lib.rs | 7 +++- ethexe/processor/src/tests.rs | 28 +++++++------- ethexe/runtime/common/src/lib.rs | 5 ++- ethexe/service/src/lib.rs | 4 +- 14 files changed, 99 insertions(+), 45 deletions(-) diff --git a/ethexe/cli/src/commands/check.rs b/ethexe/cli/src/commands/check.rs index 14d53419b1b..a02af5a766e 100644 --- a/ethexe/cli/src/commands/check.rs +++ b/ethexe/cli/src/commands/check.rs @@ -239,9 +239,13 @@ impl Checker { let announce_parent_hash = announce.parent; let mut processor = processor.clone().overlaid(); - let executable = - ethexe_compute::prepare_executable_for_announce(db, announce, canonical_quarantine) - .context("Unable to preparing announce data for execution")?; + let executable = ethexe_compute::prepare_executable_for_announce( + db, + announce, + false, + canonical_quarantine, + ) + .context("Unable to preparing announce data for execution")?; let res = processor .as_mut() .process_programs(executable) diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 5585ebba82f..090eced4493 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -79,7 +79,7 @@ pub struct ComputeSubService { config: ComputeConfig, metrics: Metrics, - input: VecDeque, + input: VecDeque<(Announce, bool)>, computation: Option, } @@ -95,8 +95,12 @@ impl ComputeSubService

{ } } - pub fn receive_announce_to_compute(&mut self, announce: Announce) { - self.input.push_back(announce); + pub fn receive_announce_to_compute( + &mut self, + announce: Announce, + should_produce_promises: bool, + ) { + self.input.push_back((announce, should_produce_promises)); } async fn compute( @@ -104,6 +108,7 @@ impl ComputeSubService

{ config: ComputeConfig, mut processor: P, announce: Announce, + should_produce_promises: bool, ) -> Result> { let announce_hash = announce.to_hash(); let block_hash = announce.block_hash; @@ -135,7 +140,15 @@ impl ComputeSubService

{ } for (announce_hash, announce) in announces_chain { - Self::compute_one(&db, &mut processor, announce_hash, announce, config).await?; + Self::compute_one( + &db, + &mut processor, + config, + announce_hash, + announce, + should_produce_promises, + ) + .await?; } Ok(announce_hash) @@ -144,12 +157,17 @@ impl ComputeSubService

{ async fn compute_one( db: &Database, processor: &mut P, + config: ComputeConfig, announce_hash: HashOf, announce: Announce, - config: ComputeConfig, + should_produce_promises: bool, ) -> Result> { - let executable = - prepare_executable_for_announce(db, announce, config.canonical_quarantine())?; + let executable = prepare_executable_for_announce( + db, + announce, + should_produce_promises, + config.canonical_quarantine(), + )?; let processing_result = processor.process_announce(executable).await?; let FinalizedBlockTransitions { @@ -186,7 +204,7 @@ impl SubService for ComputeSubService

{ fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll> { if self.computation.is_none() - && let Some(announce) = self.input.pop_front() + && let Some((announce, should_produce_promises)) = self.input.pop_front() { self.computation = Some(future_timing::timed( Self::compute( @@ -194,6 +212,7 @@ impl SubService for ComputeSubService

{ self.config, self.processor.clone(), announce, + should_produce_promises, ) .boxed(), )); @@ -217,6 +236,7 @@ impl SubService for ComputeSubService

{ pub fn prepare_executable_for_announce( db: &Database, announce: Announce, + should_produce_promises: bool, canonical_quarantine: u8, ) -> Result { let block_hash = announce.block_hash; @@ -249,6 +269,7 @@ pub fn prepare_executable_for_announce( .collect(), gas_allowance: announce.gas_allowance, events, + should_produce_promises, }) } diff --git a/ethexe/compute/src/service.rs b/ethexe/compute/src/service.rs index ac61068cfcd..164ae7f6c1c 100644 --- a/ethexe/compute/src/service.rs +++ b/ethexe/compute/src/service.rs @@ -64,9 +64,9 @@ impl ComputeService

{ self.prepare_sub_service.receive_block_to_prepare(block); } - pub fn compute_announce(&mut self, announce: Announce) { + pub fn compute_announce(&mut self, announce: Announce, should_produce_promises: bool) { self.compute_sub_service - .receive_announce_to_compute(announce); + .receive_announce_to_compute(announce, should_produce_promises); } } @@ -100,7 +100,7 @@ impl Stream for ComputeService

{ return Poll::Ready(Some( maybe_promise .map(Into::into) - .ok_or(ComputeError::PromiseSenderDropped), + .ok_or_else(|| ComputeError::PromiseSenderDropped), )); } @@ -185,7 +185,7 @@ mod tests { injected_transactions: vec![], }; let announce_hash = announce.to_hash(); - service.compute_announce(announce); + service.compute_announce(announce, false); // Poll service to process the block let event = service.next().await.unwrap().unwrap(); diff --git a/ethexe/compute/src/tests.rs b/ethexe/compute/src/tests.rs index 5170651bd50..e2ea5ffc422 100644 --- a/ethexe/compute/src/tests.rs +++ b/ethexe/compute/src/tests.rs @@ -224,7 +224,7 @@ impl TestEnv { async fn compute_and_assert_announce(&mut self, announce: Announce) { let announce_hash = announce.to_hash(); - self.compute.compute_announce(announce.clone()); + self.compute.compute_announce(announce.clone(), false); let event = self .compute diff --git a/ethexe/consensus/src/connect/mod.rs b/ethexe/consensus/src/connect/mod.rs index fc672c8be64..ba9f17f7c54 100644 --- a/ethexe/consensus/src/connect/mod.rs +++ b/ethexe/consensus/src/connect/mod.rs @@ -162,7 +162,7 @@ impl ConnectService { self.output .push_back(ConsensusEvent::AnnounceAccepted(announce_hash)); self.output - .push_back(ConsensusEvent::ComputeAnnounce(announce)); + .push_back(ConsensusEvent::ComputeAnnounce(announce, false)); } } diff --git a/ethexe/consensus/src/lib.rs b/ethexe/consensus/src/lib.rs index b204b196537..3ca2c586cae 100644 --- a/ethexe/consensus/src/lib.rs +++ b/ethexe/consensus/src/lib.rs @@ -109,8 +109,7 @@ pub enum ConsensusEvent { /// Announce from producer was rejected AnnounceRejected(HashOf), /// Outer service have to compute announce - #[from] - ComputeAnnounce(Announce), + ComputeAnnounce(Announce, bool), /// Outer service have to publish signed message #[from] PublishMessage(SignedValidatorMessage), diff --git a/ethexe/consensus/src/validator/producer.rs b/ethexe/consensus/src/validator/producer.rs index 7a822cb1a65..8293c80d3f6 100644 --- a/ethexe/consensus/src/validator/producer.rs +++ b/ethexe/consensus/src/validator/producer.rs @@ -217,7 +217,8 @@ impl Producer { self.state = State::WaitingAnnounceComputed(announce_hash); self.ctx .output(ConsensusEvent::PublishMessage(message.into())); - self.ctx.output(ConsensusEvent::ComputeAnnounce(announce)); + self.ctx + .output(ConsensusEvent::ComputeAnnounce(announce, true)); Ok(self.into()) } @@ -465,7 +466,7 @@ mod tests { assert!(state.is_producer(), "Expected producer state, got {state}"); assert!(event.is_compute_announce()); - Ok((state, event.unwrap_compute_announce().to_hash())) + Ok((state, event.unwrap_compute_announce().0.to_hash())) } } } diff --git a/ethexe/consensus/src/validator/subordinate.rs b/ethexe/consensus/src/validator/subordinate.rs index 696a981b929..95edb19e670 100644 --- a/ethexe/consensus/src/validator/subordinate.rs +++ b/ethexe/consensus/src/validator/subordinate.rs @@ -173,7 +173,8 @@ impl Subordinate { AnnounceStatus::Accepted(announce_hash) => { self.ctx .output(ConsensusEvent::AnnounceAccepted(announce_hash)); - self.ctx.output(ConsensusEvent::ComputeAnnounce(announce)); + self.ctx + .output(ConsensusEvent::ComputeAnnounce(announce, false)); self.state = State::WaitingAnnounceComputed { announce_hash }; Ok(self.into()) @@ -234,7 +235,7 @@ mod tests { s.context().output, vec![ ConsensusEvent::AnnounceAccepted(announce1.data().to_hash()), - ConsensusEvent::ComputeAnnounce(announce1.data().clone()) + ConsensusEvent::ComputeAnnounce(announce1.data().clone(), false) ] ); // announce2 must stay in pending events, because it's not from current producer. @@ -294,7 +295,7 @@ mod tests { s.context().output, vec![ ConsensusEvent::AnnounceAccepted(announce.data().to_hash()), - announce.data().clone().into() + ConsensusEvent::ComputeAnnounce(announce.data().clone(), false) ] ); assert_eq!(s.context().pending_events.len(), MAX_PENDING_EVENTS); @@ -323,7 +324,7 @@ mod tests { s.context().output, vec![ ConsensusEvent::AnnounceAccepted(announce.data().to_hash()), - announce.data().clone().into() + ConsensusEvent::ComputeAnnounce(announce.data().clone(), false) ] ); @@ -336,7 +337,7 @@ mod tests { s.context().output, vec![ ConsensusEvent::AnnounceAccepted(announce.data().to_hash()), - ConsensusEvent::ComputeAnnounce(announce.data().clone()) + ConsensusEvent::ComputeAnnounce(announce.data().clone(), false) ] ); } @@ -365,7 +366,7 @@ mod tests { s.context().output, vec![ ConsensusEvent::AnnounceAccepted(announce.data().to_hash()), - announce.data().clone().into() + ConsensusEvent::ComputeAnnounce(announce.data().clone(), false) ] ); @@ -400,7 +401,7 @@ mod tests { s.context().output, vec![ ConsensusEvent::AnnounceAccepted(producer_announce.data().to_hash()), - producer_announce.data().clone().into() + ConsensusEvent::ComputeAnnounce(producer_announce.data().clone(), false) ] ); assert_eq!(s.context().pending_events, vec![alice_announce.into()]); diff --git a/ethexe/processor/src/handling/overlaid.rs b/ethexe/processor/src/handling/overlaid.rs index ab8d60571ac..30c2a63f3c3 100644 --- a/ethexe/processor/src/handling/overlaid.rs +++ b/ethexe/processor/src/handling/overlaid.rs @@ -86,6 +86,7 @@ impl OverlaidRunContext { gas_allowance, chunk_size, block_header, + false, ), base_program, nullified_queue_programs: [base_program].into_iter().collect(), diff --git a/ethexe/processor/src/handling/run.rs b/ethexe/processor/src/handling/run.rs index c8a5cb38c29..128e8f21726 100644 --- a/ethexe/processor/src/handling/run.rs +++ b/ethexe/processor/src/handling/run.rs @@ -254,6 +254,11 @@ pub(super) trait RunContext { false } + // TODO: add docs + fn should_produce_promises(&self) -> bool { + false + } + /// Checks whether the run must be stopped early without executing the rest chunks. /// /// In common execution, the run is never stopped early. @@ -271,6 +276,7 @@ pub(crate) struct CommonRunContext { pub(crate) gas_allowance_counter: GasAllowanceCounter, pub(crate) chunk_size: usize, pub(crate) block_header: BlockHeader, + pub(crate) should_produce_promises: bool, } impl CommonRunContext { @@ -281,6 +287,7 @@ impl CommonRunContext { gas_allowance: u64, chunk_size: usize, block_header: BlockHeader, + should_produce_promises: bool, ) -> Self { CommonRunContext { db, @@ -289,6 +296,7 @@ impl CommonRunContext { gas_allowance_counter: GasAllowanceCounter::new(gas_allowance), chunk_size, block_header, + should_produce_promises, } } @@ -352,6 +360,10 @@ impl RunContext for CommonRunContext { states(&self.transitions, processing_queue_type) } + fn should_produce_promises(&self) -> bool { + self.should_produce_promises + } + fn chunk_size(&self) -> usize { self.chunk_size } @@ -533,6 +545,7 @@ mod chunk_execution_spawn { executor: InstanceWrapper, db: Box, gas_allowance_for_chunk: u64, + should_produce_promises: bool, } let (db, _, gas_allowance_counter) = ctx.borrow_inner(); @@ -557,6 +570,7 @@ mod chunk_execution_spawn { executor, db: db.clone_boxed(), gas_allowance_for_chunk, + should_produce_promises: ctx.should_produce_promises(), }) }) .collect::>>()?; @@ -579,6 +593,7 @@ mod chunk_execution_spawn { mut executor, db, gas_allowance_for_chunk, + should_produce_promises, }| { let (jn, new_state_hash, gas_spent) = executor .run( @@ -593,6 +608,7 @@ mod chunk_execution_spawn { gas_allowance_for_chunk, ), block_info, + should_produce_promises, }, promise_sender.clone(), ) @@ -757,6 +773,7 @@ mod tests { gas_allowance_counter: GasAllowanceCounter::new(1_000_000), chunk_size: CHUNK_PROCESSING_THREADS, block_header: BlockHeader::dummy(3), + should_produce_promises: false, }; let chunks = chunks_splitting::prepare_execution_chunks(&mut ctx, MessageType::Canonical); diff --git a/ethexe/processor/src/lib.rs b/ethexe/processor/src/lib.rs index e7e43416762..210c98a40c1 100644 --- a/ethexe/processor/src/lib.rs +++ b/ethexe/processor/src/lib.rs @@ -196,6 +196,7 @@ impl Processor { injected_transactions, gas_allowance, events, + should_produce_promises, } = executable; let mut transitions = @@ -206,7 +207,7 @@ impl Processor { if let Some(gas_allowance) = gas_allowance { transitions = self - .process_queues(transitions, block, gas_allowance) + .process_queues(transitions, block, gas_allowance, should_produce_promises) .await?; } transitions = self.process_tasks(transitions); @@ -247,6 +248,7 @@ impl Processor { transitions: InBlockTransitions, block: SimpleBlockData, gas_allowance: u64, + should_produce_promises: bool, ) -> Result { CommonRunContext::new( self.db.clone(), @@ -255,6 +257,7 @@ impl Processor { gas_allowance, self.config.chunk_size, block.header, + should_produce_promises, ) .run(self.promise_sender.clone()) .await @@ -306,6 +309,7 @@ pub struct ExecutableData { pub injected_transactions: Vec>, pub gas_allowance: Option, pub events: Vec, + pub should_produce_promises: bool, } #[cfg(test)] @@ -318,6 +322,7 @@ impl Default for ExecutableData { injected_transactions: vec![], gas_allowance: Some(ethexe_common::DEFAULT_BLOCK_GAS_LIMIT), events: vec![], + should_produce_promises: false, } } } diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index f2a2e8398fb..6bfc4cf067d 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -332,7 +332,7 @@ async fn ping_pong() { .expect("failed to send message"); let to_users = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) .await .unwrap() .current_messages(); @@ -446,7 +446,7 @@ async fn async_and_ping() { .expect("failed to send message"); let transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) .await .unwrap(); @@ -542,7 +542,7 @@ async fn many_waits() { handler.transitions = processor.process_tasks(handler.transitions); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) .await .unwrap(); assert_eq!( @@ -567,7 +567,7 @@ async fn many_waits() { .expect("failed to send message"); } handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) .await .unwrap(); assert_eq!( @@ -589,7 +589,7 @@ async fn many_waits() { let transitions = InBlockTransitions::new(wake_block.header.height, states, schedule); let transitions = processor.process_tasks(transitions); let transitions = processor - .process_queues(transitions, wake_block, DEFAULT_BLOCK_GAS_LIMIT) + .process_queues(transitions, wake_block, DEFAULT_BLOCK_GAS_LIMIT, false) .await .unwrap(); @@ -896,7 +896,7 @@ async fn injected_ping_pong() { .expect("failed to send message"); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) .await .unwrap(); @@ -920,7 +920,7 @@ async fn injected_ping_pong() { .expect("failed to send message"); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, true) .await .unwrap(); @@ -1004,7 +1004,7 @@ async fn injected_prioritized_over_canonical() { .expect("failed to send message"); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) .await .unwrap(); @@ -1035,7 +1035,7 @@ async fn injected_prioritized_over_canonical() { } let transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, true) .await .unwrap(); @@ -1113,7 +1113,7 @@ async fn executable_balance_charged() { .expect("failed to send message"); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) .await .unwrap(); @@ -1201,7 +1201,7 @@ async fn executable_balance_injected_panic_not_charged() { ) .unwrap(); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) .await .unwrap(); let init_balance = handler.program_state(actor_id).executable_balance; @@ -1213,7 +1213,7 @@ async fn executable_balance_injected_panic_not_charged() { .handle_injected_transaction(user_id, panic_tx.clone()) .unwrap(); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, true) .await .unwrap(); @@ -1256,7 +1256,7 @@ async fn executable_balance_injected_panic_not_charged() { ) .expect("failed to send message"); let transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) .await .unwrap(); @@ -1323,7 +1323,7 @@ async fn insufficient_executable_balance_still_charged() { .expect("failed to send message"); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) .await .unwrap(); diff --git a/ethexe/runtime/common/src/lib.rs b/ethexe/runtime/common/src/lib.rs index fda22458ebb..160ed96b582 100644 --- a/ethexe/runtime/common/src/lib.rs +++ b/ethexe/runtime/common/src/lib.rs @@ -74,6 +74,9 @@ pub struct ProcessQueueContext { pub code_metadata: CodeMetadata, pub gas_allowance: GasAllowanceCounter, pub block_info: BlockInfo, + // TODO: fix the naming + /// Wether should compute service produce promises + pub should_produce_promises: bool, } pub trait RuntimeInterface: Storage { @@ -227,7 +230,7 @@ where stop_processing: false, }; - if is_promise_required { + if ctx.should_produce_promises && is_promise_required { for note in journal.iter() { if let JournalNote::SendDispatch { message_id, diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index ff1ef133ff0..133d4e05148 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -669,7 +669,9 @@ impl Service { } } Event::Consensus(event) => match event { - ConsensusEvent::ComputeAnnounce(announce) => compute.compute_announce(announce), + ConsensusEvent::ComputeAnnounce(announce, should_produce_promises) => { + compute.compute_announce(announce, should_produce_promises) + } ConsensusEvent::PublishMessage(message) => { let Some(network) = network.as_mut() else { continue; From bd3f009ec26936fd0a41b0c12f9dbcd41769a431 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Thu, 26 Feb 2026 10:17:46 +0300 Subject: [PATCH 16/59] small refactoring --- ethexe/compute/src/codes.rs | 94 ++++++++++---------- ethexe/processor/src/handling/overlaid.rs | 13 +-- ethexe/processor/src/handling/run.rs | 37 ++++---- ethexe/processor/src/host/api/promise.rs | 23 ++--- ethexe/processor/src/lib.rs | 10 ++- ethexe/runtime/common/src/lib.rs | 17 ++-- ethexe/runtime/src/wasm/interface/promise.rs | 26 +++--- ethexe/runtime/src/wasm/storage.rs | 10 +-- 8 files changed, 118 insertions(+), 112 deletions(-) diff --git a/ethexe/compute/src/codes.rs b/ethexe/compute/src/codes.rs index 84797c6f2f0..d7e2d4ad77c 100644 --- a/ethexe/compute/src/codes.rs +++ b/ethexe/compute/src/codes.rs @@ -122,50 +122,50 @@ impl SubService for CodesSubService

{ } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::tests::*; -// use ethexe_common::CodeAndId; -// use gear_core::code::{InstantiatedSectionSizes, InstrumentedCode}; - -// #[tokio::test] -// #[ntest::timeout(3000)] -// async fn process_code() { -// let db = Database::memory(); -// let mut service = CodesSubService::new(db.clone(), MockProcessor); - -// let code_and_id = CodeAndId::new(vec![1, 2, 3, 4]); - -// service.receive_code_to_process(code_and_id.clone().into_unchecked()); -// assert_eq!(service.next().await.unwrap(), code_and_id.code_id()); -// } - -// #[tokio::test] -// #[ntest::timeout(3000)] -// async fn process_already_validated_code() { -// let db = Database::memory(); -// let mut service = CodesSubService::new(db.clone(), MockProcessor); - -// let code_and_id = CodeAndId::new(vec![1, 2, 3, 4]); -// let code_id = code_and_id.code_id(); -// db.set_code_valid(code_id, true); -// db.set_original_code(code_and_id.code()); -// db.set_instrumented_code( -// ethexe_runtime_common::VERSION, -// code_id, -// InstrumentedCode::new( -// vec![5, 6, 7, 8], -// InstantiatedSectionSizes::new(1, 1, 1, 1, 1, 1), -// ), -// ); -// service.receive_code_to_process(code_and_id.into_unchecked()); -// assert_eq!(service.next().await.unwrap(), code_id); - -// let code_and_id = CodeAndId::new(vec![100, 101, 102, 103]); -// let code_id = code_and_id.code_id(); -// db.set_code_valid(code_id, false); -// service.receive_code_to_process(code_and_id.into_unchecked()); -// assert_eq!(service.next().await.unwrap(), code_id); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + use ethexe_common::CodeAndId; + use gear_core::code::{InstantiatedSectionSizes, InstrumentedCode}; + + #[tokio::test] + #[ntest::timeout(3000)] + async fn process_code() { + let db = Database::memory(); + let mut service = CodesSubService::new(db.clone(), MockProcessor); + + let code_and_id = CodeAndId::new(vec![1, 2, 3, 4]); + + service.receive_code_to_process(code_and_id.clone().into_unchecked()); + assert_eq!(service.next().await.unwrap(), code_and_id.code_id()); + } + + #[tokio::test] + #[ntest::timeout(3000)] + async fn process_already_validated_code() { + let db = Database::memory(); + let mut service = CodesSubService::new(db.clone(), MockProcessor); + + let code_and_id = CodeAndId::new(vec![1, 2, 3, 4]); + let code_id = code_and_id.code_id(); + db.set_code_valid(code_id, true); + db.set_original_code(code_and_id.code()); + db.set_instrumented_code( + ethexe_runtime_common::VERSION, + code_id, + InstrumentedCode::new( + vec![5, 6, 7, 8], + InstantiatedSectionSizes::new(1, 1, 1, 1, 1, 1), + ), + ); + service.receive_code_to_process(code_and_id.into_unchecked()); + assert_eq!(service.next().await.unwrap(), code_id); + + let code_and_id = CodeAndId::new(vec![100, 101, 102, 103]); + let code_id = code_and_id.code_id(); + db.set_code_valid(code_id, false); + service.receive_code_to_process(code_and_id.into_unchecked()); + assert_eq!(service.next().await.unwrap(), code_id); + } +} diff --git a/ethexe/processor/src/handling/overlaid.rs b/ethexe/processor/src/handling/overlaid.rs index 30c2a63f3c3..211c6ca9e3b 100644 --- a/ethexe/processor/src/handling/overlaid.rs +++ b/ethexe/processor/src/handling/overlaid.rs @@ -87,17 +87,15 @@ impl OverlaidRunContext { chunk_size, block_header, false, + None, ), base_program, nullified_queue_programs: [base_program].into_iter().collect(), } } - pub(crate) async fn run( - mut self, - promise_sender: Option>, - ) -> Result { - let _ = run::run_for_queue_type(&mut self, MessageType::Canonical, promise_sender).await?; + pub(crate) async fn run(mut self) -> Result { + let _ = run::run_for_queue_type(&mut self, MessageType::Canonical).await?; Ok(self.inner.transitions) } @@ -179,6 +177,11 @@ impl RunContext for OverlaidRunContext { &self.inner.instance_creator } + fn promise_sender(&self) -> &Option> { + // OverlaidRunContext should never produce promises + &None + } + fn block_header(&self) -> BlockHeader { self.inner.block_header } diff --git a/ethexe/processor/src/handling/run.rs b/ethexe/processor/src/handling/run.rs index 128e8f21726..225531af1f3 100644 --- a/ethexe/processor/src/handling/run.rs +++ b/ethexe/processor/src/handling/run.rs @@ -135,7 +135,6 @@ use tokio::sync::mpsc; pub(super) async fn run_for_queue_type( ctx: &mut impl RunContext, queue_type: MessageType, - promise_sender: Option>, ) -> Result { let mut is_out_of_gas_for_block = false; @@ -150,13 +149,8 @@ pub(super) async fn run_for_queue_type( for chunk in chunks { // Spawn on a separate thread an execution of each program (it's queue) in the chunk. - let chunk_outputs = chunk_execution_spawn::spawn_chunk_execution( - ctx, - chunk, - queue_type, - promise_sender.clone(), - ) - .await?; + let chunk_outputs = + chunk_execution_spawn::spawn_chunk_execution(ctx, chunk, queue_type).await?; // Collect journals from all executed programs in the chunk. let (chunk_journals, max_gas_spent_in_chunk) = @@ -196,6 +190,8 @@ pub(super) trait RunContext { /// Get reference to instance creator. fn instance_creator(&self) -> &InstanceCreator; + fn promise_sender(&self) -> &Option>; + /// Returns the header of the current block. fn block_header(&self) -> BlockHeader; @@ -276,10 +272,15 @@ pub(crate) struct CommonRunContext { pub(crate) gas_allowance_counter: GasAllowanceCounter, pub(crate) chunk_size: usize, pub(crate) block_header: BlockHeader, + + // TODO think about removing this pub(crate) should_produce_promises: bool, + + pub(crate) promise_sender: Option>, } impl CommonRunContext { + #[allow(clippy::too_many_arguments)] pub(crate) fn new( db: Database, instance_creator: InstanceCreator, @@ -288,6 +289,7 @@ impl CommonRunContext { chunk_size: usize, block_header: BlockHeader, should_produce_promises: bool, + promise_sender: Option>, ) -> Self { CommonRunContext { db, @@ -297,21 +299,17 @@ impl CommonRunContext { chunk_size, block_header, should_produce_promises, + promise_sender, } } - pub(crate) async fn run( - mut self, - promise_sender: Option>, - ) -> Result { + pub(crate) async fn run(mut self) -> Result { // Start with injected queues processing. - let can_continue = - run_for_queue_type(&mut self, MessageType::Injected, promise_sender.clone()).await?; + let can_continue = run_for_queue_type(&mut self, MessageType::Injected).await?; if can_continue { // If gas is still left in block, process canonical (Ethereum) queues - let _ = run_for_queue_type(&mut self, MessageType::Canonical, promise_sender.clone()) - .await?; + let _ = run_for_queue_type(&mut self, MessageType::Canonical).await?; } Ok(self.transitions) @@ -323,6 +321,10 @@ impl RunContext for CommonRunContext { &self.instance_creator } + fn promise_sender(&self) -> &Option> { + &self.promise_sender + } + fn block_header(&self) -> BlockHeader { self.block_header } @@ -535,7 +537,6 @@ mod chunk_execution_spawn { ctx: &mut impl RunContext, chunk: Vec<(ActorId, H256)>, queue_type: MessageType, - promise_sender: Option>, ) -> Result> { struct Executable { program_id: ActorId, @@ -575,6 +576,7 @@ mod chunk_execution_spawn { }) .collect::>>()?; + let promise_sender = ctx.promise_sender().clone(); let block_header = ctx.block_header(); let block_info = BlockInfo { height: block_header.height, @@ -774,6 +776,7 @@ mod tests { chunk_size: CHUNK_PROCESSING_THREADS, block_header: BlockHeader::dummy(3), should_produce_promises: false, + promise_sender: None, }; let chunks = chunks_splitting::prepare_execution_chunks(&mut ctx, MessageType::Canonical); diff --git a/ethexe/processor/src/host/api/promise.rs b/ethexe/processor/src/host/api/promise.rs index fe49d0ed511..ee1fe0a350a 100644 --- a/ethexe/processor/src/host/api/promise.rs +++ b/ethexe/processor/src/host/api/promise.rs @@ -16,43 +16,34 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use ethexe_common::{HashOf, injected::Promise}; -use gprimitives::MessageId; use sp_wasm_interface::StoreData; use wasmtime::{Caller, Linker}; use crate::host::{api::MemoryWrap, threads}; pub fn link(linker: &mut Linker) -> Result<(), wasmtime::Error> { - linker.func_wrap("env", "ext_forward_promise_to_service", forward_promise)?; + linker.func_wrap("env", "ext_publish_promise", publish_promise)?; Ok(()) } -// TODO: it is a raw implementation, should be fixed -fn forward_promise( - caller: Caller<'_, StoreData>, - encoded_reply_ptr_len: i64, - message_id_ptr_len: i64, -) { +fn publish_promise(caller: Caller<'_, StoreData>, promise_ptr_len: i64) { let memory = MemoryWrap(caller.data().memory()); - let reply = memory.decode_by_val(&caller, encoded_reply_ptr_len); - let message_id = memory.decode_by_val::<_, MessageId>(&caller, message_id_ptr_len); - threads::with_params(|params| { if let Some(ref sender) = params.promise_sender { - let tx_hash = unsafe { HashOf::new(message_id.into_bytes().into()) }; - let promise = Promise { tx_hash, reply }; + let promise = memory.decode_by_val(&caller, promise_ptr_len); match sender.send(promise) { Ok(()) => { log::trace!( - "successfully send promise to outer service: encoded_reply_ptr_len={encoded_reply_ptr_len}, message_id_ptr_len={message_id_ptr_len}" + "successfully send promise to outer service: promise_ptr_len={promise_ptr_len}" ); } Err(err) => { - log::trace!("failed to send promise to outer service: error={err}"); + log::trace!( + "`publish_promise`: failed to send promise to receiver because of error={err}" + ); } } } diff --git a/ethexe/processor/src/lib.rs b/ethexe/processor/src/lib.rs index 210c98a40c1..e273a90ad68 100644 --- a/ethexe/processor/src/lib.rs +++ b/ethexe/processor/src/lib.rs @@ -108,6 +108,11 @@ pub struct Processor { config: ProcessorConfig, db: Database, creator: InstanceCreator, + // TODO: Think about adding the + // #[cfg(test)] + // promise_sender: Option>, + // #[cfg(not(test))] + // promise_sender: mpsc::UnboundedSender, promise_sender: Option>, } @@ -258,8 +263,9 @@ impl Processor { self.config.chunk_size, block.header, should_produce_promises, + self.promise_sender.clone(), ) - .run(self.promise_sender.clone()) + .run() .await } @@ -407,7 +413,7 @@ impl OverlaidProcessor { self.0.creator.clone(), block.header, ) - .run(self.0.promise_sender.clone()) + .run() .await?; let res = transitions diff --git a/ethexe/runtime/common/src/lib.rs b/ethexe/runtime/common/src/lib.rs index 160ed96b582..b1be1e8c1e6 100644 --- a/ethexe/runtime/common/src/lib.rs +++ b/ethexe/runtime/common/src/lib.rs @@ -28,7 +28,11 @@ use core_processor::{ common::{ExecutableActorData, JournalNote}, configs::{BlockConfig, SyscallName}, }; -use ethexe_common::gear::{CHUNK_PROCESSING_GAS_LIMIT, MessageType}; +use ethexe_common::{ + HashOf, + gear::{CHUNK_PROCESSING_GAS_LIMIT, MessageType}, + injected::Promise, +}; use gear_core::{ code::{CodeMetadata, InstrumentedCode, MAX_WASM_PAGES_AMOUNT}, gas::GasAllowanceCounter, @@ -38,7 +42,7 @@ use gear_core::{ rpc::ReplyInfo, }; use gear_lazy_pages_common::LazyPagesInterface; -use gprimitives::{H256, MessageId}; +use gprimitives::H256; use gsys::{GasMultiplier, Percent}; use journal::RuntimeJournalHandler; use state::{Dispatch, ProgramState, Storage}; @@ -85,8 +89,7 @@ pub trait RuntimeInterface: Storage { fn init_lazy_pages(&self); fn random_data(&self) -> (Vec, u32); fn update_state_hash(&self, state_hash: &H256); - // TODO: create more meaningful function name - fn send_promise(&self, reply: &ReplyInfo, message_id: &MessageId); + fn publish_promise(&self, promise: &Promise); } /// A main low-level interface to perform state changes @@ -230,6 +233,7 @@ where stop_processing: false, }; + // TODO: move to separate function if ctx.should_produce_promises && is_promise_required { for note in journal.iter() { if let JournalNote::SendDispatch { @@ -251,7 +255,10 @@ where payload: dispatch.message().payload_bytes().to_vec(), }; - ri.send_promise(&reply, &dispatch_id); + let tx_hash = unsafe { HashOf::new(dispatch_id.into_bytes().into()) }; + let promise = Promise { reply, tx_hash }; + + ri.publish_promise(&promise); break; } } diff --git a/ethexe/runtime/src/wasm/interface/promise.rs b/ethexe/runtime/src/wasm/interface/promise.rs index 76f85a97ddd..7ed2f4c50ad 100644 --- a/ethexe/runtime/src/wasm/interface/promise.rs +++ b/ethexe/runtime/src/wasm/interface/promise.rs @@ -17,27 +17,23 @@ // along with this program. If not, see . use crate::wasm::interface; +use ethexe_common::injected::Promise; use ethexe_runtime_common::pack_u32_to_i64; -use gear_core::rpc::ReplyInfo; -use gprimitives::MessageId; use parity_scale_codec::Encode; interface::declare!( - pub(super) fn ext_forward_promise_to_service( - encoded_reply_ptr_len: i64, - message_id_ptr_len: i64, - ); + pub(super) fn ext_publish_promise(promise_ptr_len: i64); ); -pub fn send_promise(reply: &ReplyInfo, message_id: &MessageId) { + +pub fn publish_promise(promise: &Promise) { unsafe { - let message_id_ptr_len = pack_u32_to_i64( - message_id.as_ref().as_ptr() as _, - message_id.encoded_size() as _, - ); - let encoded_reply = reply.encode(); - let encoded_reply_ptr_len = - pack_u32_to_i64(encoded_reply.as_ptr() as _, reply.encoded_size() as _); + // Important: the `Promise` struct contains the `ReplyInfo` which have the dynamic type. + // So we need to encode the promise and pass to host handler a pointer and size of encoded data. + + let encoded_promise = promise.encode(); + let promise_ptr_len = + pack_u32_to_i64(encoded_promise.as_ptr() as _, encoded_promise.len() as _); - sys::ext_forward_promise_to_service(encoded_reply_ptr_len, message_id_ptr_len); + sys::ext_publish_promise(promise_ptr_len); } } diff --git a/ethexe/runtime/src/wasm/storage.rs b/ethexe/runtime/src/wasm/storage.rs index c73040360a3..3ff6ca29d82 100644 --- a/ethexe/runtime/src/wasm/storage.rs +++ b/ethexe/runtime/src/wasm/storage.rs @@ -20,7 +20,7 @@ use crate::wasm::interface::promise_ri; use super::interface::database_ri; use alloc::vec::Vec; -use ethexe_common::HashOf; +use ethexe_common::{HashOf, injected::Promise}; use ethexe_runtime_common::{ RuntimeInterface, state::{ @@ -28,9 +28,9 @@ use ethexe_runtime_common::{ ProgramState, Storage, UserMailbox, Waitlist, }, }; -use gear_core::{buffer::Payload, memory::PageBuf, rpc::ReplyInfo}; +use gear_core::{buffer::Payload, memory::PageBuf}; use gear_lazy_pages_interface::{LazyPagesInterface, LazyPagesRuntimeInterface}; -use gprimitives::{H256, MessageId}; +use gprimitives::H256; #[derive(Debug, Clone)] pub struct NativeRuntimeInterface; @@ -153,7 +153,7 @@ impl RuntimeInterface for NativeRuntimeInterface { database_ri::update_state_hash(hash); } - fn send_promise(&self, reply: &ReplyInfo, message_id: &MessageId) { - promise_ri::send_promise(reply, message_id); + fn publish_promise(&self, promise: &Promise) { + promise_ri::publish_promise(promise); } } From e4f07c0e329b28fcc4b6642d7cd3439e362d1566 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Thu, 26 Feb 2026 14:22:52 +0300 Subject: [PATCH 17/59] implement the builder for compute service --- ethexe/compute/src/lib.rs | 2 +- ethexe/compute/src/service.rs | 152 ++++++++++++++++--- ethexe/compute/src/tests.rs | 12 +- ethexe/processor/src/handling/overlaid.rs | 2 +- ethexe/processor/src/handling/run.rs | 18 +-- ethexe/processor/src/host/api/promise.rs | 2 +- ethexe/processor/src/host/mod.rs | 4 +- ethexe/processor/src/host/threads.rs | 6 +- ethexe/processor/src/lib.rs | 16 +- ethexe/processor/src/tests.rs | 16 +- ethexe/runtime/common/src/lib.rs | 2 + ethexe/runtime/src/wasm/interface/promise.rs | 1 + ethexe/service/src/lib.rs | 33 ++-- ethexe/service/src/tests/utils/env.rs | 23 ++- 14 files changed, 190 insertions(+), 99 deletions(-) diff --git a/ethexe/compute/src/lib.rs b/ethexe/compute/src/lib.rs index 911a89c40bd..6085349a8b5 100644 --- a/ethexe/compute/src/lib.rs +++ b/ethexe/compute/src/lib.rs @@ -23,7 +23,7 @@ use gprimitives::{CodeId, H256}; use std::collections::HashSet; pub use compute::{ComputeConfig, ComputeSubService, prepare_executable_for_announce}; -pub use service::ComputeService; +pub use service::{ComputeService, builder::Builder as ComputeServiceBuilder}; mod codes; mod compute; diff --git a/ethexe/compute/src/service.rs b/ethexe/compute/src/service.rs index 164ae7f6c1c..348d0910d0f 100644 --- a/ethexe/compute/src/service.rs +++ b/ethexe/compute/src/service.rs @@ -16,6 +16,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +#[cfg(test)] +use crate::tests::MockProcessor; use crate::{ ComputeError, ComputeEvent, ProcessorExt, Result, codes::CodesSubService, @@ -24,10 +26,11 @@ use crate::{ }; use ethexe_common::{Announce, CodeAndIdUnchecked, injected::Promise}; use ethexe_db::Database; -use ethexe_processor::Processor; +use ethexe_processor::{Processor, ProcessorConfig}; use futures::{Stream, stream::FusedStream}; use gprimitives::H256; use std::{ + marker::PhantomData, pin::Pin, task::{Context, Poll}, }; @@ -42,19 +45,6 @@ pub struct ComputeService { impl ComputeService

{ // TODO #4550: consider to create Processor inside ComputeService - pub fn new( - config: ComputeConfig, - db: Database, - processor: P, - promise_receiver: Option>, - ) -> Self { - Self { - prepare_sub_service: PrepareSubService::new(db.clone()), - compute_sub_service: ComputeSubService::new(config, db.clone(), processor.clone()), - codes_sub_service: CodesSubService::new(db, processor), - promise_receiver, - } - } pub fn process_code(&mut self, code_and_id: CodeAndIdUnchecked) { self.codes_sub_service.receive_code_to_process(code_and_id); @@ -125,10 +115,130 @@ pub(crate) trait SubService: Unpin + Send + 'static { } } +pub(crate) mod builder { + use super::*; + + // Builder states + #[cfg(test)] + #[derive(Default)] + pub struct Mock; + #[derive(Default)] + pub struct Production; + #[derive(Default)] + pub struct SetDatabase; + #[derive(Default)] + pub struct SetComputeConfig; + #[derive(Default)] + pub struct SetProcessorConfig; + + #[derive(Default)] + pub struct Builder { + config: Option, + db: Option, + processor_config: Option, + _state: PhantomData, + } + + #[cfg(test)] + impl Builder { + pub(crate) fn mock() -> Self { + Self::default() + } + + #[allow(unused)] + pub(crate) fn with_config(mut self, config: ComputeConfig) -> Self { + self.config = Some(config); + self + } + + #[allow(unused)] + pub(crate) fn with_db(mut self, db: Database) -> Self { + self.db = Some(db); + self + } + + pub(crate) fn build(self) -> ComputeService { + let processor = MockProcessor; + let config = self.config.unwrap_or(ComputeConfig::without_quarantine()); + let db = self.db.unwrap_or(Database::memory()); + + ComputeService { + prepare_sub_service: PrepareSubService::new(db.clone()), + compute_sub_service: ComputeSubService::new(config, db.clone(), processor.clone()), + codes_sub_service: CodesSubService::new(db, processor), + promise_receiver: None, + } + } + } + + impl Builder { + pub fn production() -> Self { + Self::default() + } + + pub fn with_db(self, db: Database) -> Builder { + Builder { + db: Some(db), + _state: PhantomData, + ..Default::default() + } + } + + #[cfg(test)] + pub fn with_defaults(self, db: Database) -> Builder { + self.with_db(db) + .with_compute_config(ComputeConfig::without_quarantine()) + .with_processor_config(ProcessorConfig::default()) + } + } + + impl Builder { + pub fn with_compute_config(self, config: ComputeConfig) -> Builder { + Builder { + db: self.db, + config: Some(config), + _state: PhantomData, + ..Default::default() + } + } + } + + impl Builder { + pub fn with_processor_config(self, config: ProcessorConfig) -> Builder { + Builder { + db: self.db, + config: self.config, + processor_config: Some(config), + _state: PhantomData, + } + } + } + + impl Builder { + pub fn build(self) -> Result { + let db = self.db.unwrap(); + let config = self.config.unwrap(); + let processor_config = self.processor_config.unwrap(); + + let (promise_out_tx, promise_receiver) = mpsc::unbounded_channel(); + let processor = + Processor::with_config(processor_config, db.clone(), Some(promise_out_tx))?; + + Ok(ComputeService { + prepare_sub_service: PrepareSubService::new(db.clone()), + compute_sub_service: ComputeSubService::new(config, db.clone(), processor.clone()), + codes_sub_service: CodesSubService::new(db, processor), + promise_receiver: Some(promise_receiver), + }) + } + } +} + #[cfg(test)] mod tests { + use crate::ComputeServiceBuilder; + use super::*; - use crate::tests::MockProcessor; use ethexe_common::{CodeAndIdUnchecked, db::*, mock::*}; use ethexe_db::Database as DB; use futures::StreamExt; @@ -141,9 +251,7 @@ mod tests { gear_utils::init_default_logger(); let db = DB::memory(); - let processor = MockProcessor; - let config = ComputeConfig::without_quarantine(); - let mut service = ComputeService::new(config, db.clone(), processor, None); + let mut service = ComputeServiceBuilder::mock().with_db(db.clone()).build(); let chain = BlockChain::mock(1).setup(&db); let block = chain.blocks[1].to_simple().next_block().setup(&db); @@ -165,10 +273,8 @@ mod tests { gear_utils::init_default_logger(); let db = DB::memory(); - let processor = MockProcessor; + let mut service = ComputeServiceBuilder::mock().with_db(db.clone()).build(); - let config = ComputeConfig::without_quarantine(); - let mut service = ComputeService::new(config, db.clone(), processor, None); let chain = BlockChain::mock(1).setup(&db); let block = chain.blocks[1].to_simple().next_block().setup(&db); @@ -201,9 +307,7 @@ mod tests { gear_utils::init_default_logger(); let db = DB::memory(); - let processor = MockProcessor; - let config = ComputeConfig::without_quarantine(); - let mut service = ComputeService::new(config, db.clone(), processor, None); + let mut service = ComputeServiceBuilder::mock().with_db(db.clone()).build(); // Create test code let code = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]; // Simple WASM header diff --git a/ethexe/compute/src/tests.rs b/ethexe/compute/src/tests.rs index e2ea5ffc422..e38db3e05bb 100644 --- a/ethexe/compute/src/tests.rs +++ b/ethexe/compute/src/tests.rs @@ -27,7 +27,6 @@ use ethexe_common::{ mock::*, }; use ethexe_db::Database; -use ethexe_processor::Processor; use futures::StreamExt; use gear_core::{ code::{CodeMetadata, InstantiatedSectionSizes, InstrumentedCode}, @@ -167,13 +166,10 @@ impl TestEnv { mark_as_not_prepared(&mut chain); chain = chain.setup(&db); - let config = ComputeConfig::without_quarantine(); - let compute = ComputeService::new( - config, - db.clone(), - Processor::new(db.clone(), None).unwrap(), - None, - ); + let compute = ComputeServiceBuilder::production() + .with_defaults(db.clone()) + .build() + .unwrap(); TestEnv { db, compute, chain } } diff --git a/ethexe/processor/src/handling/overlaid.rs b/ethexe/processor/src/handling/overlaid.rs index 211c6ca9e3b..e3a03a95a35 100644 --- a/ethexe/processor/src/handling/overlaid.rs +++ b/ethexe/processor/src/handling/overlaid.rs @@ -177,7 +177,7 @@ impl RunContext for OverlaidRunContext { &self.inner.instance_creator } - fn promise_sender(&self) -> &Option> { + fn promise_out_tx(&self) -> &Option> { // OverlaidRunContext should never produce promises &None } diff --git a/ethexe/processor/src/handling/run.rs b/ethexe/processor/src/handling/run.rs index 225531af1f3..028f1c50b68 100644 --- a/ethexe/processor/src/handling/run.rs +++ b/ethexe/processor/src/handling/run.rs @@ -190,7 +190,7 @@ pub(super) trait RunContext { /// Get reference to instance creator. fn instance_creator(&self) -> &InstanceCreator; - fn promise_sender(&self) -> &Option>; + fn promise_out_tx(&self) -> &Option>; /// Returns the header of the current block. fn block_header(&self) -> BlockHeader; @@ -276,7 +276,7 @@ pub(crate) struct CommonRunContext { // TODO think about removing this pub(crate) should_produce_promises: bool, - pub(crate) promise_sender: Option>, + pub(crate) promise_out_tx: Option>, } impl CommonRunContext { @@ -289,7 +289,7 @@ impl CommonRunContext { chunk_size: usize, block_header: BlockHeader, should_produce_promises: bool, - promise_sender: Option>, + promise_out_tx: Option>, ) -> Self { CommonRunContext { db, @@ -299,7 +299,7 @@ impl CommonRunContext { chunk_size, block_header, should_produce_promises, - promise_sender, + promise_out_tx, } } @@ -321,8 +321,8 @@ impl RunContext for CommonRunContext { &self.instance_creator } - fn promise_sender(&self) -> &Option> { - &self.promise_sender + fn promise_out_tx(&self) -> &Option> { + &self.promise_out_tx } fn block_header(&self) -> BlockHeader { @@ -576,7 +576,7 @@ mod chunk_execution_spawn { }) .collect::>>()?; - let promise_sender = ctx.promise_sender().clone(); + let promise_out_tx = ctx.promise_out_tx().clone(); let block_header = ctx.block_header(); let block_info = BlockInfo { height: block_header.height, @@ -612,7 +612,7 @@ mod chunk_execution_spawn { block_info, should_produce_promises, }, - promise_sender.clone(), + promise_out_tx.clone(), ) .expect("Some error occurs while running program in instance"); @@ -776,7 +776,7 @@ mod tests { chunk_size: CHUNK_PROCESSING_THREADS, block_header: BlockHeader::dummy(3), should_produce_promises: false, - promise_sender: None, + promise_out_tx: None, }; let chunks = chunks_splitting::prepare_execution_chunks(&mut ctx, MessageType::Canonical); diff --git a/ethexe/processor/src/host/api/promise.rs b/ethexe/processor/src/host/api/promise.rs index ee1fe0a350a..ab68264a0cb 100644 --- a/ethexe/processor/src/host/api/promise.rs +++ b/ethexe/processor/src/host/api/promise.rs @@ -31,7 +31,7 @@ fn publish_promise(caller: Caller<'_, StoreData>, promise_ptr_len: i64) { let memory = MemoryWrap(caller.data().memory()); threads::with_params(|params| { - if let Some(ref sender) = params.promise_sender { + if let Some(ref sender) = params.promise_out_tx { let promise = memory.decode_by_val(&caller, promise_ptr_len); match sender.send(promise) { diff --git a/ethexe/processor/src/host/mod.rs b/ethexe/processor/src/host/mod.rs index 20e42aa57ef..6bb88de998b 100644 --- a/ethexe/processor/src/host/mod.rs +++ b/ethexe/processor/src/host/mod.rs @@ -171,9 +171,9 @@ impl InstanceWrapper { &mut self, db: Box, ctx: ProcessQueueContext, - promise_sender: Option>, + promise_out_tx: Option>, ) -> Result<(ProgramJournals, H256, u64)> { - threads::set(db, ctx.state_root, promise_sender.clone()); + threads::set(db, ctx.state_root, promise_out_tx.clone()); // Pieces of resulting journal. Hack to avoid single allocation limit. let (ptr_lens, gas_spent): (Vec, i64) = self.call("run", ctx.encode())?; diff --git a/ethexe/processor/src/host/threads.rs b/ethexe/processor/src/host/threads.rs index b30aa2ac5d1..c64b975d978 100644 --- a/ethexe/processor/src/host/threads.rs +++ b/ethexe/processor/src/host/threads.rs @@ -43,7 +43,7 @@ pub struct ThreadParams { pub db: Box, pub state_hash: H256, /// TODO: think about using [`mpsc::sync_channel`] instead of [`mpsc::channel`]. - pub promise_sender: Option>, + pub promise_out_tx: Option>, pages_registry_cache: Option, pages_regions_cache: Option>, } @@ -108,14 +108,14 @@ impl PageKey { pub fn set( db: Box, state_hash: H256, - promise_sender: Option>, + promise_out_tx: Option>, ) { PARAMS.set(Some(ThreadParams { db, state_hash, pages_registry_cache: None, pages_regions_cache: None, - promise_sender, + promise_out_tx, })) } diff --git a/ethexe/processor/src/lib.rs b/ethexe/processor/src/lib.rs index e273a90ad68..0fd5fc837a5 100644 --- a/ethexe/processor/src/lib.rs +++ b/ethexe/processor/src/lib.rs @@ -110,10 +110,10 @@ pub struct Processor { creator: InstanceCreator, // TODO: Think about adding the // #[cfg(test)] - // promise_sender: Option>, + // promise_out_tx: Option>, // #[cfg(not(test))] - // promise_sender: mpsc::UnboundedSender, - promise_sender: Option>, + // promise_out_tx: mpsc::UnboundedSender, + promise_out_tx: Option>, } /// TODO: consider avoiding re-instantiations on processing events. @@ -122,22 +122,22 @@ impl Processor { /// Creates processor with default config. pub fn new( db: Database, - promise_sender: Option>, + promise_out_tx: Option>, ) -> Result { - Self::with_config(Default::default(), db, promise_sender) + Self::with_config(Default::default(), db, promise_out_tx) } pub fn with_config( config: ProcessorConfig, db: Database, - promise_sender: Option>, + promise_out_tx: Option>, ) -> Result { let creator = InstanceCreator::new(host::runtime())?; Ok(Self { config, db, creator, - promise_sender, + promise_out_tx, }) } @@ -263,7 +263,7 @@ impl Processor { self.config.chunk_size, block.header, should_produce_promises, - self.promise_sender.clone(), + self.promise_out_tx.clone(), ) .run() .await diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index 6bfc4cf067d..35390eda342 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -98,10 +98,10 @@ mod utils { pub fn setup_test_env_and_load_codes( codes: [&[u8]; N], - promise_sender: Option>, + promise_out_tx: Option>, ) -> (Processor, BlockChain, [CodeId; N]) { let db = Database::memory(); - let mut processor = Processor::new(db.clone(), promise_sender).unwrap(); + let mut processor = Processor::new(db.clone(), promise_out_tx).unwrap(); let chain = BlockChain::mock(20).setup(&db); let mut code_ids = Vec::new(); @@ -853,9 +853,9 @@ async fn overlay_execution() { async fn injected_ping_pong() { init_logger(); - let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); + let (promise_out_tx, mut promise_receiver) = mpsc::unbounded_channel(); let (mut processor, chain, [code_id]) = - setup_test_env_and_load_codes([demo_ping::WASM_BINARY], Some(promise_sender)); + setup_test_env_and_load_codes([demo_ping::WASM_BINARY], Some(promise_out_tx)); let block1 = chain.blocks[1].to_simple(); let user_1 = ActorId::from(10); @@ -961,9 +961,9 @@ async fn injected_prioritized_over_canonical() { init_logger(); - let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); + let (promise_out_tx, mut promise_receiver) = mpsc::unbounded_channel(); let (mut processor, chain, [code_id]) = - setup_test_env_and_load_codes([demo_ping::WASM_BINARY], Some(promise_sender)); + setup_test_env_and_load_codes([demo_ping::WASM_BINARY], Some(promise_out_tx)); let block1 = chain.blocks[1].to_simple(); let canonical_user = ActorId::from(10); @@ -1156,9 +1156,9 @@ async fn executable_balance_injected_panic_not_charged() { init_logger(); - let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); + let (promise_out_tx, mut promise_receiver) = mpsc::unbounded_channel(); let (mut processor, chain, [code_id]) = - setup_test_env_and_load_codes([demo_panic_payload::WASM_BINARY], Some(promise_sender)); + setup_test_env_and_load_codes([demo_panic_payload::WASM_BINARY], Some(promise_out_tx)); let block1 = chain.blocks[1].to_simple(); let user_id = ActorId::from(10); diff --git a/ethexe/runtime/common/src/lib.rs b/ethexe/runtime/common/src/lib.rs index b1be1e8c1e6..74b6fee9fae 100644 --- a/ethexe/runtime/common/src/lib.rs +++ b/ethexe/runtime/common/src/lib.rs @@ -89,6 +89,8 @@ pub trait RuntimeInterface: Storage { fn init_lazy_pages(&self); fn random_data(&self) -> (Vec, u32); fn update_state_hash(&self, state_hash: &H256); + /// Publish a promise produced during execution to the compute service layer. + /// The implementation is expected to forward it to external subscribers. fn publish_promise(&self, promise: &Promise); } diff --git a/ethexe/runtime/src/wasm/interface/promise.rs b/ethexe/runtime/src/wasm/interface/promise.rs index 7ed2f4c50ad..06b38acdd69 100644 --- a/ethexe/runtime/src/wasm/interface/promise.rs +++ b/ethexe/runtime/src/wasm/interface/promise.rs @@ -25,6 +25,7 @@ interface::declare!( pub(super) fn ext_publish_promise(promise_ptr_len: i64); ); +/// Encode and forward a promise to the host for publication. pub fn publish_promise(promise: &Promise) { unsafe { // Important: the `Promise` struct contains the `ReplyInfo` which have the dynamic type. diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 133d4e05148..ccd9380de87 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -25,7 +25,7 @@ use anyhow::{Context, Result, bail}; use async_trait::async_trait; use ethexe_blob_loader::{BlobLoader, BlobLoaderEvent, BlobLoaderService, ConsensusLayerConfig}; use ethexe_common::{COMMITMENT_DELAY_LIMIT, gear::CodeState, network::VerifiedValidatorMessage}; -use ethexe_compute::{ComputeConfig, ComputeEvent, ComputeService}; +use ethexe_compute::{ComputeConfig, ComputeEvent, ComputeService, ComputeServiceBuilder}; use ethexe_consensus::{ ConnectService, ConsensusEvent, ConsensusService, ValidatorConfig, ValidatorService, }; @@ -39,7 +39,7 @@ use ethexe_observer::{ ObserverEvent, ObserverService, utils::{BlockId, BlockLoader}, }; -use ethexe_processor::{Processor, ProcessorConfig}; +use ethexe_processor::ProcessorConfig; use ethexe_prometheus::PrometheusService; use ethexe_rpc::{RpcEvent, RpcServer}; use ethexe_service_utils::{OptionFuture as _, OptionStreamNext as _}; @@ -53,7 +53,7 @@ use std::{ pin::Pin, time::Duration, }; -use tokio::sync::{mpsc, oneshot}; +use tokio::sync::oneshot; pub mod config; @@ -277,20 +277,10 @@ impl Service { .await .with_context(|| "failed to query validators threshold")?; log::info!("🔒 Multisig threshold: {threshold} / {}", validators.len()); - let (promise_sender, promise_receiver) = mpsc::unbounded_channel(); - - let processor = Processor::with_config( - ProcessorConfig { - chunk_size: config.node.chunk_processing_threads, - }, - db.clone(), - Some(promise_sender), - ) - .with_context(|| "failed to create processor")?; log::info!( "🔧 Amount of chunk processing threads for programs processing: {}", - processor.config().chunk_size + config.node.chunk_processing_threads ); let signer = Signer::fs(config.node.key_path.clone())?; @@ -379,12 +369,15 @@ impl Service { .map(|config| RpcServer::new(config.clone(), db.clone())); let compute_config = ComputeConfig::new(config.node.canonical_quarantine); - let compute = ComputeService::new( - compute_config, - db.clone(), - processor, - Some(promise_receiver), - ); + let processor_config = ProcessorConfig { + chunk_size: config.node.chunk_processing_threads, + }; + let compute = ComputeServiceBuilder::production() + .with_db(db.clone()) + .with_compute_config(compute_config) + .with_processor_config(processor_config) + .build() + .with_context(|| "failed to build compute service")?; let fast_sync = config.node.fast_sync; diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index 66ead2a2b77..99c3782e722 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -42,7 +42,7 @@ use ethexe_common::{ }, network::{SignedValidatorMessage, ValidatorMessage}, }; -use ethexe_compute::{ComputeConfig, ComputeService}; +use ethexe_compute::{ComputeConfig, ComputeServiceBuilder}; use ethexe_consensus::{BatchCommitter, ConnectService, ConsensusService, ValidatorService}; use ethexe_db::Database; use ethexe_ethereum::{ @@ -56,7 +56,7 @@ use ethexe_observer::{ EthereumConfig, ObserverService, utils::{BlockId, BlockLoader, EthereumBlockLoader}, }; -use ethexe_processor::{DEFAULT_CHUNK_SIZE, Processor}; +use ethexe_processor::{DEFAULT_CHUNK_SIZE, ProcessorConfig}; use ethexe_rpc::{DEFAULT_BLOCK_GAS_LIMIT_MULTIPLIER, RpcConfig, RpcServer}; use futures::StreamExt; use gear_core_errors::ReplyCode; @@ -79,10 +79,7 @@ use std::{ sync::atomic::{AtomicUsize, Ordering}, time::Duration, }; -use tokio::{ - sync::mpsc, - task::{self, JoinHandle}, -}; +use tokio::task::{self, JoinHandle}; use tracing::Instrument; /// Max network services which can be created by one test environment. @@ -889,14 +886,12 @@ impl Node { "Service is already running" ); - let (promise_sender, promise_receiver) = mpsc::unbounded_channel(); - let processor = Processor::new(self.db.clone(), Some(promise_sender)).unwrap(); - let compute = ComputeService::new( - self.compute_config, - self.db.clone(), - processor, - Some(promise_receiver), - ); + let compute = ComputeServiceBuilder::production() + .with_db(self.db.clone()) + .with_compute_config(self.compute_config) + .with_processor_config(ProcessorConfig::default()) + .build() + .unwrap(); let observer = ObserverService::new(&self.eth_cfg, u32::MAX, self.db.clone()) .await From 84146595a02a5b5a65f20034829ce01fca8b2fbf Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Thu, 26 Feb 2026 15:12:11 +0300 Subject: [PATCH 18/59] make compute service builder implementation much prettier --- ethexe/compute/src/service.rs | 71 ++++++++++++++++----------- ethexe/compute/src/tests.rs | 3 +- ethexe/service/src/lib.rs | 6 +-- ethexe/service/src/tests/utils/env.rs | 6 +-- 4 files changed, 50 insertions(+), 36 deletions(-) diff --git a/ethexe/compute/src/service.rs b/ethexe/compute/src/service.rs index 348d0910d0f..71543db7937 100644 --- a/ethexe/compute/src/service.rs +++ b/ethexe/compute/src/service.rs @@ -115,32 +115,39 @@ pub(crate) trait SubService: Unpin + Send + 'static { } } +/// Module provides a builder for [`ComputeService`]. +/// The [`builder::Builder`] must be used for both production and testing purposes. pub(crate) mod builder { use super::*; - // Builder states + // Builder environments #[cfg(test)] #[derive(Default)] pub struct Mock; #[derive(Default)] pub struct Production; + + // Builder states #[derive(Default)] - pub struct SetDatabase; - #[derive(Default)] - pub struct SetComputeConfig; + pub struct Set; #[derive(Default)] - pub struct SetProcessorConfig; + pub struct Unset; + /// A type-state builder for [`ComputeService`]. + /// Provides the easy construction the [`ComputeService`] for both + /// testing and production environments. #[derive(Default)] - pub struct Builder { + pub struct Builder { config: Option, db: Option, processor_config: Option, - _state: PhantomData, + _state: PhantomData<(Env, C, D, P)>, } + /// Mock builder uses defaults when fields are None; type-states are fixed to Set. #[cfg(test)] - impl Builder { + impl Builder { + /// Creates a new mock builder. pub(crate) fn mock() -> Self { Self::default() } @@ -157,6 +164,7 @@ pub(crate) mod builder { self } + /// Creates a [`ComputeService`] from a mock builder. pub(crate) fn build(self) -> ComputeService { let processor = MockProcessor; let config = self.config.unwrap_or(ComputeConfig::without_quarantine()); @@ -171,50 +179,57 @@ pub(crate) mod builder { } } - impl Builder { + impl Builder { + /// Creates a new production builder. pub fn production() -> Self { Self::default() } - pub fn with_db(self, db: Database) -> Builder { + /// Creates a new production builder with default configs: [`ComputeConfig`], [`ProcessorConfig`]. + #[cfg(test)] + pub fn production_with_defaults(db: Database) -> Builder { + Self::production() + .db(db) + .compute_config(ComputeConfig::without_quarantine()) + .processor_config(ProcessorConfig::default()) + } + } + + impl Builder { + pub fn compute_config(self, config: ComputeConfig) -> Builder { Builder { - db: Some(db), + config: Some(config), + db: self.db, + processor_config: self.processor_config, _state: PhantomData, - ..Default::default() } } - - #[cfg(test)] - pub fn with_defaults(self, db: Database) -> Builder { - self.with_db(db) - .with_compute_config(ComputeConfig::without_quarantine()) - .with_processor_config(ProcessorConfig::default()) - } } - impl Builder { - pub fn with_compute_config(self, config: ComputeConfig) -> Builder { + impl Builder { + pub fn db(self, db: Database) -> Builder { Builder { - db: self.db, - config: Some(config), + config: self.config, + db: Some(db), + processor_config: self.processor_config, _state: PhantomData, - ..Default::default() } } } - impl Builder { - pub fn with_processor_config(self, config: ProcessorConfig) -> Builder { + impl Builder { + pub fn processor_config(self, config: ProcessorConfig) -> Builder { Builder { - db: self.db, config: self.config, + db: self.db, processor_config: Some(config), _state: PhantomData, } } } - impl Builder { + impl Builder { + /// Creates the [`ComputeService`] from a production builder. pub fn build(self) -> Result { let db = self.db.unwrap(); let config = self.config.unwrap(); diff --git a/ethexe/compute/src/tests.rs b/ethexe/compute/src/tests.rs index e38db3e05bb..277cb9c9745 100644 --- a/ethexe/compute/src/tests.rs +++ b/ethexe/compute/src/tests.rs @@ -166,8 +166,7 @@ impl TestEnv { mark_as_not_prepared(&mut chain); chain = chain.setup(&db); - let compute = ComputeServiceBuilder::production() - .with_defaults(db.clone()) + let compute = ComputeServiceBuilder::production_with_defaults(db.clone()) .build() .unwrap(); diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index ccd9380de87..0704ec2548d 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -373,9 +373,9 @@ impl Service { chunk_size: config.node.chunk_processing_threads, }; let compute = ComputeServiceBuilder::production() - .with_db(db.clone()) - .with_compute_config(compute_config) - .with_processor_config(processor_config) + .db(db.clone()) + .compute_config(compute_config) + .processor_config(processor_config) .build() .with_context(|| "failed to build compute service")?; diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index 99c3782e722..aa5b7d34236 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -887,9 +887,9 @@ impl Node { ); let compute = ComputeServiceBuilder::production() - .with_db(self.db.clone()) - .with_compute_config(self.compute_config) - .with_processor_config(ProcessorConfig::default()) + .db(self.db.clone()) + .compute_config(self.compute_config) + .processor_config(ProcessorConfig::default()) .build() .unwrap(); From a97cf4c0726ec72d418f93ef5d8c4facf9a23263 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Thu, 26 Feb 2026 16:19:20 +0300 Subject: [PATCH 19/59] transfer promise for signing to consensus service --- ethexe/consensus/src/connect/mod.rs | 9 +++++- ethexe/consensus/src/lib.rs | 7 ++++- ethexe/consensus/src/validator/mod.rs | 25 +++++++++++++++- ethexe/consensus/src/validator/producer.rs | 16 +++++++++- ethexe/runtime/common/src/lib.rs | 2 +- ethexe/service/src/lib.rs | 34 ++++++++++------------ 6 files changed, 70 insertions(+), 23 deletions(-) diff --git a/ethexe/consensus/src/connect/mod.rs b/ethexe/consensus/src/connect/mod.rs index ba9f17f7c54..1db849d9f5d 100644 --- a/ethexe/consensus/src/connect/mod.rs +++ b/ethexe/consensus/src/connect/mod.rs @@ -30,7 +30,7 @@ use ethexe_common::{ Address, Announce, HashOf, SimpleBlockData, consensus::{VerifiedAnnounce, VerifiedValidationRequest}, db::OnChainStorageRO, - injected::SignedInjectedTransaction, + injected::{Promise, SignedInjectedTransaction}, network::{AnnouncesRequest, AnnouncesResponse}, }; use ethexe_db::Database; @@ -285,6 +285,13 @@ impl ConsensusService for ConnectService { Ok(()) } + fn receive_promise_for_signing(&mut self, promise: Promise) -> Result<()> { + tracing::error!( + "Connected consensus node receives the promise for signing, but it not responsible for promises providing: promise={promise:?}" + ); + Ok(()) + } + fn receive_injected_transaction(&mut self, tx: SignedInjectedTransaction) -> Result<()> { // In "connect-node" we do not process injected transactions. tracing::trace!("Received injected transaction: {tx:?}. Ignoring it."); diff --git a/ethexe/consensus/src/lib.rs b/ethexe/consensus/src/lib.rs index 3ca2c586cae..58de9477818 100644 --- a/ethexe/consensus/src/lib.rs +++ b/ethexe/consensus/src/lib.rs @@ -36,7 +36,7 @@ use anyhow::Result; use ethexe_common::{ Announce, Digest, HashOf, SimpleBlockData, consensus::{BatchCommitmentValidationReply, VerifiedAnnounce, VerifiedValidationRequest}, - injected::SignedInjectedTransaction, + injected::{Promise, SignedInjectedTransaction, SignedPromise}, network::{AnnouncesRequest, AnnouncesResponse, SignedValidatorMessage}, }; use futures::{Stream, stream::FusedStream}; @@ -76,6 +76,9 @@ pub trait ConsensusService: /// Process a received producer announce fn receive_announce(&mut self, announce: VerifiedAnnounce) -> Result<()>; + /// Receives the raw promise for signing. + fn receive_promise_for_signing(&mut self, promise: Promise) -> Result<()>; + /// Process a received validation request fn receive_validation_request(&mut self, request: VerifiedValidationRequest) -> Result<()>; @@ -119,6 +122,8 @@ pub enum ConsensusEvent { /// Informational event: commitment was successfully submitted #[from] CommitmentSubmitted(CommitmentSubmitted), + #[from] + SignedPromise(SignedPromise), /// Informational event: during service processing, a warning situation was detected Warning(String), } diff --git a/ethexe/consensus/src/validator/mod.rs b/ethexe/consensus/src/validator/mod.rs index 3a8f207a9cc..3ffad54fbed 100644 --- a/ethexe/consensus/src/validator/mod.rs +++ b/ethexe/consensus/src/validator/mod.rs @@ -58,7 +58,7 @@ use ethexe_common::{ consensus::{VerifiedAnnounce, VerifiedValidationRequest}, db::OnChainStorageRO, ecdsa::PublicKey, - injected::SignedInjectedTransaction, + injected::{Promise, SignedInjectedTransaction}, network::AnnouncesResponse, }; use ethexe_db::Database; @@ -218,6 +218,10 @@ impl ConsensusService for ValidatorService { self.update_inner(|inner| inner.process_announce(announce)) } + fn receive_promise_for_signing(&mut self, promise: Promise) -> Result<()> { + self.update_inner(|inner| inner.process_raw_promise(promise)) + } + fn receive_validation_request(&mut self, batch: VerifiedValidationRequest) -> Result<()> { self.update_inner(|inner| inner.process_validation_request(batch)) } @@ -322,6 +326,10 @@ where DefaultProcessing::announce_from_producer(self, announce) } + fn process_raw_promise(self, promise: Promise) -> Result { + DefaultProcessing::promise_for_signing(self, promise) + } + fn process_validation_request( self, request: VerifiedValidationRequest, @@ -410,6 +418,10 @@ impl StateHandler for ValidatorState { delegate_call!(self => process_announce(verified_announce)) } + fn process_raw_promise(self, promise: Promise) -> Result { + delegate_call!(self => process_raw_promise(promise)) + } + fn process_validation_request( self, request: VerifiedValidationRequest, @@ -465,6 +477,17 @@ impl DefaultProcessing { Ok(s) } + fn promise_for_signing( + s: impl Into, + promise: Promise, + ) -> Result { + let mut s = s.into(); + s.warning(format!( + "unexpected promise for signing: promise={promise:?}" + )); + Ok(s) + } + fn announce_from_producer( s: impl Into, announce: VerifiedAnnounce, diff --git a/ethexe/consensus/src/validator/producer.rs b/ethexe/consensus/src/validator/producer.rs index 8293c80d3f6..b630a6af5d0 100644 --- a/ethexe/consensus/src/validator/producer.rs +++ b/ethexe/consensus/src/validator/producer.rs @@ -28,7 +28,7 @@ use anyhow::{Result, anyhow}; use derive_more::{Debug, Display}; use ethexe_common::{ Announce, HashOf, SimpleBlockData, ValidatorsVec, db::BlockMetaStorageRO, - gear::BatchCommitment, network::ValidatorMessage, + gear::BatchCommitment, injected::Promise, network::ValidatorMessage, }; use ethexe_service_utils::Timer; use futures::{FutureExt, future::BoxFuture}; @@ -104,6 +104,20 @@ impl StateHandler for Producer { } } + fn process_raw_promise(mut self, promise: Promise) -> Result { + let tx_hash = promise.tx_hash; + + let signed_promise = + self.ctx + .core + .signer + .signed_message(self.ctx.core.pub_key, promise, None)?; + self.ctx.output(signed_promise); + + tracing::trace!("consensus sign promise for transaction-hash={tx_hash}"); + Ok(self.into()) + } + fn poll_next_state(mut self, cx: &mut Context<'_>) -> Result<(Poll<()>, ValidatorState)> { match &mut self.state { State::Delay { timer: Some(timer) } => { diff --git a/ethexe/runtime/common/src/lib.rs b/ethexe/runtime/common/src/lib.rs index 74b6fee9fae..040475f04d5 100644 --- a/ethexe/runtime/common/src/lib.rs +++ b/ethexe/runtime/common/src/lib.rs @@ -79,7 +79,7 @@ pub struct ProcessQueueContext { pub gas_allowance: GasAllowanceCounter, pub block_info: BlockInfo, // TODO: fix the naming - /// Wether should compute service produce promises + /// Whether should compute service produce promises pub should_produce_promises: bool, } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 0704ec2548d..dae441a6854 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -45,7 +45,7 @@ use ethexe_rpc::{RpcEvent, RpcServer}; use ethexe_service_utils::{OptionFuture as _, OptionStreamNext as _}; use futures::{StreamExt, stream::FuturesUnordered}; use gprimitives::{ActorId, CodeId, H256}; -use gsigner::secp256k1::{Address, PrivateKey, PublicKey, Secp256k1SignerExt, Signer}; +use gsigner::secp256k1::{Address, PrivateKey, PublicKey, Signer}; use std::{ collections::{BTreeSet, HashMap}, num::NonZero, @@ -457,7 +457,7 @@ impl Service { mut blob_loader, mut compute, mut consensus, - signer, + signer: _signer, mut prometheus, rpc, fast_sync: _, @@ -554,22 +554,7 @@ impl Service { // Nothing } ComputeEvent::Promise(promise) => { - // TODO: think to send the promise to consensus service - if let Some(pub_key) = validator_pub_key { - let signed_promise = signer.signed_message(pub_key, promise, None)?; - - if rpc.is_none() && network.is_none() { - panic!("Promise without network or rpc"); - } - - if let Some(rpc) = &rpc { - rpc.provide_promise(signed_promise.clone()); - } - - if let Some(network) = &mut network { - network.publish_promise(signed_promise); - } - } + consensus.receive_promise_for_signing(promise)?; } }, Event::Network(event) => { @@ -665,6 +650,19 @@ impl Service { ConsensusEvent::ComputeAnnounce(announce, should_produce_promises) => { compute.compute_announce(announce, should_produce_promises) } + ConsensusEvent::SignedPromise(signed_promise) => { + if rpc.is_none() && network.is_none() { + panic!("Promise without network or rpc"); + } + + if let Some(rpc) = &rpc { + rpc.provide_promise(signed_promise.clone()); + } + + if let Some(network) = &mut network { + network.publish_promise(signed_promise); + } + } ConsensusEvent::PublishMessage(message) => { let Some(network) = network.as_mut() else { continue; From 51ede2a6d8ae7e60b60d4b1e0717ea720fc21e73 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Thu, 26 Feb 2026 16:46:51 +0300 Subject: [PATCH 20/59] return tests in compute service --- ethexe/compute/src/compute.rs | 116 +++++++++++++++++----------------- ethexe/compute/src/service.rs | 21 ++++-- 2 files changed, 74 insertions(+), 63 deletions(-) diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 090eced4493..c75a9556b76 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -306,61 +306,61 @@ fn find_canonical_events_post_quarantine( .ok_or(ComputeError::BlockEventsNotFound(block_hash)) } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::tests::{MockProcessor, PROCESSOR_RESULT}; -// use ethexe_common::{gear::StateTransition, mock::*}; -// use gprimitives::{ActorId, H256}; - -// #[tokio::test] -// #[ntest::timeout(3000)] -// async fn test_compute() { -// gear_utils::init_default_logger(); - -// let db = Database::memory(); -// let block_hash = BlockChain::mock(1).setup(&db).blocks[1].hash; -// let config = ComputeConfig::without_quarantine(); -// let mut service = ComputeSubService::new(config, db.clone(), MockProcessor); - -// let announce = Announce { -// block_hash, -// parent: db.latest_data().unwrap().genesis_announce_hash, -// gas_allowance: Some(100), -// injected_transactions: vec![], -// }; -// let announce_hash = announce.to_hash(); - -// // Create non-empty processor result with transitions -// let non_empty_result = FinalizedBlockTransitions { -// transitions: vec![StateTransition { -// actor_id: ActorId::from([1; 32]), -// new_state_hash: H256::from([2; 32]), -// value_to_receive: 100, -// ..Default::default() -// }], -// ..Default::default() -// }; - -// // Set the PROCESSOR_RESULT to return non-empty result -// PROCESSOR_RESULT.with_borrow_mut(|r| *r = non_empty_result.clone()); -// service.receive_announce_to_compute(announce); - -// assert_eq!(service.next().await.unwrap().announce_hash, announce_hash); - -// // Verify block was marked as computed -// assert!(db.announce_meta(announce_hash).computed); - -// // Verify transitions were stored in DB -// let stored_transitions = db.announce_outcome(announce_hash).unwrap(); -// assert_eq!(stored_transitions.len(), 1); -// assert_eq!(stored_transitions[0].actor_id, ActorId::from([1; 32])); -// assert_eq!(stored_transitions[0].new_state_hash, H256::from([2; 32])); - -// // Verify latest announce -// assert_eq!( -// db.latest_data().unwrap().computed_announce_hash, -// announce_hash -// ); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::{MockProcessor, PROCESSOR_RESULT}; + use ethexe_common::{gear::StateTransition, mock::*}; + use gprimitives::{ActorId, H256}; + + #[tokio::test] + #[ntest::timeout(3000)] + async fn test_compute() { + gear_utils::init_default_logger(); + + let db = Database::memory(); + let block_hash = BlockChain::mock(1).setup(&db).blocks[1].hash; + let config = ComputeConfig::without_quarantine(); + let mut service = ComputeSubService::new(config, db.clone(), MockProcessor); + + let announce = Announce { + block_hash, + parent: db.latest_data().unwrap().genesis_announce_hash, + gas_allowance: Some(100), + injected_transactions: vec![], + }; + let announce_hash = announce.to_hash(); + + // Create non-empty processor result with transitions + let non_empty_result = FinalizedBlockTransitions { + transitions: vec![StateTransition { + actor_id: ActorId::from([1; 32]), + new_state_hash: H256::from([2; 32]), + value_to_receive: 100, + ..Default::default() + }], + ..Default::default() + }; + + // Set the PROCESSOR_RESULT to return non-empty result + PROCESSOR_RESULT.with_borrow_mut(|r| *r = non_empty_result.clone()); + service.receive_announce_to_compute(announce, false); + + assert_eq!(service.next().await.unwrap(), announce_hash); + + // Verify block was marked as computed + assert!(db.announce_meta(announce_hash).computed); + + // Verify transitions were stored in DB + let stored_transitions = db.announce_outcome(announce_hash).unwrap(); + assert_eq!(stored_transitions.len(), 1); + assert_eq!(stored_transitions[0].actor_id, ActorId::from([1; 32])); + assert_eq!(stored_transitions[0].new_state_hash, H256::from([2; 32])); + + // Verify latest announce + assert_eq!( + db.latest_data().unwrap().computed_announce_hash, + announce_hash + ); + } +} diff --git a/ethexe/compute/src/service.rs b/ethexe/compute/src/service.rs index 71543db7937..0af2a61ad6b 100644 --- a/ethexe/compute/src/service.rs +++ b/ethexe/compute/src/service.rs @@ -137,11 +137,12 @@ pub(crate) mod builder { /// Provides the easy construction the [`ComputeService`] for both /// testing and production environments. #[derive(Default)] - pub struct Builder { + pub struct Builder { config: Option, db: Option, processor_config: Option, - _state: PhantomData<(Env, C, D, P)>, + + _state: PhantomData<(Env, Config, DB, Processor)>, } /// Mock builder uses defaults when fields are None; type-states are fixed to Set. @@ -195,6 +196,8 @@ pub(crate) mod builder { } } + // Important: production builder allows to set variable only once. + impl Builder { pub fn compute_config(self, config: ComputeConfig) -> Builder { Builder { @@ -228,12 +231,11 @@ pub(crate) mod builder { } } + /// Implementation for builder with all filled fields. impl Builder { /// Creates the [`ComputeService`] from a production builder. pub fn build(self) -> Result { - let db = self.db.unwrap(); - let config = self.config.unwrap(); - let processor_config = self.processor_config.unwrap(); + let (config, db, processor_config) = self.into_parts_unchecked(); let (promise_out_tx, promise_receiver) = mpsc::unbounded_channel(); let processor = @@ -246,6 +248,15 @@ pub(crate) mod builder { promise_receiver: Some(promise_receiver), }) } + + /// Reconstructs builder into parts.. + fn into_parts_unchecked(self) -> (ComputeConfig, Database, ProcessorConfig) { + ( + self.config.unwrap(), + self.db.unwrap(), + self.processor_config.unwrap(), + ) + } } } From 4d62e9a407ffda42f059b700d45bf6465dd8a5f1 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Thu, 26 Feb 2026 17:34:23 +0300 Subject: [PATCH 21/59] redesign to PromisePolicy enum --- ethexe/cli/src/commands/check.rs | 4 +- ethexe/common/src/primitives.rs | 10 ++ ethexe/compute/src/compute.rs | 26 ++--- ethexe/compute/src/service.rs | 28 +++--- ethexe/compute/src/tests.rs | 5 +- ethexe/consensus/src/connect/mod.rs | 8 +- ethexe/consensus/src/lib.rs | 4 +- ethexe/consensus/src/validator/producer.rs | 8 +- ethexe/consensus/src/validator/subordinate.rs | 23 +++-- ethexe/processor/src/handling/overlaid.rs | 6 +- ethexe/processor/src/handling/run.rs | 29 +++--- ethexe/processor/src/lib.rs | 14 +-- ethexe/processor/src/tests.rs | 98 ++++++++++++++++--- ethexe/runtime/common/src/lib.rs | 8 +- ethexe/service/src/lib.rs | 4 +- 15 files changed, 181 insertions(+), 94 deletions(-) diff --git a/ethexe/cli/src/commands/check.rs b/ethexe/cli/src/commands/check.rs index a02af5a766e..555917fc4c9 100644 --- a/ethexe/cli/src/commands/check.rs +++ b/ethexe/cli/src/commands/check.rs @@ -19,7 +19,7 @@ use anyhow::{Context, Result, anyhow, ensure}; use clap::Parser; use ethexe_common::{ - Announce, HashOf, SimpleBlockData, + Announce, HashOf, PromisePolicy, SimpleBlockData, db::{AnnounceStorageRO, LatestData, LatestDataStorageRO, OnChainStorageRO}, }; use ethexe_db::{ @@ -242,7 +242,7 @@ impl Checker { let executable = ethexe_compute::prepare_executable_for_announce( db, announce, - false, + PromisePolicy::Disabled, canonical_quarantine, ) .context("Unable to preparing announce data for execution")?; diff --git a/ethexe/common/src/primitives.rs b/ethexe/common/src/primitives.rs index 5ac1bd3f2b9..0c82c5103fa 100644 --- a/ethexe/common/src/primitives.rs +++ b/ethexe/common/src/primitives.rs @@ -129,6 +129,16 @@ impl ToDigest for Announce { } } +/// [`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 { + /// Emits promises in execution process. + Enabled, + // Do not emit promises in execution process. + #[default] + Disabled, +} + #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, Default, Encode, Decode, TypeInfo)] #[cfg_attr(feature = "std", derive(serde::Serialize))] pub struct StateHashWithQueueSize { diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index c75a9556b76..931dbf0d327 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -18,7 +18,7 @@ use crate::{ComputeError, ProcessorExt, Result, service::SubService}; use ethexe_common::{ - Announce, HashOf, SimpleBlockData, + Announce, HashOf, PromisePolicy, SimpleBlockData, db::{ AnnounceStorageRO, AnnounceStorageRW, BlockMetaStorageRO, CodesStorageRW, LatestDataStorageRO, LatestDataStorageRW, OnChainStorageRO, @@ -79,7 +79,7 @@ pub struct ComputeSubService { config: ComputeConfig, metrics: Metrics, - input: VecDeque<(Announce, bool)>, + input: VecDeque<(Announce, PromisePolicy)>, computation: Option, } @@ -98,9 +98,9 @@ impl ComputeSubService

{ pub fn receive_announce_to_compute( &mut self, announce: Announce, - should_produce_promises: bool, + promise_policy: PromisePolicy, ) { - self.input.push_back((announce, should_produce_promises)); + self.input.push_back((announce, promise_policy)); } async fn compute( @@ -108,7 +108,7 @@ impl ComputeSubService

{ config: ComputeConfig, mut processor: P, announce: Announce, - should_produce_promises: bool, + promise_policy: PromisePolicy, ) -> Result> { let announce_hash = announce.to_hash(); let block_hash = announce.block_hash; @@ -146,7 +146,7 @@ impl ComputeSubService

{ config, announce_hash, announce, - should_produce_promises, + promise_policy, ) .await?; } @@ -160,12 +160,12 @@ impl ComputeSubService

{ config: ComputeConfig, announce_hash: HashOf, announce: Announce, - should_produce_promises: bool, + promise_policy: PromisePolicy, ) -> Result> { let executable = prepare_executable_for_announce( db, announce, - should_produce_promises, + promise_policy, config.canonical_quarantine(), )?; let processing_result = processor.process_announce(executable).await?; @@ -204,7 +204,7 @@ impl SubService for ComputeSubService

{ fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll> { if self.computation.is_none() - && let Some((announce, should_produce_promises)) = self.input.pop_front() + && let Some((announce, promise_policy)) = self.input.pop_front() { self.computation = Some(future_timing::timed( Self::compute( @@ -212,7 +212,7 @@ impl SubService for ComputeSubService

{ self.config, self.processor.clone(), announce, - should_produce_promises, + promise_policy, ) .boxed(), )); @@ -236,7 +236,7 @@ impl SubService for ComputeSubService

{ pub fn prepare_executable_for_announce( db: &Database, announce: Announce, - should_produce_promises: bool, + promise_policy: PromisePolicy, canonical_quarantine: u8, ) -> Result { let block_hash = announce.block_hash; @@ -269,7 +269,7 @@ pub fn prepare_executable_for_announce( .collect(), gas_allowance: announce.gas_allowance, events, - should_produce_promises, + promise_policy, }) } @@ -344,7 +344,7 @@ mod tests { // Set the PROCESSOR_RESULT to return non-empty result PROCESSOR_RESULT.with_borrow_mut(|r| *r = non_empty_result.clone()); - service.receive_announce_to_compute(announce, false); + service.receive_announce_to_compute(announce, PromisePolicy::Disabled); assert_eq!(service.next().await.unwrap(), announce_hash); diff --git a/ethexe/compute/src/service.rs b/ethexe/compute/src/service.rs index 0af2a61ad6b..613a4e6ed08 100644 --- a/ethexe/compute/src/service.rs +++ b/ethexe/compute/src/service.rs @@ -24,7 +24,7 @@ use crate::{ compute::{ComputeConfig, ComputeSubService}, prepare::PrepareSubService, }; -use ethexe_common::{Announce, CodeAndIdUnchecked, injected::Promise}; +use ethexe_common::{Announce, CodeAndIdUnchecked, PromisePolicy, injected::Promise}; use ethexe_db::Database; use ethexe_processor::{Processor, ProcessorConfig}; use futures::{Stream, stream::FusedStream}; @@ -54,9 +54,9 @@ impl ComputeService

{ self.prepare_sub_service.receive_block_to_prepare(block); } - pub fn compute_announce(&mut self, announce: Announce, should_produce_promises: bool) { + pub fn compute_announce(&mut self, announce: Announce, promise_policy: PromisePolicy) { self.compute_sub_service - .receive_announce_to_compute(announce, should_produce_promises); + .receive_announce_to_compute(announce, promise_policy); } } @@ -64,6 +64,16 @@ impl Stream for ComputeService

{ type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + if let Some(ref mut receiver) = self.promise_receiver + && let Poll::Ready(maybe_promise) = receiver.poll_recv(cx) + { + return Poll::Ready(Some( + maybe_promise + .map(Into::into) + .ok_or_else(|| ComputeError::PromiseSenderDropped), + )); + } + if let Poll::Ready(result) = self.codes_sub_service.poll_next(cx) { match result { Ok(code_id) => { @@ -84,16 +94,6 @@ impl Stream for ComputeService

{ return Poll::Ready(Some(result.map(ComputeEvent::AnnounceComputed))); }; - if let Some(ref mut receiver) = self.promise_receiver - && let Poll::Ready(maybe_promise) = receiver.poll_recv(cx) - { - return Poll::Ready(Some( - maybe_promise - .map(Into::into) - .ok_or_else(|| ComputeError::PromiseSenderDropped), - )); - } - Poll::Pending } } @@ -317,7 +317,7 @@ mod tests { injected_transactions: vec![], }; let announce_hash = announce.to_hash(); - service.compute_announce(announce, false); + service.compute_announce(announce, PromisePolicy::Disabled); // Poll service to process the block let event = service.next().await.unwrap().unwrap(); diff --git a/ethexe/compute/src/tests.rs b/ethexe/compute/src/tests.rs index 277cb9c9745..a57670e6ee9 100644 --- a/ethexe/compute/src/tests.rs +++ b/ethexe/compute/src/tests.rs @@ -18,7 +18,7 @@ use super::*; use ethexe_common::{ - CodeBlobInfo, + CodeBlobInfo, PromisePolicy, db::*, events::{ BlockEvent, RouterEvent, @@ -219,7 +219,8 @@ impl TestEnv { async fn compute_and_assert_announce(&mut self, announce: Announce) { let announce_hash = announce.to_hash(); - self.compute.compute_announce(announce.clone(), false); + self.compute + .compute_announce(announce.clone(), PromisePolicy::Disabled); let event = self .compute diff --git a/ethexe/consensus/src/connect/mod.rs b/ethexe/consensus/src/connect/mod.rs index 1db849d9f5d..2d66ba7d107 100644 --- a/ethexe/consensus/src/connect/mod.rs +++ b/ethexe/consensus/src/connect/mod.rs @@ -27,7 +27,7 @@ use crate::{ }; use anyhow::{Result, anyhow}; use ethexe_common::{ - Address, Announce, HashOf, SimpleBlockData, + Address, Announce, HashOf, PromisePolicy, SimpleBlockData, consensus::{VerifiedAnnounce, VerifiedValidationRequest}, db::OnChainStorageRO, injected::{Promise, SignedInjectedTransaction}, @@ -161,8 +161,10 @@ impl ConnectService { AnnounceStatus::Accepted(announce_hash) => { self.output .push_back(ConsensusEvent::AnnounceAccepted(announce_hash)); - self.output - .push_back(ConsensusEvent::ComputeAnnounce(announce, false)); + self.output.push_back(ConsensusEvent::ComputeAnnounce( + announce, + PromisePolicy::Disabled, + )); } } diff --git a/ethexe/consensus/src/lib.rs b/ethexe/consensus/src/lib.rs index 58de9477818..fef6dba598c 100644 --- a/ethexe/consensus/src/lib.rs +++ b/ethexe/consensus/src/lib.rs @@ -34,7 +34,7 @@ use anyhow::Result; use ethexe_common::{ - Announce, Digest, HashOf, SimpleBlockData, + Announce, Digest, HashOf, PromisePolicy, SimpleBlockData, consensus::{BatchCommitmentValidationReply, VerifiedAnnounce, VerifiedValidationRequest}, injected::{Promise, SignedInjectedTransaction, SignedPromise}, network::{AnnouncesRequest, AnnouncesResponse, SignedValidatorMessage}, @@ -112,7 +112,7 @@ pub enum ConsensusEvent { /// Announce from producer was rejected AnnounceRejected(HashOf), /// Outer service have to compute announce - ComputeAnnounce(Announce, bool), + ComputeAnnounce(Announce, PromisePolicy), /// Outer service have to publish signed message #[from] PublishMessage(SignedValidatorMessage), diff --git a/ethexe/consensus/src/validator/producer.rs b/ethexe/consensus/src/validator/producer.rs index b630a6af5d0..d919364d492 100644 --- a/ethexe/consensus/src/validator/producer.rs +++ b/ethexe/consensus/src/validator/producer.rs @@ -27,7 +27,7 @@ use crate::{ use anyhow::{Result, anyhow}; use derive_more::{Debug, Display}; use ethexe_common::{ - Announce, HashOf, SimpleBlockData, ValidatorsVec, db::BlockMetaStorageRO, + Announce, HashOf, PromisePolicy, SimpleBlockData, ValidatorsVec, db::BlockMetaStorageRO, gear::BatchCommitment, injected::Promise, network::ValidatorMessage, }; use ethexe_service_utils::Timer; @@ -231,8 +231,10 @@ impl Producer { self.state = State::WaitingAnnounceComputed(announce_hash); self.ctx .output(ConsensusEvent::PublishMessage(message.into())); - self.ctx - .output(ConsensusEvent::ComputeAnnounce(announce, true)); + self.ctx.output(ConsensusEvent::ComputeAnnounce( + announce, + PromisePolicy::Enabled, + )); Ok(self.into()) } diff --git a/ethexe/consensus/src/validator/subordinate.rs b/ethexe/consensus/src/validator/subordinate.rs index 95edb19e670..2c2a550238c 100644 --- a/ethexe/consensus/src/validator/subordinate.rs +++ b/ethexe/consensus/src/validator/subordinate.rs @@ -28,7 +28,7 @@ use crate::{ use anyhow::Result; use derive_more::{Debug, Display}; use ethexe_common::{ - Address, Announce, HashOf, SimpleBlockData, + Address, Announce, HashOf, PromisePolicy, SimpleBlockData, consensus::{VerifiedAnnounce, VerifiedValidationRequest}, }; use std::mem; @@ -173,8 +173,10 @@ impl Subordinate { AnnounceStatus::Accepted(announce_hash) => { self.ctx .output(ConsensusEvent::AnnounceAccepted(announce_hash)); - self.ctx - .output(ConsensusEvent::ComputeAnnounce(announce, false)); + self.ctx.output(ConsensusEvent::ComputeAnnounce( + announce, + PromisePolicy::Disabled, + )); self.state = State::WaitingAnnounceComputed { announce_hash }; Ok(self.into()) @@ -235,7 +237,7 @@ mod tests { s.context().output, vec![ ConsensusEvent::AnnounceAccepted(announce1.data().to_hash()), - ConsensusEvent::ComputeAnnounce(announce1.data().clone(), false) + ConsensusEvent::ComputeAnnounce(announce1.data().clone(), PromisePolicy::Disabled) ] ); // announce2 must stay in pending events, because it's not from current producer. @@ -295,7 +297,7 @@ mod tests { s.context().output, vec![ ConsensusEvent::AnnounceAccepted(announce.data().to_hash()), - ConsensusEvent::ComputeAnnounce(announce.data().clone(), false) + ConsensusEvent::ComputeAnnounce(announce.data().clone(), PromisePolicy::Disabled) ] ); assert_eq!(s.context().pending_events.len(), MAX_PENDING_EVENTS); @@ -324,7 +326,7 @@ mod tests { s.context().output, vec![ ConsensusEvent::AnnounceAccepted(announce.data().to_hash()), - ConsensusEvent::ComputeAnnounce(announce.data().clone(), false) + ConsensusEvent::ComputeAnnounce(announce.data().clone(), PromisePolicy::Disabled) ] ); @@ -337,7 +339,7 @@ mod tests { s.context().output, vec![ ConsensusEvent::AnnounceAccepted(announce.data().to_hash()), - ConsensusEvent::ComputeAnnounce(announce.data().clone(), false) + ConsensusEvent::ComputeAnnounce(announce.data().clone(), PromisePolicy::Disabled) ] ); } @@ -366,7 +368,7 @@ mod tests { s.context().output, vec![ ConsensusEvent::AnnounceAccepted(announce.data().to_hash()), - ConsensusEvent::ComputeAnnounce(announce.data().clone(), false) + ConsensusEvent::ComputeAnnounce(announce.data().clone(), PromisePolicy::Disabled) ] ); @@ -401,7 +403,10 @@ mod tests { s.context().output, vec![ ConsensusEvent::AnnounceAccepted(producer_announce.data().to_hash()), - ConsensusEvent::ComputeAnnounce(producer_announce.data().clone(), false) + ConsensusEvent::ComputeAnnounce( + producer_announce.data().clone(), + PromisePolicy::Disabled + ) ] ); assert_eq!(s.context().pending_events, vec![alice_announce.into()]); diff --git a/ethexe/processor/src/handling/overlaid.rs b/ethexe/processor/src/handling/overlaid.rs index e3a03a95a35..2ab1c4393c9 100644 --- a/ethexe/processor/src/handling/overlaid.rs +++ b/ethexe/processor/src/handling/overlaid.rs @@ -25,7 +25,9 @@ use crate::{ host::InstanceCreator, }; use core_processor::common::JournalNote; -use ethexe_common::{BlockHeader, db::CodesStorageRO, gear::MessageType, injected::Promise}; +use ethexe_common::{ + BlockHeader, PromisePolicy, db::CodesStorageRO, gear::MessageType, injected::Promise, +}; use ethexe_db::{CASDatabase, Database}; use ethexe_runtime_common::{InBlockTransitions, TransitionController}; use gear_core::{ @@ -86,7 +88,7 @@ impl OverlaidRunContext { gas_allowance, chunk_size, block_header, - false, + PromisePolicy::Disabled, None, ), base_program, diff --git a/ethexe/processor/src/handling/run.rs b/ethexe/processor/src/handling/run.rs index 028f1c50b68..b8605844552 100644 --- a/ethexe/processor/src/handling/run.rs +++ b/ethexe/processor/src/handling/run.rs @@ -113,7 +113,7 @@ use chunk_execution_processing::ChunkJournalsProcessingOutput; use chunks_splitting::ActorStateHashWithQueueSize; use core_processor::common::JournalNote; use ethexe_common::{ - BlockHeader, StateHashWithQueueSize, + BlockHeader, PromisePolicy, StateHashWithQueueSize, db::CodesStorageRO, gear::{CHUNK_PROCESSING_GAS_LIMIT, MessageType}, injected::Promise, @@ -251,8 +251,8 @@ pub(super) trait RunContext { } // TODO: add docs - fn should_produce_promises(&self) -> bool { - false + fn promise_policy(&self) -> PromisePolicy { + PromisePolicy::default() } /// Checks whether the run must be stopped early without executing the rest chunks. @@ -272,10 +272,7 @@ pub(crate) struct CommonRunContext { pub(crate) gas_allowance_counter: GasAllowanceCounter, pub(crate) chunk_size: usize, pub(crate) block_header: BlockHeader, - - // TODO think about removing this - pub(crate) should_produce_promises: bool, - + pub(crate) promise_policy: PromisePolicy, pub(crate) promise_out_tx: Option>, } @@ -288,7 +285,7 @@ impl CommonRunContext { gas_allowance: u64, chunk_size: usize, block_header: BlockHeader, - should_produce_promises: bool, + promise_policy: PromisePolicy, promise_out_tx: Option>, ) -> Self { CommonRunContext { @@ -298,7 +295,7 @@ impl CommonRunContext { gas_allowance_counter: GasAllowanceCounter::new(gas_allowance), chunk_size, block_header, - should_produce_promises, + promise_policy, promise_out_tx, } } @@ -362,8 +359,8 @@ impl RunContext for CommonRunContext { states(&self.transitions, processing_queue_type) } - fn should_produce_promises(&self) -> bool { - self.should_produce_promises + fn promise_policy(&self) -> PromisePolicy { + self.promise_policy } fn chunk_size(&self) -> usize { @@ -546,7 +543,7 @@ mod chunk_execution_spawn { executor: InstanceWrapper, db: Box, gas_allowance_for_chunk: u64, - should_produce_promises: bool, + promise_policy: PromisePolicy, } let (db, _, gas_allowance_counter) = ctx.borrow_inner(); @@ -571,7 +568,7 @@ mod chunk_execution_spawn { executor, db: db.clone_boxed(), gas_allowance_for_chunk, - should_produce_promises: ctx.should_produce_promises(), + promise_policy: ctx.promise_policy(), }) }) .collect::>>()?; @@ -595,7 +592,7 @@ mod chunk_execution_spawn { mut executor, db, gas_allowance_for_chunk, - should_produce_promises, + promise_policy, }| { let (jn, new_state_hash, gas_spent) = executor .run( @@ -610,7 +607,7 @@ mod chunk_execution_spawn { gas_allowance_for_chunk, ), block_info, - should_produce_promises, + promise_policy, }, promise_out_tx.clone(), ) @@ -775,7 +772,7 @@ mod tests { gas_allowance_counter: GasAllowanceCounter::new(1_000_000), chunk_size: CHUNK_PROCESSING_THREADS, block_header: BlockHeader::dummy(3), - should_produce_promises: false, + promise_policy: PromisePolicy::Disabled, promise_out_tx: None, }; diff --git a/ethexe/processor/src/lib.rs b/ethexe/processor/src/lib.rs index 0fd5fc837a5..bea1ca029e8 100644 --- a/ethexe/processor/src/lib.rs +++ b/ethexe/processor/src/lib.rs @@ -20,7 +20,7 @@ use core::num::NonZero; use ethexe_common::{ - CodeAndIdUnchecked, ProgramStates, Schedule, SimpleBlockData, + CodeAndIdUnchecked, ProgramStates, PromisePolicy, Schedule, SimpleBlockData, ecdsa::VerifiedData, events::{BlockRequestEvent, MirrorRequestEvent, mirror::MessageQueueingRequestedEvent}, injected::{InjectedTransaction, Promise}, @@ -201,7 +201,7 @@ impl Processor { injected_transactions, gas_allowance, events, - should_produce_promises, + promise_policy, } = executable; let mut transitions = @@ -212,7 +212,7 @@ impl Processor { if let Some(gas_allowance) = gas_allowance { transitions = self - .process_queues(transitions, block, gas_allowance, should_produce_promises) + .process_queues(transitions, block, gas_allowance, promise_policy) .await?; } transitions = self.process_tasks(transitions); @@ -253,7 +253,7 @@ impl Processor { transitions: InBlockTransitions, block: SimpleBlockData, gas_allowance: u64, - should_produce_promises: bool, + promise_policy: PromisePolicy, ) -> Result { CommonRunContext::new( self.db.clone(), @@ -262,7 +262,7 @@ impl Processor { gas_allowance, self.config.chunk_size, block.header, - should_produce_promises, + promise_policy, self.promise_out_tx.clone(), ) .run() @@ -315,7 +315,7 @@ pub struct ExecutableData { pub injected_transactions: Vec>, pub gas_allowance: Option, pub events: Vec, - pub should_produce_promises: bool, + pub promise_policy: PromisePolicy, } #[cfg(test)] @@ -328,7 +328,7 @@ impl Default for ExecutableData { injected_transactions: vec![], gas_allowance: Some(ethexe_common::DEFAULT_BLOCK_GAS_LIMIT), events: vec![], - should_produce_promises: false, + promise_policy: Default::default(), } } } diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index 35390eda342..1373c17b839 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -332,7 +332,12 @@ async fn ping_pong() { .expect("failed to send message"); let to_users = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) + .process_queues( + handler.transitions, + block1, + DEFAULT_BLOCK_GAS_LIMIT, + PromisePolicy::Disabled, + ) .await .unwrap() .current_messages(); @@ -446,7 +451,12 @@ async fn async_and_ping() { .expect("failed to send message"); let transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) + .process_queues( + handler.transitions, + block1, + DEFAULT_BLOCK_GAS_LIMIT, + PromisePolicy::Disabled, + ) .await .unwrap(); @@ -542,7 +552,12 @@ async fn many_waits() { handler.transitions = processor.process_tasks(handler.transitions); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) + .process_queues( + handler.transitions, + block1, + DEFAULT_BLOCK_GAS_LIMIT, + PromisePolicy::Disabled, + ) .await .unwrap(); assert_eq!( @@ -567,7 +582,12 @@ async fn many_waits() { .expect("failed to send message"); } handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) + .process_queues( + handler.transitions, + block1, + DEFAULT_BLOCK_GAS_LIMIT, + PromisePolicy::Disabled, + ) .await .unwrap(); assert_eq!( @@ -589,7 +609,12 @@ async fn many_waits() { let transitions = InBlockTransitions::new(wake_block.header.height, states, schedule); let transitions = processor.process_tasks(transitions); let transitions = processor - .process_queues(transitions, wake_block, DEFAULT_BLOCK_GAS_LIMIT, false) + .process_queues( + transitions, + wake_block, + DEFAULT_BLOCK_GAS_LIMIT, + PromisePolicy::Disabled, + ) .await .unwrap(); @@ -896,7 +921,12 @@ async fn injected_ping_pong() { .expect("failed to send message"); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) + .process_queues( + handler.transitions, + block1, + DEFAULT_BLOCK_GAS_LIMIT, + PromisePolicy::Disabled, + ) .await .unwrap(); @@ -920,7 +950,12 @@ async fn injected_ping_pong() { .expect("failed to send message"); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, true) + .process_queues( + handler.transitions, + block1, + DEFAULT_BLOCK_GAS_LIMIT, + PromisePolicy::Enabled, + ) .await .unwrap(); @@ -1004,7 +1039,12 @@ async fn injected_prioritized_over_canonical() { .expect("failed to send message"); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) + .process_queues( + handler.transitions, + block1, + DEFAULT_BLOCK_GAS_LIMIT, + PromisePolicy::Disabled, + ) .await .unwrap(); @@ -1035,7 +1075,12 @@ async fn injected_prioritized_over_canonical() { } let transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, true) + .process_queues( + handler.transitions, + block1, + DEFAULT_BLOCK_GAS_LIMIT, + PromisePolicy::Enabled, + ) .await .unwrap(); @@ -1113,7 +1158,12 @@ async fn executable_balance_charged() { .expect("failed to send message"); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) + .process_queues( + handler.transitions, + block1, + DEFAULT_BLOCK_GAS_LIMIT, + PromisePolicy::Disabled, + ) .await .unwrap(); @@ -1201,7 +1251,12 @@ async fn executable_balance_injected_panic_not_charged() { ) .unwrap(); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) + .process_queues( + handler.transitions, + block1, + DEFAULT_BLOCK_GAS_LIMIT, + PromisePolicy::Disabled, + ) .await .unwrap(); let init_balance = handler.program_state(actor_id).executable_balance; @@ -1213,7 +1268,12 @@ async fn executable_balance_injected_panic_not_charged() { .handle_injected_transaction(user_id, panic_tx.clone()) .unwrap(); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, true) + .process_queues( + handler.transitions, + block1, + DEFAULT_BLOCK_GAS_LIMIT, + PromisePolicy::Enabled, + ) .await .unwrap(); @@ -1256,7 +1316,12 @@ async fn executable_balance_injected_panic_not_charged() { ) .expect("failed to send message"); let transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) + .process_queues( + handler.transitions, + block1, + DEFAULT_BLOCK_GAS_LIMIT, + PromisePolicy::Disabled, + ) .await .unwrap(); @@ -1323,7 +1388,12 @@ async fn insufficient_executable_balance_still_charged() { .expect("failed to send message"); handler.transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, false) + .process_queues( + handler.transitions, + block1, + DEFAULT_BLOCK_GAS_LIMIT, + PromisePolicy::Disabled, + ) .await .unwrap(); diff --git a/ethexe/runtime/common/src/lib.rs b/ethexe/runtime/common/src/lib.rs index 040475f04d5..2a8b3f968ff 100644 --- a/ethexe/runtime/common/src/lib.rs +++ b/ethexe/runtime/common/src/lib.rs @@ -29,7 +29,7 @@ use core_processor::{ configs::{BlockConfig, SyscallName}, }; use ethexe_common::{ - HashOf, + HashOf, PromisePolicy, gear::{CHUNK_PROCESSING_GAS_LIMIT, MessageType}, injected::Promise, }; @@ -78,9 +78,7 @@ pub struct ProcessQueueContext { pub code_metadata: CodeMetadata, pub gas_allowance: GasAllowanceCounter, pub block_info: BlockInfo, - // TODO: fix the naming - /// Whether should compute service produce promises - pub should_produce_promises: bool, + pub promise_policy: PromisePolicy, } pub trait RuntimeInterface: Storage { @@ -236,7 +234,7 @@ where }; // TODO: move to separate function - if ctx.should_produce_promises && is_promise_required { + if ctx.promise_policy.is_enabled() && is_promise_required { for note in journal.iter() { if let JournalNote::SendDispatch { message_id, diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index dae441a6854..ac5b37e0b11 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -647,8 +647,8 @@ impl Service { } } Event::Consensus(event) => match event { - ConsensusEvent::ComputeAnnounce(announce, should_produce_promises) => { - compute.compute_announce(announce, should_produce_promises) + ConsensusEvent::ComputeAnnounce(announce, promise_policy) => { + compute.compute_announce(announce, promise_policy) } ConsensusEvent::SignedPromise(signed_promise) => { if rpc.is_none() && network.is_none() { From 6b11846784e4862fdf60b8fb4b1219ef811650a2 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 27 Feb 2026 12:15:11 +0300 Subject: [PATCH 22/59] refactoring inside ethexe/runtime | remove unresolved TODOs --- ethexe/compute/src/service.rs | 4 +- ethexe/processor/src/host/api/mod.rs | 1 + ethexe/processor/src/host/api/promise.rs | 3 +- ethexe/processor/src/host/threads.rs | 3 +- ethexe/processor/src/lib.rs | 6 -- ethexe/processor/src/tests.rs | 1 + ethexe/runtime/common/src/lib.rs | 73 +++++++++++++++--------- 7 files changed, 51 insertions(+), 40 deletions(-) diff --git a/ethexe/compute/src/service.rs b/ethexe/compute/src/service.rs index 613a4e6ed08..0ecf42052f9 100644 --- a/ethexe/compute/src/service.rs +++ b/ethexe/compute/src/service.rs @@ -180,6 +180,8 @@ pub(crate) mod builder { } } + /// Production [`ComputeService`] builder. + /// Important: production builder allows to set variable only once. impl Builder { /// Creates a new production builder. pub fn production() -> Self { @@ -196,8 +198,6 @@ pub(crate) mod builder { } } - // Important: production builder allows to set variable only once. - impl Builder { pub fn compute_config(self, config: ComputeConfig) -> Builder { Builder { diff --git a/ethexe/processor/src/host/api/mod.rs b/ethexe/processor/src/host/api/mod.rs index 03d0fc95300..793f2e8b9e5 100644 --- a/ethexe/processor/src/host/api/mod.rs +++ b/ethexe/processor/src/host/api/mod.rs @@ -28,6 +28,7 @@ pub mod lazy_pages; pub mod logging; pub mod promise; pub mod sandbox; + pub struct MemoryWrap(Memory); // TODO: return results for mem accesses. diff --git a/ethexe/processor/src/host/api/promise.rs b/ethexe/processor/src/host/api/promise.rs index ab68264a0cb..58578772a43 100644 --- a/ethexe/processor/src/host/api/promise.rs +++ b/ethexe/processor/src/host/api/promise.rs @@ -28,10 +28,9 @@ pub fn link(linker: &mut Linker) -> Result<(), wasmtime::Error> { } fn publish_promise(caller: Caller<'_, StoreData>, promise_ptr_len: i64) { - let memory = MemoryWrap(caller.data().memory()); - threads::with_params(|params| { if let Some(ref sender) = params.promise_out_tx { + let memory = MemoryWrap(caller.data().memory()); let promise = memory.decode_by_val(&caller, promise_ptr_len); match sender.send(promise) { diff --git a/ethexe/processor/src/host/threads.rs b/ethexe/processor/src/host/threads.rs index c64b975d978..4f2e48a1b0d 100644 --- a/ethexe/processor/src/host/threads.rs +++ b/ethexe/processor/src/host/threads.rs @@ -42,7 +42,6 @@ thread_local! { pub struct ThreadParams { pub db: Box, pub state_hash: H256, - /// TODO: think about using [`mpsc::sync_channel`] instead of [`mpsc::channel`]. pub promise_out_tx: Option>, pages_registry_cache: Option, pages_regions_cache: Option>, @@ -113,9 +112,9 @@ pub fn set( PARAMS.set(Some(ThreadParams { db, state_hash, + promise_out_tx, pages_registry_cache: None, pages_regions_cache: None, - promise_out_tx, })) } diff --git a/ethexe/processor/src/lib.rs b/ethexe/processor/src/lib.rs index bea1ca029e8..31098aeba9d 100644 --- a/ethexe/processor/src/lib.rs +++ b/ethexe/processor/src/lib.rs @@ -108,11 +108,6 @@ pub struct Processor { config: ProcessorConfig, db: Database, creator: InstanceCreator, - // TODO: Think about adding the - // #[cfg(test)] - // promise_out_tx: Option>, - // #[cfg(not(test))] - // promise_out_tx: mpsc::UnboundedSender, promise_out_tx: Option>, } @@ -197,7 +192,6 @@ impl Processor { block, program_states, schedule, - // TODO: remove injected_transactions, gas_allowance, events, diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index 1373c17b839..3c8d9ff3ea1 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -36,6 +36,7 @@ use gear_core::{ use gear_core_errors::SimpleExecutionError; use gprimitives::{ActorId, MessageId}; use parity_scale_codec::Encode; +use tokio::sync::mpsc; use utils::*; mod utils { diff --git a/ethexe/runtime/common/src/lib.rs b/ethexe/runtime/common/src/lib.rs index 2a8b3f968ff..0ad7ea9b47c 100644 --- a/ethexe/runtime/common/src/lib.rs +++ b/ethexe/runtime/common/src/lib.rs @@ -42,7 +42,7 @@ use gear_core::{ rpc::ReplyInfo, }; use gear_lazy_pages_common::LazyPagesInterface; -use gprimitives::H256; +use gprimitives::{H256, MessageId}; use gsys::{GasMultiplier, Percent}; use journal::RuntimeJournalHandler; use state::{Dispatch, ProgramState, Storage}; @@ -235,34 +235,9 @@ where // TODO: move to separate function if ctx.promise_policy.is_enabled() && is_promise_required { - for note in journal.iter() { - if let JournalNote::SendDispatch { - message_id, - dispatch, - .. - } = note - && *message_id == dispatch_id - && dispatch.kind().is_reply() - { - let code = dispatch - .reply_details() - .map(|d| d.to_reply_code()) - .expect("reply details must exists for reply dispatch"); - - let reply = ReplyInfo { - value: dispatch.value(), - code, - payload: dispatch.message().payload_bytes().to_vec(), - }; - - let tx_hash = unsafe { HashOf::new(dispatch_id.into_bytes().into()) }; - let promise = Promise { reply, tx_hash }; - - ri.publish_promise(&promise); - break; - } - } + process_journal_for_injected_dispatch(ri, &journal, dispatch_id); } + let (unhandled_journal_notes, new_state_hash) = handler.handle_journal(journal); mega_journal.push((unhandled_journal_notes, message_type, call_reply)); @@ -284,6 +259,48 @@ where (mega_journal, gas_spent) } +/// Finds in [`process_dispatch`]'s the [`JournalNote::SendDispatch`] note and builds from it +/// a [`ReplyInfo`] and [`Promise`] for injected message. +fn process_journal_for_injected_dispatch( + ri: &RI, + journal: &[JournalNote], + dispatch_id: MessageId, +) where + RI: RuntimeInterface, +{ + for note in journal.iter() { + if let JournalNote::SendDispatch { + message_id, + dispatch, + .. + } = note + && *message_id == dispatch_id + && dispatch.kind().is_reply() + { + let Some(code) = dispatch.reply_details().map(|d| d.to_reply_code()) else { + log::error!( + "received reply dispatch without reply details; protocol invariant violated: \ + initial_dispatch_id={dispatch_id:?}, send_dispatch={dispatch:?}" + ); + continue; + }; + + let reply = ReplyInfo { + value: dispatch.value(), + code, + payload: dispatch.message().payload_bytes().to_vec(), + }; + + // SAFE: because of protocol logic - injected message id constructs from injected transaction hash. + let tx_hash = unsafe { HashOf::new(dispatch_id.into_bytes().into()) }; + let promise = Promise { reply, tx_hash }; + + ri.publish_promise(&promise); + break; + } + } +} + fn process_dispatch( dispatch: Dispatch, block_config: &BlockConfig, From 7f9f46c1a33c6253532bec4e5f8357cd7ab49a7f Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 27 Feb 2026 18:14:12 +0300 Subject: [PATCH 23/59] AnnouncePromisesStream inside compute service --- ethexe/cli/src/commands/check.rs | 16 +- ethexe/common/src/primitives.rs | 4 + ethexe/compute/src/compute.rs | 362 ++++++++++++++------- ethexe/compute/src/lib.rs | 17 +- ethexe/compute/src/service.rs | 203 ++---------- ethexe/compute/src/tests.rs | 6 +- ethexe/consensus/src/connect/mod.rs | 9 +- ethexe/consensus/src/lib.rs | 6 +- ethexe/consensus/src/validator/mod.rs | 27 +- ethexe/consensus/src/validator/producer.rs | 30 +- ethexe/processor/src/handling/overlaid.rs | 5 +- ethexe/processor/src/handling/run.rs | 29 +- ethexe/processor/src/lib.rs | 28 +- ethexe/processor/src/tests.rs | 122 ++----- ethexe/rpc/src/lib.rs | 1 - ethexe/runtime/common/src/lib.rs | 1 - ethexe/service/src/lib.rs | 16 +- ethexe/service/src/tests/mod.rs | 4 +- ethexe/service/src/tests/utils/env.rs | 12 +- 19 files changed, 431 insertions(+), 467 deletions(-) diff --git a/ethexe/cli/src/commands/check.rs b/ethexe/cli/src/commands/check.rs index 555917fc4c9..18a10d22809 100644 --- a/ethexe/cli/src/commands/check.rs +++ b/ethexe/cli/src/commands/check.rs @@ -19,7 +19,7 @@ use anyhow::{Context, Result, anyhow, ensure}; use clap::Parser; use ethexe_common::{ - Announce, HashOf, PromisePolicy, SimpleBlockData, + Announce, HashOf, SimpleBlockData, db::{AnnounceStorageRO, LatestData, LatestDataStorageRO, OnChainStorageRO}, }; use ethexe_db::{ @@ -227,7 +227,7 @@ impl Checker { None }; - let processor = Processor::with_config(ProcessorConfig { chunk_size }, db.clone(), None) + let processor = Processor::with_config(ProcessorConfig { chunk_size }, db.clone()) .context("failed to create processor")?; // Iterate back: from `head` announce to `bottom` announce @@ -239,16 +239,12 @@ impl Checker { let announce_parent_hash = announce.parent; let mut processor = processor.clone().overlaid(); - let executable = ethexe_compute::prepare_executable_for_announce( - db, - announce, - PromisePolicy::Disabled, - canonical_quarantine, - ) - .context("Unable to preparing announce data for execution")?; + let executable = + ethexe_compute::prepare_executable_for_announce(db, announce, canonical_quarantine) + .context("Unable to preparing announce data for execution")?; let res = processor .as_mut() - .process_programs(executable) + .process_programs(executable, None) .await .context("failed to re-compute announce")?; diff --git a/ethexe/common/src/primitives.rs b/ethexe/common/src/primitives.rs index 0c82c5103fa..cc6ee1d7697 100644 --- a/ethexe/common/src/primitives.rs +++ b/ethexe/common/src/primitives.rs @@ -139,6 +139,10 @@ pub enum PromisePolicy { Disabled, } +// Producer -> (announce, PromisePolicy::Enabled) +// Subordinate -> (announce, PromisePolicy::Disabled) +// ConnectNode -> (announce, PromisePolicy::Disabled) + #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, Default, Encode, Decode, TypeInfo)] #[cfg_attr(feature = "std", derive(serde::Serialize))] pub struct StateHashWithQueueSize { diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 931dbf0d327..3557ef843d6 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -16,7 +16,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::{ComputeError, ProcessorExt, Result, service::SubService}; +use crate::{ComputeError, ComputeEvent, ProcessorExt, Result, service::SubService}; use ethexe_common::{ Announce, HashOf, PromisePolicy, SimpleBlockData, db::{ @@ -24,16 +24,18 @@ use ethexe_common::{ LatestDataStorageRO, LatestDataStorageRW, OnChainStorageRO, }, events::BlockEvent, + injected::Promise, }; use ethexe_db::Database; use ethexe_processor::ExecutableData; use ethexe_runtime_common::FinalizedBlockTransitions; -use futures::{FutureExt, future::BoxFuture}; +use futures::{FutureExt, StreamExt, future::BoxFuture}; use gprimitives::H256; use std::{ collections::VecDeque, task::{Context, Poll}, }; +use tokio::sync::mpsc; #[derive(Debug, Clone, Copy)] pub struct ComputeConfig { @@ -81,6 +83,7 @@ pub struct ComputeSubService { input: VecDeque<(Announce, PromisePolicy)>, computation: Option, + promises_stream: Option, } impl ComputeSubService

{ @@ -92,6 +95,7 @@ impl ComputeSubService

{ metrics: Metrics::default(), input: VecDeque::new(), computation: None, + promises_stream: None, } } @@ -108,7 +112,7 @@ impl ComputeSubService

{ config: ComputeConfig, mut processor: P, announce: Announce, - promise_policy: PromisePolicy, + promise_out_tx: Option>, ) -> Result> { let announce_hash = announce.to_hash(); let block_hash = announce.block_hash; @@ -117,41 +121,30 @@ impl ComputeSubService

{ return Err(ComputeError::BlockNotPrepared(block_hash)); } - let mut parent_hash = announce.parent; - let mut announces_chain: VecDeque<_> = [(announce_hash, announce)].into(); - loop { - if db.announce_meta(parent_hash).computed { - break; - } + let not_computed_announces = utils::find_parent_not_computed_announces(&announce, &db)?; - let parent_announce = db - .announce(parent_hash) - .ok_or(ComputeError::AnnounceNotFound(parent_hash))?; - - let next_parent_hash = parent_announce.parent; - announces_chain.push_front((parent_hash, parent_announce)); + if !not_computed_announces.is_empty() { + log::trace!( + "compute-sub-service: announce({announce_hash}) contains a {} previous not computed announce, start computing...", + not_computed_announces.len() + ); - parent_hash = next_parent_hash; - } - - if announces_chain.is_empty() { - log::trace!("All announces are already computed"); - return Ok(announce_hash); - } - - for (announce_hash, announce) in announces_chain { - Self::compute_one( - &db, - &mut processor, - config, - announce_hash, - announce, - promise_policy, - ) - .await?; + for (announce_hash, announce) in not_computed_announces { + // Set the promise_out_tx = None, because we want to receive the promises only from target announce. + Self::compute_one(&db, &mut processor, config, announce_hash, announce, None) + .await?; + } } - Ok(announce_hash) + Self::compute_one( + &db, + &mut processor, + config, + announce_hash, + announce, + promise_out_tx, + ) + .await } async fn compute_one( @@ -160,15 +153,13 @@ impl ComputeSubService

{ config: ComputeConfig, announce_hash: HashOf, announce: Announce, - promise_policy: PromisePolicy, + promise_out_tx: Option>, ) -> Result> { - let executable = prepare_executable_for_announce( - db, - announce, - promise_policy, - config.canonical_quarantine(), - )?; - let processing_result = processor.process_announce(executable).await?; + let executable = + utils::prepare_executable_for_announce(db, announce, config.canonical_quarantine())?; + let processing_result = processor + .process_announce(executable, promise_out_tx) + .await?; let FinalizedBlockTransitions { transitions, @@ -200,24 +191,51 @@ impl ComputeSubService

{ } impl SubService for ComputeSubService

{ - type Output = HashOf; + type Output = ComputeEvent; fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll> { if self.computation.is_none() && let Some((announce, promise_policy)) = self.input.pop_front() { + let maybe_promise_out_tx = match promise_policy { + PromisePolicy::Enabled => { + let (sender, receiver) = mpsc::unbounded_channel(); + self.promises_stream = Some(utils::AnnouncePromisesStream::new( + receiver, + announce.to_hash(), + )); + + Some(sender) + } + PromisePolicy::Disabled => None, + }; + self.computation = Some(future_timing::timed( Self::compute( self.db.clone(), self.config, self.processor.clone(), announce, - promise_policy, + maybe_promise_out_tx, ) .boxed(), )); } + if let Some(ref mut stream) = self.promises_stream + && let Poll::Ready(maybe_event) = stream.poll_next_unpin(cx) + { + match maybe_event { + Some(event) => return Poll::Ready(Ok(event)), + None => { + self.promises_stream = None; + log::warn!( + "compute-sub-service: promises stream shouldn't ended, because the channel can not be dropped, something happen in processor" + ) + } + } + } + if let Some(computation) = &mut self.computation && let Poll::Ready(timing_result) = computation.poll_unpin(cx) { @@ -225,85 +243,164 @@ impl SubService for ComputeSubService

{ self.metrics .announce_processing_latency .record((timing.busy() + timing.idle()).as_secs_f64()); + self.computation = None; - return Poll::Ready(result); + self.promises_stream = None; + + return Poll::Ready(result.map(Into::into)); } Poll::Pending } } -pub fn prepare_executable_for_announce( - db: &Database, - announce: Announce, - promise_policy: PromisePolicy, - canonical_quarantine: u8, -) -> Result { - let block_hash = announce.block_hash; - - let matured_events = - find_canonical_events_post_quarantine(db, block_hash, canonical_quarantine)?; - - let events = matured_events - .into_iter() - .filter_map(|event| event.to_request()) - .collect(); - - Ok(ExecutableData { - block: SimpleBlockData { - hash: block_hash, - header: db - .block_header(block_hash) - .ok_or(ComputeError::BlockHeaderNotFound(block_hash))?, - }, - program_states: db - .announce_program_states(announce.parent) - .ok_or(ComputeError::ProgramStatesNotFound(announce.parent))?, - schedule: db - .announce_schedule(announce.parent) - .ok_or(ComputeError::ScheduleNotFound(announce.parent))?, - injected_transactions: announce - .injected_transactions +/// The utils for [`ComputeSubService`]. +pub(crate) mod utils { + use super::*; + use futures::Stream; + use std::pin::Pin; + + /// The stream of promises from announce execution. + pub(super) struct AnnouncePromisesStream { + receiver: mpsc::UnboundedReceiver, + announce_hash: HashOf, + } + + impl AnnouncePromisesStream { + pub fn new( + receiver: mpsc::UnboundedReceiver, + announce_hash: HashOf, + ) -> Self { + Self { + receiver, + announce_hash, + } + } + } + + impl Stream for AnnouncePromisesStream { + type Item = ComputeEvent; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let maybe_promise = futures::ready!(self.receiver.poll_recv(cx)); + Poll::Ready( + maybe_promise.map(|promise| ComputeEvent::Promise(promise, self.announce_hash)), + ) + } + } + + pub fn prepare_executable_for_announce( + db: &Database, + announce: Announce, + canonical_quarantine: u8, + ) -> Result { + let block_hash = announce.block_hash; + + let matured_events = + find_canonical_events_post_quarantine(db, block_hash, canonical_quarantine)?; + + let events = matured_events .into_iter() - .map(|tx| tx.into_verified()) - .collect(), - gas_allowance: announce.gas_allowance, - events, - promise_policy, - }) -} + .filter_map(|event| event.to_request()) + .collect(); + + Ok(ExecutableData { + block: SimpleBlockData { + hash: block_hash, + header: db + .block_header(block_hash) + .ok_or(ComputeError::BlockHeaderNotFound(block_hash))?, + }, + program_states: db + .announce_program_states(announce.parent) + .ok_or(ComputeError::ProgramStatesNotFound(announce.parent))?, + schedule: db + .announce_schedule(announce.parent) + .ok_or(ComputeError::ScheduleNotFound(announce.parent))?, + injected_transactions: announce + .injected_transactions + .into_iter() + .map(|tx| tx.into_verified()) + .collect(), + gas_allowance: announce.gas_allowance, + events, + }) + } -/// Finds events from Ethereum in database which can be processed in current block. -fn find_canonical_events_post_quarantine( - db: &Database, - mut block_hash: H256, - canonical_quarantine: u8, -) -> Result> { - let genesis_block = db - .latest_data() - .ok_or_else(|| ComputeError::LatestDataNotFound)? - .genesis_block_hash; - - let mut block_header = db - .block_header(block_hash) - .ok_or_else(|| ComputeError::BlockHeaderNotFound(block_hash))?; - - for _ in 0..canonical_quarantine { - if block_hash == genesis_block { - return Ok(Default::default()); + pub(super) fn find_parent_not_computed_announces( + announce: &Announce, + db: &DB, + ) -> Result, Announce)>> + where + DB: AnnounceStorageRO + LatestDataStorageRO, + { + let mut parent_hash = announce.parent; + let mut announces_chain = VecDeque::new(); + let start_announce_hash = db + .latest_data() + .ok_or_else(|| ComputeError::LatestDataNotFound)? + .start_announce_hash; + + loop { + if db.announce_meta(parent_hash).computed { + break; + } + + let parent_announce = db + .announce(parent_hash) + .ok_or(ComputeError::AnnounceNotFound(parent_hash))?; + + let next_parent_hash = parent_announce.parent; + announces_chain.push_front((parent_hash, parent_announce)); + + // This was a start announce, no need to go further. + if parent_hash == start_announce_hash { + break; + } + + parent_hash = next_parent_hash; } - let parent_hash = block_header.parent_hash; - let parent_header = db - .block_header(parent_hash) - .ok_or(ComputeError::BlockHeaderNotFound(parent_hash))?; + Ok(announces_chain) - block_hash = parent_hash; - block_header = parent_header; + // if announces_chain.is_empty() { + // log::trace!("All announces are already computed"); + // return Ok(announce_hash); + // } } - db.block_events(block_hash) - .ok_or(ComputeError::BlockEventsNotFound(block_hash)) + /// Finds events from Ethereum in database which can be processed in current block. + pub fn find_canonical_events_post_quarantine( + db: &Database, + mut block_hash: H256, + canonical_quarantine: u8, + ) -> Result> { + let genesis_block = db + .latest_data() + .ok_or_else(|| ComputeError::LatestDataNotFound)? + .genesis_block_hash; + + let mut block_header = db + .block_header(block_hash) + .ok_or_else(|| ComputeError::BlockHeaderNotFound(block_hash))?; + + for _ in 0..canonical_quarantine { + if block_hash == genesis_block { + return Ok(Default::default()); + } + + let parent_hash = block_header.parent_hash; + let parent_header = db + .block_header(parent_hash) + .ok_or(ComputeError::BlockHeaderNotFound(parent_hash))?; + + block_hash = parent_hash; + block_header = parent_header; + } + + db.block_events(block_hash) + .ok_or(ComputeError::BlockEventsNotFound(block_hash)) + } } #[cfg(test)] @@ -346,7 +443,10 @@ mod tests { PROCESSOR_RESULT.with_borrow_mut(|r| *r = non_empty_result.clone()); service.receive_announce_to_compute(announce, PromisePolicy::Disabled); - assert_eq!(service.next().await.unwrap(), announce_hash); + assert_eq!( + service.next().await.unwrap().unwrap_announce_computed(), + announce_hash + ); // Verify block was marked as computed assert!(db.announce_meta(announce_hash).computed); @@ -363,4 +463,46 @@ mod tests { announce_hash ); } + + #[test] + fn find_not_computed_announces_work_correctly() { + const BLOCKCHAIN_LEN: usize = 10; + + let db = Database::memory(); + let mut blockchain = BlockChain::mock(BLOCKCHAIN_LEN as u32).setup(&db); + + // Setup announces except the head to not-computed state. + blockchain + .announces + .iter_mut() + .enumerate() + .for_each(|(idx, (announce_hash, _))| { + // Set the announces to not computed state + if idx != BLOCKCHAIN_LEN - 1 { + db.mutate_announce_meta(*announce_hash, |meta| { + meta.computed = false; + }); + } + }); + + let expected_not_computed_announces = (0..BLOCKCHAIN_LEN - 1) + .map(|idx| blockchain.block_top_announce(idx).announce.to_hash()) + .collect::>(); + + let head_announce = blockchain + .block_top_announce(BLOCKCHAIN_LEN - 1) + .announce + .clone(); + let not_computed_announces = utils::find_parent_not_computed_announces(&head_announce, &db) + .unwrap() + .into_iter() + .map(|v| v.0) + .collect::>(); + + assert_eq!( + expected_not_computed_announces.len(), + not_computed_announces.len() + ); + assert_eq!(expected_not_computed_announces, not_computed_announces); + } } diff --git a/ethexe/compute/src/lib.rs b/ethexe/compute/src/lib.rs index 6085349a8b5..2fba5c6dd96 100644 --- a/ethexe/compute/src/lib.rs +++ b/ethexe/compute/src/lib.rs @@ -16,14 +16,17 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +pub use compute::{ + ComputeConfig, ComputeSubService, + utils::{find_canonical_events_post_quarantine, prepare_executable_for_announce}, +}; use ethexe_common::{Announce, CodeAndIdUnchecked, HashOf, injected::Promise}; use ethexe_processor::{ExecutableData, ProcessedCodeInfo, Processor, ProcessorError}; use ethexe_runtime_common::FinalizedBlockTransitions; use gprimitives::{CodeId, H256}; +pub use service::ComputeService; use std::collections::HashSet; - -pub use compute::{ComputeConfig, ComputeSubService, prepare_executable_for_announce}; -pub use service::{ComputeService, builder::Builder as ComputeServiceBuilder}; +use tokio::sync::mpsc; mod codes; mod compute; @@ -44,7 +47,7 @@ pub enum ComputeEvent { CodeProcessed(CodeId), BlockPrepared(H256), AnnounceComputed(HashOf), - Promise(Promise), + Promise(Promise, HashOf), } #[derive(thiserror::Error, Debug)] @@ -98,6 +101,7 @@ pub trait ProcessorExt: Sized + Unpin + Send + Clone + 'static { fn process_announce( &mut self, executable: ExecutableData, + promise_out_tx: Option>, ) -> impl Future> + Send; fn process_upload_code(&mut self, code_and_id: CodeAndIdUnchecked) -> Result; @@ -107,8 +111,11 @@ impl ProcessorExt for Processor { async fn process_announce( &mut self, executable: ExecutableData, + promise_out_tx: Option>, ) -> Result { - self.process_programs(executable).await.map_err(Into::into) + self.process_programs(executable, promise_out_tx) + .await + .map_err(Into::into) } fn process_upload_code( diff --git a/ethexe/compute/src/service.rs b/ethexe/compute/src/service.rs index 0ecf42052f9..0a082784738 100644 --- a/ethexe/compute/src/service.rs +++ b/ethexe/compute/src/service.rs @@ -19,28 +19,53 @@ #[cfg(test)] use crate::tests::MockProcessor; use crate::{ - ComputeError, ComputeEvent, ProcessorExt, Result, + ComputeEvent, ProcessorExt, Result, codes::CodesSubService, compute::{ComputeConfig, ComputeSubService}, prepare::PrepareSubService, }; -use ethexe_common::{Announce, CodeAndIdUnchecked, PromisePolicy, injected::Promise}; +use ethexe_common::{Announce, CodeAndIdUnchecked, PromisePolicy}; use ethexe_db::Database; -use ethexe_processor::{Processor, ProcessorConfig}; +use ethexe_processor::Processor; use futures::{Stream, stream::FusedStream}; use gprimitives::H256; use std::{ - marker::PhantomData, pin::Pin, task::{Context, Poll}, }; -use tokio::sync::mpsc; pub struct ComputeService { codes_sub_service: CodesSubService

, prepare_sub_service: PrepareSubService, compute_sub_service: ComputeSubService

, - promise_receiver: Option>, +} + +impl ComputeService

{ + /// Creates new compute service. + pub fn new(config: ComputeConfig, db: Database, processor: P) -> Self { + Self { + prepare_sub_service: PrepareSubService::new(db.clone()), + compute_sub_service: ComputeSubService::new(config, db.clone(), processor.clone()), + codes_sub_service: CodesSubService::new(db, processor), + } + } +} + +#[cfg(test)] +impl ComputeService { + /// Creates the processor with default [`ComputeConfig::without_quarantine`] and [`Processor`] with default config. + pub fn new_with_defaults(db: Database) -> Self { + let config = ComputeConfig::without_quarantine(); + let processor = Processor::new(db.clone()).unwrap(); + Self::new(config, db, processor) + } +} + +#[cfg(test)] +impl ComputeService { + pub fn new_mock_processor(db: Database) -> Self { + Self::new(ComputeConfig::without_quarantine(), db, MockProcessor) + } } impl ComputeService

{ @@ -64,16 +89,6 @@ impl Stream for ComputeService

{ type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - if let Some(ref mut receiver) = self.promise_receiver - && let Poll::Ready(maybe_promise) = receiver.poll_recv(cx) - { - return Poll::Ready(Some( - maybe_promise - .map(Into::into) - .ok_or_else(|| ComputeError::PromiseSenderDropped), - )); - } - if let Poll::Ready(result) = self.codes_sub_service.poll_next(cx) { match result { Ok(code_id) => { @@ -90,8 +105,8 @@ impl Stream for ComputeService

{ return Poll::Ready(Some(result.map(ComputeEvent::from))); }; - if let Poll::Ready(result) = self.compute_sub_service.poll_next(cx) { - return Poll::Ready(Some(result.map(ComputeEvent::AnnounceComputed))); + if let Poll::Ready(event) = self.compute_sub_service.poll_next(cx) { + return Poll::Ready(Some(event)); }; Poll::Pending @@ -115,154 +130,8 @@ pub(crate) trait SubService: Unpin + Send + 'static { } } -/// Module provides a builder for [`ComputeService`]. -/// The [`builder::Builder`] must be used for both production and testing purposes. -pub(crate) mod builder { - use super::*; - - // Builder environments - #[cfg(test)] - #[derive(Default)] - pub struct Mock; - #[derive(Default)] - pub struct Production; - - // Builder states - #[derive(Default)] - pub struct Set; - #[derive(Default)] - pub struct Unset; - - /// A type-state builder for [`ComputeService`]. - /// Provides the easy construction the [`ComputeService`] for both - /// testing and production environments. - #[derive(Default)] - pub struct Builder { - config: Option, - db: Option, - processor_config: Option, - - _state: PhantomData<(Env, Config, DB, Processor)>, - } - - /// Mock builder uses defaults when fields are None; type-states are fixed to Set. - #[cfg(test)] - impl Builder { - /// Creates a new mock builder. - pub(crate) fn mock() -> Self { - Self::default() - } - - #[allow(unused)] - pub(crate) fn with_config(mut self, config: ComputeConfig) -> Self { - self.config = Some(config); - self - } - - #[allow(unused)] - pub(crate) fn with_db(mut self, db: Database) -> Self { - self.db = Some(db); - self - } - - /// Creates a [`ComputeService`] from a mock builder. - pub(crate) fn build(self) -> ComputeService { - let processor = MockProcessor; - let config = self.config.unwrap_or(ComputeConfig::without_quarantine()); - let db = self.db.unwrap_or(Database::memory()); - - ComputeService { - prepare_sub_service: PrepareSubService::new(db.clone()), - compute_sub_service: ComputeSubService::new(config, db.clone(), processor.clone()), - codes_sub_service: CodesSubService::new(db, processor), - promise_receiver: None, - } - } - } - - /// Production [`ComputeService`] builder. - /// Important: production builder allows to set variable only once. - impl Builder { - /// Creates a new production builder. - pub fn production() -> Self { - Self::default() - } - - /// Creates a new production builder with default configs: [`ComputeConfig`], [`ProcessorConfig`]. - #[cfg(test)] - pub fn production_with_defaults(db: Database) -> Builder { - Self::production() - .db(db) - .compute_config(ComputeConfig::without_quarantine()) - .processor_config(ProcessorConfig::default()) - } - } - - impl Builder { - pub fn compute_config(self, config: ComputeConfig) -> Builder { - Builder { - config: Some(config), - db: self.db, - processor_config: self.processor_config, - _state: PhantomData, - } - } - } - - impl Builder { - pub fn db(self, db: Database) -> Builder { - Builder { - config: self.config, - db: Some(db), - processor_config: self.processor_config, - _state: PhantomData, - } - } - } - - impl Builder { - pub fn processor_config(self, config: ProcessorConfig) -> Builder { - Builder { - config: self.config, - db: self.db, - processor_config: Some(config), - _state: PhantomData, - } - } - } - - /// Implementation for builder with all filled fields. - impl Builder { - /// Creates the [`ComputeService`] from a production builder. - pub fn build(self) -> Result { - let (config, db, processor_config) = self.into_parts_unchecked(); - - let (promise_out_tx, promise_receiver) = mpsc::unbounded_channel(); - let processor = - Processor::with_config(processor_config, db.clone(), Some(promise_out_tx))?; - - Ok(ComputeService { - prepare_sub_service: PrepareSubService::new(db.clone()), - compute_sub_service: ComputeSubService::new(config, db.clone(), processor.clone()), - codes_sub_service: CodesSubService::new(db, processor), - promise_receiver: Some(promise_receiver), - }) - } - - /// Reconstructs builder into parts.. - fn into_parts_unchecked(self) -> (ComputeConfig, Database, ProcessorConfig) { - ( - self.config.unwrap(), - self.db.unwrap(), - self.processor_config.unwrap(), - ) - } - } -} - #[cfg(test)] mod tests { - use crate::ComputeServiceBuilder; use super::*; use ethexe_common::{CodeAndIdUnchecked, db::*, mock::*}; @@ -277,7 +146,7 @@ mod tests { gear_utils::init_default_logger(); let db = DB::memory(); - let mut service = ComputeServiceBuilder::mock().with_db(db.clone()).build(); + let mut service = ComputeService::new_mock_processor(db.clone()); let chain = BlockChain::mock(1).setup(&db); let block = chain.blocks[1].to_simple().next_block().setup(&db); @@ -299,7 +168,7 @@ mod tests { gear_utils::init_default_logger(); let db = DB::memory(); - let mut service = ComputeServiceBuilder::mock().with_db(db.clone()).build(); + let mut service = ComputeService::new_mock_processor(db.clone()); let chain = BlockChain::mock(1).setup(&db); @@ -333,7 +202,7 @@ mod tests { gear_utils::init_default_logger(); let db = DB::memory(); - let mut service = ComputeServiceBuilder::mock().with_db(db.clone()).build(); + let mut service = ComputeService::new_mock_processor(db.clone()); // Create test code let code = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]; // Simple WASM header diff --git a/ethexe/compute/src/tests.rs b/ethexe/compute/src/tests.rs index a57670e6ee9..af9f4da2893 100644 --- a/ethexe/compute/src/tests.rs +++ b/ethexe/compute/src/tests.rs @@ -33,6 +33,7 @@ use gear_core::{ ids::prelude::CodeIdExt, }; use std::{cell::RefCell, collections::BTreeMap}; +use tokio::sync::mpsc; thread_local! { pub(crate) static PROCESSOR_RESULT: RefCell = const { RefCell::new( @@ -53,6 +54,7 @@ impl ProcessorExt for MockProcessor { async fn process_announce( &mut self, _executable: ExecutableData, + _promise_out_tx: Option>, ) -> Result { let result = PROCESSOR_RESULT.with_borrow(|r| r.clone()); PROCESSOR_RESULT.with_borrow_mut(|r| { @@ -166,9 +168,7 @@ impl TestEnv { mark_as_not_prepared(&mut chain); chain = chain.setup(&db); - let compute = ComputeServiceBuilder::production_with_defaults(db.clone()) - .build() - .unwrap(); + let compute = ComputeService::new_with_defaults(db.clone()); TestEnv { db, compute, chain } } diff --git a/ethexe/consensus/src/connect/mod.rs b/ethexe/consensus/src/connect/mod.rs index 2d66ba7d107..081be6187b0 100644 --- a/ethexe/consensus/src/connect/mod.rs +++ b/ethexe/consensus/src/connect/mod.rs @@ -287,9 +287,14 @@ impl ConsensusService for ConnectService { Ok(()) } - fn receive_promise_for_signing(&mut self, promise: Promise) -> Result<()> { + fn receive_promise_for_signing( + &mut self, + promise: Promise, + announce_hash: HashOf, + ) -> Result<()> { tracing::error!( - "Connected consensus node receives the promise for signing, but it not responsible for promises providing: promise={promise:?}" + "Connected consensus node receives the promise for signing, but it not responsible for promises providing: \ + promise={promise:?}, announce_hash={announce_hash}" ); Ok(()) } diff --git a/ethexe/consensus/src/lib.rs b/ethexe/consensus/src/lib.rs index fef6dba598c..77709c6a56c 100644 --- a/ethexe/consensus/src/lib.rs +++ b/ethexe/consensus/src/lib.rs @@ -77,7 +77,11 @@ pub trait ConsensusService: fn receive_announce(&mut self, announce: VerifiedAnnounce) -> Result<()>; /// Receives the raw promise for signing. - fn receive_promise_for_signing(&mut self, promise: Promise) -> Result<()>; + fn receive_promise_for_signing( + &mut self, + promise: Promise, + announce_hash: HashOf, + ) -> Result<()>; /// Process a received validation request fn receive_validation_request(&mut self, request: VerifiedValidationRequest) -> Result<()>; diff --git a/ethexe/consensus/src/validator/mod.rs b/ethexe/consensus/src/validator/mod.rs index 3ffad54fbed..fc1785374ec 100644 --- a/ethexe/consensus/src/validator/mod.rs +++ b/ethexe/consensus/src/validator/mod.rs @@ -218,8 +218,12 @@ impl ConsensusService for ValidatorService { self.update_inner(|inner| inner.process_announce(announce)) } - fn receive_promise_for_signing(&mut self, promise: Promise) -> Result<()> { - self.update_inner(|inner| inner.process_raw_promise(promise)) + fn receive_promise_for_signing( + &mut self, + promise: Promise, + announce_hash: HashOf, + ) -> Result<()> { + self.update_inner(|inner| inner.process_raw_promise(promise, announce_hash)) } fn receive_validation_request(&mut self, batch: VerifiedValidationRequest) -> Result<()> { @@ -326,8 +330,12 @@ where DefaultProcessing::announce_from_producer(self, announce) } - fn process_raw_promise(self, promise: Promise) -> Result { - DefaultProcessing::promise_for_signing(self, promise) + fn process_raw_promise( + self, + promise: Promise, + announce_hash: HashOf, + ) -> Result { + DefaultProcessing::promise_for_signing(self, promise, announce_hash) } fn process_validation_request( @@ -418,8 +426,12 @@ impl StateHandler for ValidatorState { delegate_call!(self => process_announce(verified_announce)) } - fn process_raw_promise(self, promise: Promise) -> Result { - delegate_call!(self => process_raw_promise(promise)) + fn process_raw_promise( + self, + promise: Promise, + announce_hash: HashOf, + ) -> Result { + delegate_call!(self => process_raw_promise(promise, announce_hash)) } fn process_validation_request( @@ -480,10 +492,11 @@ impl DefaultProcessing { fn promise_for_signing( s: impl Into, promise: Promise, + announce_hash: HashOf, ) -> Result { let mut s = s.into(); s.warning(format!( - "unexpected promise for signing: promise={promise:?}" + "unexpected promise for signing: promise={promise:?}, announce_hash={announce_hash:?}" )); Ok(s) } diff --git a/ethexe/consensus/src/validator/producer.rs b/ethexe/consensus/src/validator/producer.rs index d919364d492..f42bb9e9f01 100644 --- a/ethexe/consensus/src/validator/producer.rs +++ b/ethexe/consensus/src/validator/producer.rs @@ -104,18 +104,28 @@ impl StateHandler for Producer { } } - fn process_raw_promise(mut self, promise: Promise) -> Result { - let tx_hash = promise.tx_hash; + fn process_raw_promise( + mut self, + promise: Promise, + announce_hash: HashOf, + ) -> Result { + match &self.state { + State::WaitingAnnounceComputed(expected) if *expected == announce_hash => { + let tx_hash = promise.tx_hash; - let signed_promise = - self.ctx - .core - .signer - .signed_message(self.ctx.core.pub_key, promise, None)?; - self.ctx.output(signed_promise); + let signed_promise = + self.ctx + .core + .signer + .signed_message(self.ctx.core.pub_key, promise, None)?; + self.ctx.output(signed_promise); - tracing::trace!("consensus sign promise for transaction-hash={tx_hash}"); - Ok(self.into()) + tracing::trace!("consensus sign promise for transaction-hash={tx_hash}"); + Ok(self.into()) + } + + _ => DefaultProcessing::promise_for_signing(self, promise, announce_hash), + } } fn poll_next_state(mut self, cx: &mut Context<'_>) -> Result<(Poll<()>, ValidatorState)> { diff --git a/ethexe/processor/src/handling/overlaid.rs b/ethexe/processor/src/handling/overlaid.rs index 2ab1c4393c9..4e94bd56b97 100644 --- a/ethexe/processor/src/handling/overlaid.rs +++ b/ethexe/processor/src/handling/overlaid.rs @@ -25,9 +25,7 @@ use crate::{ host::InstanceCreator, }; use core_processor::common::JournalNote; -use ethexe_common::{ - BlockHeader, PromisePolicy, db::CodesStorageRO, gear::MessageType, injected::Promise, -}; +use ethexe_common::{BlockHeader, db::CodesStorageRO, gear::MessageType, injected::Promise}; use ethexe_db::{CASDatabase, Database}; use ethexe_runtime_common::{InBlockTransitions, TransitionController}; use gear_core::{ @@ -88,7 +86,6 @@ impl OverlaidRunContext { gas_allowance, chunk_size, block_header, - PromisePolicy::Disabled, None, ), base_program, diff --git a/ethexe/processor/src/handling/run.rs b/ethexe/processor/src/handling/run.rs index b8605844552..2f4697f6dc0 100644 --- a/ethexe/processor/src/handling/run.rs +++ b/ethexe/processor/src/handling/run.rs @@ -190,8 +190,18 @@ pub(super) trait RunContext { /// Get reference to instance creator. fn instance_creator(&self) -> &InstanceCreator; + /// Returns the promises output channel if it set for current execution. fn promise_out_tx(&self) -> &Option>; + /// [`PromisePolicy`] tells processor should it emit promises or not. + /// By default if [`RunContext::promise_out_tx`] returns [`Some`] this function will return [`PromisePolicy::Enabled`]. + fn promise_policy(&self) -> PromisePolicy { + match self.promise_out_tx().is_some() { + true => PromisePolicy::Enabled, + false => PromisePolicy::Disabled, + } + } + /// Returns the header of the current block. fn block_header(&self) -> BlockHeader; @@ -250,11 +260,6 @@ pub(super) trait RunContext { false } - // TODO: add docs - fn promise_policy(&self) -> PromisePolicy { - PromisePolicy::default() - } - /// Checks whether the run must be stopped early without executing the rest chunks. /// /// In common execution, the run is never stopped early. @@ -272,12 +277,10 @@ pub(crate) struct CommonRunContext { pub(crate) gas_allowance_counter: GasAllowanceCounter, pub(crate) chunk_size: usize, pub(crate) block_header: BlockHeader, - pub(crate) promise_policy: PromisePolicy, pub(crate) promise_out_tx: Option>, } impl CommonRunContext { - #[allow(clippy::too_many_arguments)] pub(crate) fn new( db: Database, instance_creator: InstanceCreator, @@ -285,7 +288,6 @@ impl CommonRunContext { gas_allowance: u64, chunk_size: usize, block_header: BlockHeader, - promise_policy: PromisePolicy, promise_out_tx: Option>, ) -> Self { CommonRunContext { @@ -295,7 +297,6 @@ impl CommonRunContext { gas_allowance_counter: GasAllowanceCounter::new(gas_allowance), chunk_size, block_header, - promise_policy, promise_out_tx, } } @@ -359,10 +360,6 @@ impl RunContext for CommonRunContext { states(&self.transitions, processing_queue_type) } - fn promise_policy(&self) -> PromisePolicy { - self.promise_policy - } - fn chunk_size(&self) -> usize { self.chunk_size } @@ -543,7 +540,6 @@ mod chunk_execution_spawn { executor: InstanceWrapper, db: Box, gas_allowance_for_chunk: u64, - promise_policy: PromisePolicy, } let (db, _, gas_allowance_counter) = ctx.borrow_inner(); @@ -568,12 +564,13 @@ mod chunk_execution_spawn { executor, db: db.clone_boxed(), gas_allowance_for_chunk, - promise_policy: ctx.promise_policy(), }) }) .collect::>>()?; + let promise_policy = ctx.promise_policy(); let promise_out_tx = ctx.promise_out_tx().clone(); + let block_header = ctx.block_header(); let block_info = BlockInfo { height: block_header.height, @@ -592,7 +589,6 @@ mod chunk_execution_spawn { mut executor, db, gas_allowance_for_chunk, - promise_policy, }| { let (jn, new_state_hash, gas_spent) = executor .run( @@ -772,7 +768,6 @@ mod tests { gas_allowance_counter: GasAllowanceCounter::new(1_000_000), chunk_size: CHUNK_PROCESSING_THREADS, block_header: BlockHeader::dummy(3), - promise_policy: PromisePolicy::Disabled, promise_out_tx: None, }; diff --git a/ethexe/processor/src/lib.rs b/ethexe/processor/src/lib.rs index 31098aeba9d..33a62123a2d 100644 --- a/ethexe/processor/src/lib.rs +++ b/ethexe/processor/src/lib.rs @@ -20,7 +20,7 @@ use core::num::NonZero; use ethexe_common::{ - CodeAndIdUnchecked, ProgramStates, PromisePolicy, Schedule, SimpleBlockData, + CodeAndIdUnchecked, ProgramStates, Schedule, SimpleBlockData, ecdsa::VerifiedData, events::{BlockRequestEvent, MirrorRequestEvent, mirror::MessageQueueingRequestedEvent}, injected::{InjectedTransaction, Promise}, @@ -108,31 +108,22 @@ pub struct Processor { config: ProcessorConfig, db: Database, creator: InstanceCreator, - promise_out_tx: Option>, } /// TODO: consider avoiding re-instantiations on processing events. /// Maybe impl `struct EventProcessor`. impl Processor { /// Creates processor with default config. - pub fn new( - db: Database, - promise_out_tx: Option>, - ) -> Result { - Self::with_config(Default::default(), db, promise_out_tx) + pub fn new(db: Database) -> Result { + Self::with_config(Default::default(), db) } - pub fn with_config( - config: ProcessorConfig, - db: Database, - promise_out_tx: Option>, - ) -> Result { + pub fn with_config(config: ProcessorConfig, db: Database) -> Result { let creator = InstanceCreator::new(host::runtime())?; Ok(Self { config, db, creator, - promise_out_tx, }) } @@ -185,6 +176,7 @@ impl Processor { pub async fn process_programs( &mut self, executable: ExecutableData, + promise_out_tx: Option>, ) -> Result { log::debug!("{executable}"); @@ -195,7 +187,6 @@ impl Processor { injected_transactions, gas_allowance, events, - promise_policy, } = executable; let mut transitions = @@ -206,7 +197,7 @@ impl Processor { if let Some(gas_allowance) = gas_allowance { transitions = self - .process_queues(transitions, block, gas_allowance, promise_policy) + .process_queues(transitions, block, gas_allowance, promise_out_tx) .await?; } transitions = self.process_tasks(transitions); @@ -247,7 +238,7 @@ impl Processor { transitions: InBlockTransitions, block: SimpleBlockData, gas_allowance: u64, - promise_policy: PromisePolicy, + promise_out_tx: Option>, ) -> Result { CommonRunContext::new( self.db.clone(), @@ -256,8 +247,7 @@ impl Processor { gas_allowance, self.config.chunk_size, block.header, - promise_policy, - self.promise_out_tx.clone(), + promise_out_tx, ) .run() .await @@ -309,7 +299,6 @@ pub struct ExecutableData { pub injected_transactions: Vec>, pub gas_allowance: Option, pub events: Vec, - pub promise_policy: PromisePolicy, } #[cfg(test)] @@ -322,7 +311,6 @@ impl Default for ExecutableData { injected_transactions: vec![], gas_allowance: Some(ethexe_common::DEFAULT_BLOCK_GAS_LIMIT), events: vec![], - promise_policy: Default::default(), } } } diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index 3c8d9ff3ea1..e3455fa7bcb 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -99,10 +99,9 @@ mod utils { pub fn setup_test_env_and_load_codes( codes: [&[u8]; N], - promise_out_tx: Option>, ) -> (Processor, BlockChain, [CodeId; N]) { let db = Database::memory(); - let mut processor = Processor::new(db.clone(), promise_out_tx).unwrap(); + let mut processor = Processor::new(db.clone()).unwrap(); let chain = BlockChain::mock(20).setup(&db); let mut code_ids = Vec::new(); @@ -139,8 +138,7 @@ mod utils { async fn ping_init() { init_logger(); - let (mut processor, chain, [code_id]) = - setup_test_env_and_load_codes([demo_ping::WASM_BINARY], None); + let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY]); // Empty processing for block1 let executable = ExecutableData { @@ -152,7 +150,7 @@ async fn ping_init() { schedule, program_creations, .. - } = processor.process_programs(executable).await.unwrap(); + } = processor.process_programs(executable, None).await.unwrap(); program_creations .into_iter() .for_each(|(pid, cid)| processor.db.set_program_code_id(pid, cid)); @@ -198,7 +196,7 @@ async fn ping_init() { program_creations, .. } = processor - .process_programs(executable) + .process_programs(executable, None) .await .expect("failed to process create program"); program_creations @@ -225,7 +223,7 @@ async fn ping_init() { ..Default::default() }; processor - .process_programs(executable) + .process_programs(executable, None) .await .expect("failed to process send message"); } @@ -234,8 +232,7 @@ async fn ping_init() { fn handle_new_code_valid() { init_logger(); - let mut processor = - Processor::new(Database::memory(), None).expect("failed to create processor"); + let mut processor = Processor::new(Database::memory()).expect("failed to create processor"); let (code_id, code) = utils::wat_to_wasm(utils::VALID_PROGRAM); @@ -261,8 +258,7 @@ fn handle_new_code_valid() { fn handle_new_code_invalid() { init_logger(); - let mut processor = - Processor::new(Database::memory(), None).expect("failed to create processor"); + let mut processor = Processor::new(Database::memory()).expect("failed to create processor"); let (code_id, code) = utils::wat_to_wasm(utils::INVALID_PROGRAM); @@ -280,7 +276,7 @@ async fn ping_pong() { init_logger(); let (mut processor, chain, [code_id, ..]) = - setup_test_env_and_load_codes([demo_ping::WASM_BINARY, demo_async::WASM_BINARY], None); + setup_test_env_and_load_codes([demo_ping::WASM_BINARY, demo_async::WASM_BINARY]); let block1 = chain.blocks[1].to_simple(); let user_id = ActorId::from(10); @@ -333,12 +329,7 @@ async fn ping_pong() { .expect("failed to send message"); let to_users = processor - .process_queues( - handler.transitions, - block1, - DEFAULT_BLOCK_GAS_LIMIT, - PromisePolicy::Disabled, - ) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, None) .await .unwrap() .current_messages(); @@ -365,7 +356,7 @@ async fn async_and_ping() { }; let (mut processor, chain, [ping_code_id, upload_code_id, ..]) = - setup_test_env_and_load_codes([demo_ping::WASM_BINARY, demo_async::WASM_BINARY], None); + setup_test_env_and_load_codes([demo_ping::WASM_BINARY, demo_async::WASM_BINARY]); let block1 = chain.blocks[1].to_simple(); let mut handler = setup_handler(processor.db.clone(), block1); @@ -452,12 +443,7 @@ async fn async_and_ping() { .expect("failed to send message"); let transitions = processor - .process_queues( - handler.transitions, - block1, - DEFAULT_BLOCK_GAS_LIMIT, - PromisePolicy::Disabled, - ) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, None) .await .unwrap(); @@ -509,7 +495,7 @@ async fn many_waits() { let (_, code) = wat_to_wasm(wat.as_str()); - let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([code.as_slice()], None); + let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([code.as_slice()]); let block1 = chain.blocks[1].to_simple(); let wake_block = chain.blocks[1 + blocks_to_wait].to_simple(); @@ -553,12 +539,7 @@ async fn many_waits() { handler.transitions = processor.process_tasks(handler.transitions); handler.transitions = processor - .process_queues( - handler.transitions, - block1, - DEFAULT_BLOCK_GAS_LIMIT, - PromisePolicy::Disabled, - ) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, None) .await .unwrap(); assert_eq!( @@ -583,12 +564,7 @@ async fn many_waits() { .expect("failed to send message"); } handler.transitions = processor - .process_queues( - handler.transitions, - block1, - DEFAULT_BLOCK_GAS_LIMIT, - PromisePolicy::Disabled, - ) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, None) .await .unwrap(); assert_eq!( @@ -610,12 +586,7 @@ async fn many_waits() { let transitions = InBlockTransitions::new(wake_block.header.height, states, schedule); let transitions = processor.process_tasks(transitions); let transitions = processor - .process_queues( - transitions, - wake_block, - DEFAULT_BLOCK_GAS_LIMIT, - PromisePolicy::Disabled, - ) + .process_queues(transitions, wake_block, DEFAULT_BLOCK_GAS_LIMIT, None) .await .unwrap(); @@ -650,7 +621,7 @@ async fn overlay_execution() { }; let (mut processor, chain, [ping_code_id, async_code_id]) = - setup_test_env_and_load_codes([demo_ping::WASM_BINARY, demo_async::WASM_BINARY], None); + setup_test_env_and_load_codes([demo_ping::WASM_BINARY, demo_async::WASM_BINARY]); let block1 = chain.blocks[1].to_simple(); // ----------------------------------------------------------------------------- @@ -723,7 +694,7 @@ async fn overlay_execution() { program_creations, .. } = processor - .process_programs(executable_data) + .process_programs(executable_data, None) .await .expect("failed to process events"); program_creations.into_iter().for_each(|(pid, cid)| { @@ -880,8 +851,7 @@ async fn injected_ping_pong() { init_logger(); let (promise_out_tx, mut promise_receiver) = mpsc::unbounded_channel(); - let (mut processor, chain, [code_id]) = - setup_test_env_and_load_codes([demo_ping::WASM_BINARY], Some(promise_out_tx)); + let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY]); let block1 = chain.blocks[1].to_simple(); let user_1 = ActorId::from(10); @@ -922,12 +892,7 @@ async fn injected_ping_pong() { .expect("failed to send message"); handler.transitions = processor - .process_queues( - handler.transitions, - block1, - DEFAULT_BLOCK_GAS_LIMIT, - PromisePolicy::Disabled, - ) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, None) .await .unwrap(); @@ -955,7 +920,7 @@ async fn injected_ping_pong() { handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, - PromisePolicy::Enabled, + Some(promise_out_tx.clone()), ) .await .unwrap(); @@ -991,15 +956,13 @@ async fn injected_ping_pong() { #[cfg(debug_assertions)] // FIXME: test fails in release mode #[tokio::test(flavor = "multi_thread")] -// TODO: add here testing the promises async fn injected_prioritized_over_canonical() { const MSG_NUM: usize = 100; init_logger(); let (promise_out_tx, mut promise_receiver) = mpsc::unbounded_channel(); - let (mut processor, chain, [code_id]) = - setup_test_env_and_load_codes([demo_ping::WASM_BINARY], Some(promise_out_tx)); + let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY]); let block1 = chain.blocks[1].to_simple(); let canonical_user = ActorId::from(10); @@ -1040,12 +1003,7 @@ async fn injected_prioritized_over_canonical() { .expect("failed to send message"); handler.transitions = processor - .process_queues( - handler.transitions, - block1, - DEFAULT_BLOCK_GAS_LIMIT, - PromisePolicy::Disabled, - ) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, None) .await .unwrap(); @@ -1066,10 +1024,10 @@ async fn injected_prioritized_over_canonical() { } // Send injected messages - let mut tx_hahes = vec![]; + let mut tx_hashes = vec![]; for _ in 0..MSG_NUM { let tx = injected(actor_id, b"PING", 0); - tx_hahes.push(tx.to_hash()); + tx_hashes.push(tx.to_hash()); handler .handle_injected_transaction(injected_user, tx) .expect("failed to send message"); @@ -1080,12 +1038,12 @@ async fn injected_prioritized_over_canonical() { handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, - PromisePolicy::Enabled, + Some(promise_out_tx.clone()), ) .await .unwrap(); - for tx_hash in tx_hahes { + for tx_hash in tx_hashes { let promise = promise_receiver .recv() .await @@ -1116,8 +1074,7 @@ async fn injected_prioritized_over_canonical() { async fn executable_balance_charged() { init_logger(); - let (mut processor, chain, [code_id]) = - setup_test_env_and_load_codes([demo_ping::WASM_BINARY], None); + let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY]); let block1 = chain.blocks[1].to_simple(); let mut handler = setup_handler(processor.db.clone(), block1); @@ -1159,12 +1116,7 @@ async fn executable_balance_charged() { .expect("failed to send message"); handler.transitions = processor - .process_queues( - handler.transitions, - block1, - DEFAULT_BLOCK_GAS_LIMIT, - PromisePolicy::Disabled, - ) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, None) .await .unwrap(); @@ -1209,7 +1161,7 @@ async fn executable_balance_injected_panic_not_charged() { let (promise_out_tx, mut promise_receiver) = mpsc::unbounded_channel(); let (mut processor, chain, [code_id]) = - setup_test_env_and_load_codes([demo_panic_payload::WASM_BINARY], Some(promise_out_tx)); + setup_test_env_and_load_codes([demo_panic_payload::WASM_BINARY]); let block1 = chain.blocks[1].to_simple(); let user_id = ActorId::from(10); @@ -1256,7 +1208,7 @@ async fn executable_balance_injected_panic_not_charged() { handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, - PromisePolicy::Disabled, + Some(promise_out_tx.clone()), ) .await .unwrap(); @@ -1273,7 +1225,7 @@ async fn executable_balance_injected_panic_not_charged() { handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, - PromisePolicy::Enabled, + Some(promise_out_tx.clone()), ) .await .unwrap(); @@ -1321,7 +1273,7 @@ async fn executable_balance_injected_panic_not_charged() { handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, - PromisePolicy::Disabled, + Some(promise_out_tx.clone()), ) .await .unwrap(); @@ -1348,8 +1300,7 @@ async fn insufficient_executable_balance_still_charged() { init_logger(); - let (mut processor, chain, [code_id]) = - setup_test_env_and_load_codes([demo_ping::WASM_BINARY], None); + let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY]); let block1 = chain.blocks[1].to_simple(); let mut handler = setup_handler(processor.db.clone(), block1); @@ -1389,12 +1340,7 @@ async fn insufficient_executable_balance_still_charged() { .expect("failed to send message"); handler.transitions = processor - .process_queues( - handler.transitions, - block1, - DEFAULT_BLOCK_GAS_LIMIT, - PromisePolicy::Disabled, - ) + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, None) .await .unwrap(); diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index b06b4026eb4..4a142649b76 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -107,7 +107,6 @@ impl RpcServer { chunk_size: self.config.chunk_size, }, self.db.clone(), - None, )? .overlaid(); diff --git a/ethexe/runtime/common/src/lib.rs b/ethexe/runtime/common/src/lib.rs index 0ad7ea9b47c..d83b7ab70fb 100644 --- a/ethexe/runtime/common/src/lib.rs +++ b/ethexe/runtime/common/src/lib.rs @@ -233,7 +233,6 @@ where stop_processing: false, }; - // TODO: move to separate function if ctx.promise_policy.is_enabled() && is_promise_required { process_journal_for_injected_dispatch(ri, &journal, dispatch_id); } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 3f2c08573f5..94f2d72356a 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -25,7 +25,7 @@ use anyhow::{Context, Result}; use async_trait::async_trait; use ethexe_blob_loader::{BlobLoader, BlobLoaderEvent, BlobLoaderService, ConsensusLayerConfig}; use ethexe_common::{COMMITMENT_DELAY_LIMIT, gear::CodeState, network::VerifiedValidatorMessage}; -use ethexe_compute::{ComputeConfig, ComputeEvent, ComputeService, ComputeServiceBuilder}; +use ethexe_compute::{ComputeConfig, ComputeEvent, ComputeService}; use ethexe_consensus::{ ConnectService, ConsensusEvent, ConsensusService, ValidatorConfig, ValidatorService, }; @@ -39,7 +39,7 @@ use ethexe_observer::{ ObserverEvent, ObserverService, utils::{BlockId, BlockLoader}, }; -use ethexe_processor::ProcessorConfig; +use ethexe_processor::{Processor, ProcessorConfig}; use ethexe_prometheus::{PrometheusEvent, PrometheusService}; use ethexe_rpc::{RpcEvent, RpcServer}; use ethexe_service_utils::{OptionFuture as _, OptionStreamNext as _}; @@ -373,12 +373,8 @@ impl Service { let processor_config = ProcessorConfig { chunk_size: config.node.chunk_processing_threads, }; - let compute = ComputeServiceBuilder::production() - .db(db.clone()) - .compute_config(compute_config) - .processor_config(processor_config) - .build() - .with_context(|| "failed to build compute service")?; + let processor = Processor::with_config(processor_config, db.clone())?; + let compute = ComputeService::new(compute_config, db.clone(), processor); let fast_sync = config.node.fast_sync; @@ -552,8 +548,8 @@ impl Service { ComputeEvent::CodeProcessed(_) => { // Nothing } - ComputeEvent::Promise(promise) => { - consensus.receive_promise_for_signing(promise)?; + ComputeEvent::Promise(promise, announce_hash) => { + consensus.receive_promise_for_signing(promise, announce_hash)?; } }, Event::Network(event) => { diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 7a37f563d29..f4d60d59265 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -2493,7 +2493,7 @@ async fn injected_tx_fungible_token() { // Listen for inclusion and check the expected payload. node.events() .find(|event| { - if let TestingEvent::Compute(ComputeEvent::Promise(promise)) = event { + if let TestingEvent::Compute(ComputeEvent::Promise(promise, _)) = event { assert_eq!(promise.reply.payload, expected_event.encode()); assert_eq!( promise.reply.code, @@ -3379,5 +3379,3 @@ async fn catch_up_test_case(commitment_delay_limit: u32) { unreachable!(); } } - -// TODO: implement test checks that promises produced only by validator diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index aa5b7d34236..f9c411f3a15 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -42,7 +42,7 @@ use ethexe_common::{ }, network::{SignedValidatorMessage, ValidatorMessage}, }; -use ethexe_compute::{ComputeConfig, ComputeServiceBuilder}; +use ethexe_compute::{ComputeConfig, ComputeService}; use ethexe_consensus::{BatchCommitter, ConnectService, ConsensusService, ValidatorService}; use ethexe_db::Database; use ethexe_ethereum::{ @@ -56,7 +56,7 @@ use ethexe_observer::{ EthereumConfig, ObserverService, utils::{BlockId, BlockLoader, EthereumBlockLoader}, }; -use ethexe_processor::{DEFAULT_CHUNK_SIZE, ProcessorConfig}; +use ethexe_processor::{DEFAULT_CHUNK_SIZE, Processor}; use ethexe_rpc::{DEFAULT_BLOCK_GAS_LIMIT_MULTIPLIER, RpcConfig, RpcServer}; use futures::StreamExt; use gear_core_errors::ReplyCode; @@ -886,12 +886,8 @@ impl Node { "Service is already running" ); - let compute = ComputeServiceBuilder::production() - .db(self.db.clone()) - .compute_config(self.compute_config) - .processor_config(ProcessorConfig::default()) - .build() - .unwrap(); + let processor = Processor::new(self.db.clone()).unwrap(); + let compute = ComputeService::new(self.compute_config, self.db.clone(), processor); let observer = ObserverService::new(&self.eth_cfg, u32::MAX, self.db.clone()) .await From 9c0171163394abec2d6f26217f7402ad341c3826 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 2 Mar 2026 13:06:35 +0300 Subject: [PATCH 24/59] stabilize the AnnouncePromisesStream implementation --- Cargo.lock | 1 + ethexe/compute/Cargo.toml | 3 +- ethexe/compute/src/compute.rs | 274 +++++++++++++++++++++++++-- ethexe/processor/src/handling/run.rs | 1 + ethexe/processor/src/host/mod.rs | 6 + ethexe/processor/src/host/threads.rs | 7 + ethexe/processor/src/lib.rs | 6 +- 7 files changed, 277 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3794c897b30..d213e662732 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5105,6 +5105,7 @@ dependencies = [ name = "ethexe-compute" version = "1.10.0" dependencies = [ + "demo-ping", "derive_more 2.1.1", "ethexe-common", "ethexe-db", diff --git a/ethexe/compute/Cargo.toml b/ethexe/compute/Cargo.toml index f457a556a4b..f92be57ca7c 100644 --- a/ethexe/compute/Cargo.toml +++ b/ethexe/compute/Cargo.toml @@ -34,4 +34,5 @@ wat.workspace = true wasmparser.workspace = true ethexe-common = { workspace = true, features = ["mock"] } ntest.workspace = true - +# test examples +demo-ping.workspace = true diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 3557ef843d6..2f35c26e3e0 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -82,8 +82,10 @@ pub struct ComputeSubService { metrics: Metrics, input: VecDeque<(Announce, PromisePolicy)>, + computation: Option, promises_stream: Option, + pending_event: Option>, } impl ComputeSubService

{ @@ -96,6 +98,7 @@ impl ComputeSubService

{ input: VecDeque::new(), computation: None, promises_stream: None, + pending_event: None, } } @@ -122,11 +125,10 @@ impl ComputeSubService

{ } let not_computed_announces = utils::find_parent_not_computed_announces(&announce, &db)?; - if !not_computed_announces.is_empty() { log::trace!( "compute-sub-service: announce({announce_hash}) contains a {} previous not computed announce, start computing...", - not_computed_announces.len() + not_computed_announces.len(), ); for (announce_hash, announce) in not_computed_announces { @@ -136,6 +138,7 @@ impl ComputeSubService

{ } } + // Compute the target announce Self::compute_one( &db, &mut processor, @@ -195,6 +198,7 @@ impl SubService for ComputeSubService

{ fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll> { if self.computation.is_none() + && self.promises_stream.is_none() && let Some((announce, promise_policy)) = self.input.pop_front() { let maybe_promise_out_tx = match promise_policy { @@ -228,15 +232,18 @@ impl SubService for ComputeSubService

{ match maybe_event { Some(event) => return Poll::Ready(Ok(event)), None => { + log::trace!("announce's promises stream is ended"); self.promises_stream = None; - log::warn!( - "compute-sub-service: promises stream shouldn't ended, because the channel can not be dropped, something happen in processor" - ) + + // Checking for possible event of finishing announce computation. + if let Some(event) = self.pending_event.take() { + return Poll::Ready(event); + } } } } - if let Some(computation) = &mut self.computation + if let Some(ref mut computation) = self.computation && let Poll::Ready(timing_result) = computation.poll_unpin(cx) { let (timing, result) = timing_result.into_parts(); @@ -245,9 +252,16 @@ impl SubService for ComputeSubService

{ .record((timing.busy() + timing.idle()).as_secs_f64()); self.computation = None; - self.promises_stream = None; - return Poll::Ready(result.map(Into::into)); + match self.promises_stream.is_some() { + true => { + // We cannot return [`ComputeEvent::AnnounceComputed`] before all promises will be given. + self.pending_event = Some(result.map(Into::into)); + } + false => { + return Poll::Ready(result.map(Into::into)); + } + } } Poll::Pending @@ -282,9 +296,9 @@ pub(crate) mod utils { type Item = ComputeEvent; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let maybe_promise = futures::ready!(self.receiver.poll_recv(cx)); Poll::Ready( - maybe_promise.map(|promise| ComputeEvent::Promise(promise, self.announce_hash)), + futures::ready!(self.receiver.poll_recv(cx)) + .map(|promise| ComputeEvent::Promise(promise, self.announce_hash)), ) } } @@ -362,11 +376,6 @@ pub(crate) mod utils { } Ok(announces_chain) - - // if announces_chain.is_empty() { - // log::trace!("All announces are already computed"); - // return Ok(announce_hash); - // } } /// Finds events from Ethereum in database which can be processed in current block. @@ -403,13 +412,126 @@ pub(crate) mod utils { } } +// TODO kuzmindev: implement a test case, when we have a early break on injected queue and do not run the canonical one. +// Test the correctness of closing the `AnnouncePromisesStream`. #[cfg(test)] mod tests { use super::*; - use crate::tests::{MockProcessor, PROCESSOR_RESULT}; - use ethexe_common::{gear::StateTransition, mock::*}; + use crate::{ + ComputeService, + tests::{MockProcessor, PROCESSOR_RESULT}, + }; + use ethexe_common::{ + DEFAULT_BLOCK_GAS_LIMIT, + db::OnChainStorageRW, + events::{ + RouterEvent, mirror::ExecutableBalanceTopUpRequestedEvent, router::ProgramCreatedEvent, + }, + gear::StateTransition, + mock::*, + }; + use ethexe_processor::Processor; + use gear_core::{ + message::{ReplyCode, SuccessReplyReason}, + rpc::ReplyInfo, + }; use gprimitives::{ActorId, H256}; + mod test_utils { + use crate::CodeAndIdUnchecked; + use ethexe_common::{ + PrivateKey, SignedMessage, + events::{MirrorEvent, mirror::MessageQueueingRequestedEvent}, + injected::{InjectedTransaction, SignedInjectedTransaction}, + }; + use ethexe_processor::ValidCodeInfo; + use ethexe_runtime_common::RUNTIME_ID; + use gear_core::ids::prelude::CodeIdExt; + use gprimitives::{CodeId, MessageId}; + + use super::*; + + const USER_ID: ActorId = ActorId::new([1u8; 32]); + + pub fn upload_code(processor: &mut Processor, code: &[u8], db: &Database) -> CodeId { + let code_id = CodeId::generate(code); + + let ValidCodeInfo { + code, + instrumented_code, + code_metadata, + } = processor + .process_code(CodeAndIdUnchecked { + code: code.to_vec(), + code_id, + }) + .expect("failed to process code") + .valid + .expect("code is invalid"); + + db.set_original_code(&code); + db.set_instrumented_code(RUNTIME_ID, code_id, instrumented_code); + db.set_code_metadata(code_id, code_metadata); + db.set_code_valid(code_id, true); + + code_id + } + + pub fn block_events(len: usize, actor_id: ActorId, payload: Vec) -> Vec { + (0..len) + .map(|_| canonical_event(actor_id, payload.clone())) + .collect() + } + + pub fn canonical_event(actor_id: ActorId, payload: Vec) -> BlockEvent { + BlockEvent::Mirror { + actor_id, + event: MirrorEvent::MessageQueueingRequested(MessageQueueingRequestedEvent { + id: MessageId::new(H256::random().0), + source: USER_ID, + value: 0, + payload, + call_reply: false, + }), + } + } + + pub fn create_program_events(actor_id: ActorId, code_id: CodeId) -> Vec { + let created_event = + BlockEvent::Router(RouterEvent::ProgramCreated(ProgramCreatedEvent { + actor_id, + code_id, + })); + + let top_up_event = BlockEvent::Mirror { + actor_id, + event: MirrorEvent::ExecutableBalanceTopUpRequested( + ExecutableBalanceTopUpRequestedEvent { + value: 500_000_000_000_000, + }, + ), + }; + + vec![created_event, top_up_event] + } + + pub fn injected_tx( + destination: ActorId, + payload: Vec, + ref_block: H256, + ) -> SignedInjectedTransaction { + let tx = InjectedTransaction { + destination, + payload: payload.into(), + value: 0, + reference_block: ref_block, + salt: H256::random().0.to_vec().into(), + }; + let pk = PrivateKey::random(); + SignedMessage::create(pk, tx).unwrap() + } + } + #[tokio::test] #[ntest::timeout(3000)] async fn test_compute() { @@ -464,6 +586,124 @@ mod tests { ); } + #[tokio::test] + #[ntest::timeout(15000)] + async fn test_compute_with_promises() { + gear_utils::init_default_logger(); + const BLOCKCHAIN_LEN: usize = 10; + + let db = Database::memory(); + let mut processor = Processor::new(db.clone()).unwrap(); + let ping_code_id = test_utils::upload_code(&mut processor, demo_ping::WASM_BINARY, &db); + let ping_id = ActorId::from(0x10000); + + let blockchain = BlockChain::mock(BLOCKCHAIN_LEN as u32).setup(&db); + + // Setup first announce. + let start_announce_hash = { + let mut announce = blockchain.block_top_announce(0).announce.clone(); + announce.gas_allowance = Some(DEFAULT_BLOCK_GAS_LIMIT); + + let announce_hash = db.set_announce(announce); + db.mutate_announce_meta(announce_hash, |meta| meta.computed = true); + db.mutate_latest_data(|data| { + data.start_announce_hash = announce_hash; + }); + db.set_announce_program_states(announce_hash, Default::default()); + db.set_announce_schedule(announce_hash, Default::default()); + + announce_hash + }; + + // Setup announces and events. + let mut parent_announce = start_announce_hash; + let announces_chain = (1..BLOCKCHAIN_LEN) + .map(|i| { + let announce = { + let mut announce = blockchain.block_top_announce(i).announce.clone(); + announce.gas_allowance = Some(DEFAULT_BLOCK_GAS_LIMIT); + announce.parent = parent_announce; + + let block = announce.block_hash; + let txs = (i != 1) + .then(|| vec![test_utils::injected_tx(ping_id, b"PING".to_vec(), block)]) + .unwrap_or_default(); + announce.injected_transactions = txs; + announce + }; + + let announce_hash = db.set_announce(announce.clone()); + db.mutate_announce_meta(announce_hash, |meta| meta.computed = false); + + let mut block_events = (i == 1) + .then(|| test_utils::create_program_events(ping_id, ping_code_id)) + .unwrap_or_default(); + block_events.extend(test_utils::block_events(5, ping_id, b"PING".to_vec())); + db.set_block_events(announce.block_hash, &block_events); + + parent_announce = announce_hash; + announce + }) + .collect::>(); + + let mut compute_service = + ComputeService::new(ComputeConfig::without_quarantine(), db.clone(), processor); + + // Send announces for computation. + compute_service.compute_announce( + announces_chain.get(2).unwrap().clone(), + PromisePolicy::Enabled, + ); + compute_service.compute_announce( + announces_chain.get(5).unwrap().clone(), + PromisePolicy::Enabled, + ); + compute_service.compute_announce( + announces_chain.get(8).unwrap().clone(), + PromisePolicy::Enabled, + ); + + let mut expected_announces = vec![ + announces_chain.get(2).unwrap().to_hash(), + announces_chain.get(5).unwrap().to_hash(), + announces_chain.get(8).unwrap().to_hash(), + ]; + + let mut expected_promises = expected_announces + .iter() + .map(|hash| { + let announce = db.announce(*hash).unwrap(); + let tx = announce.injected_transactions[0].clone().into_data(); + Promise { + tx_hash: tx.to_hash(), + reply: ReplyInfo { + payload: b"PONG".to_vec(), + value: 0, + code: ReplyCode::Success(SuccessReplyReason::Manual), + }, + } + }) + .collect::>(); + + while !expected_announces.is_empty() || !expected_promises.is_empty() { + match compute_service.next().await.unwrap().unwrap() { + ComputeEvent::AnnounceComputed(hash) => { + if *expected_announces.first().unwrap() == hash { + expected_announces.remove(0); + } + } + ComputeEvent::Promise(promise, announce) => { + if *expected_announces.first().unwrap() == announce + && expected_promises.first().unwrap().clone() == promise + { + expected_promises.remove(0); + } + } + _ => unreachable!("unexpected event for current test"), + } + } + } + #[test] fn find_not_computed_announces_work_correctly() { const BLOCKCHAIN_LEN: usize = 10; diff --git a/ethexe/processor/src/handling/run.rs b/ethexe/processor/src/handling/run.rs index 2f4697f6dc0..d01a0b26edd 100644 --- a/ethexe/processor/src/handling/run.rs +++ b/ethexe/processor/src/handling/run.rs @@ -307,6 +307,7 @@ impl CommonRunContext { if can_continue { // If gas is still left in block, process canonical (Ethereum) queues + log::trace!("running for canonical queue..."); let _ = run_for_queue_type(&mut self, MessageType::Canonical).await?; } diff --git a/ethexe/processor/src/host/mod.rs b/ethexe/processor/src/host/mod.rs index 6bb88de998b..e93f3341a8e 100644 --- a/ethexe/processor/src/host/mod.rs +++ b/ethexe/processor/src/host/mod.rs @@ -188,6 +188,12 @@ impl InstanceWrapper { let new_state_hash = threads::with_params(|params| params.state_hash); + // Clear the thread-local sender after the injected queue run. If processing stops here + // and we never start the canonical queue, the sender stored in ThreadParams would stay + // alive on the worker thread and keep the promise channel open, so the outer + // AnnouncePromisesStream would never observe completion. + threads::clear_promise_out_tx(); + Ok((mega_journal, new_state_hash, gas_spent as u64)) } diff --git a/ethexe/processor/src/host/threads.rs b/ethexe/processor/src/host/threads.rs index 4f2e48a1b0d..fdf70e9f68e 100644 --- a/ethexe/processor/src/host/threads.rs +++ b/ethexe/processor/src/host/threads.rs @@ -144,6 +144,13 @@ pub fn with_params(f: impl FnOnce(&mut ThreadParams) -> T) -> T { }) } +pub fn clear_promise_out_tx() { + PARAMS.with_borrow_mut(|maybe_params| { + let params = maybe_params.as_mut().expect(UNSET_PANIC); + let _ = params.promise_out_tx.take(); + }) +} + #[derive(Debug)] pub struct EthexeHostLazyPages; diff --git a/ethexe/processor/src/lib.rs b/ethexe/processor/src/lib.rs index 33a62123a2d..bcbc60da5fd 100644 --- a/ethexe/processor/src/lib.rs +++ b/ethexe/processor/src/lib.rs @@ -193,7 +193,7 @@ impl Processor { InBlockTransitions::new(block.header.height, program_states, schedule); transitions = - self.process_injected_and_events(transitions, injected_transactions, events)?; + self.handle_injected_and_events(transitions, injected_transactions, events)?; if let Some(gas_allowance) = gas_allowance { transitions = self @@ -205,7 +205,7 @@ impl Processor { Ok(transitions.finalize()) } - fn process_injected_and_events( + fn handle_injected_and_events( &mut self, transitions: InBlockTransitions, injected_transactions: Vec>, @@ -369,7 +369,7 @@ impl OverlaidProcessor { let transitions = InBlockTransitions::new(block.header.height, program_states, Schedule::default()); - let transitions = self.0.process_injected_and_events( + let transitions = self.0.handle_injected_and_events( transitions, vec![], vec![BlockRequestEvent::Mirror { From db7b81bf78ebae8f25d96af33763db3368f9801e Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 2 Mar 2026 13:50:23 +0300 Subject: [PATCH 25/59] implement test with early break --- ethexe/compute/src/compute.rs | 57 +++++++++++++++++++++++++--- ethexe/processor/src/handling/run.rs | 1 + 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 2f35c26e3e0..4f6fbb0d719 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -412,8 +412,6 @@ pub(crate) mod utils { } } -// TODO kuzmindev: implement a test case, when we have a early break on injected queue and do not run the canonical one. -// Test the correctness of closing the `AnnouncePromisesStream`. #[cfg(test)] mod tests { use super::*; @@ -626,7 +624,7 @@ mod tests { let block = announce.block_hash; let txs = (i != 1) - .then(|| vec![test_utils::injected_tx(ping_id, b"PING".to_vec(), block)]) + .then(|| vec![test_utils::injected_tx(ping_id, b"PING".into(), block)]) .unwrap_or_default(); announce.injected_transactions = txs; announce @@ -638,7 +636,7 @@ mod tests { let mut block_events = (i == 1) .then(|| test_utils::create_program_events(ping_id, ping_code_id)) .unwrap_or_default(); - block_events.extend(test_utils::block_events(5, ping_id, b"PING".to_vec())); + block_events.extend(test_utils::block_events(5, ping_id, b"PING".into())); db.set_block_events(announce.block_hash, &block_events); parent_announce = announce_hash; @@ -677,7 +675,7 @@ mod tests { Promise { tx_hash: tx.to_hash(), reply: ReplyInfo { - payload: b"PONG".to_vec(), + payload: b"PONG".into(), value: 0, code: ReplyCode::Success(SuccessReplyReason::Manual), }, @@ -704,6 +702,55 @@ mod tests { } } + #[tokio::test] + async fn test_compute_with_early_break() { + gear_utils::init_default_logger(); + + let db = Database::memory(); + let mut processor = Processor::new(db.clone()).unwrap(); + + let ping_code_id = test_utils::upload_code(&mut processor, demo_ping::WASM_BINARY, &db); + let ping_id = ActorId::from(0x10000); + + let blockchain = BlockChain::mock(3).setup(&db); + + let first_announce_hash = { + let mut announce = blockchain.block_top_announce(1).announce.clone(); + announce.gas_allowance = Some(DEFAULT_BLOCK_GAS_LIMIT); + + let mut canonical_events = test_utils::create_program_events(ping_id, ping_code_id); + canonical_events.push(test_utils::canonical_event(ping_id, b"PING".into())); + + db.set_block_events(announce.block_hash, &canonical_events); + db.set_announce(announce) + }; + + let (announce, announce_hash) = { + let mut announce = blockchain.block_top_announce(2).announce.clone(); + announce.gas_allowance = Some(400_000); + announce.parent = first_announce_hash; + + let ref_block = announce.block_hash; + let txs = (0..300) + .map(|_| test_utils::injected_tx(ping_id, b"PING".into(), ref_block)) + .collect::>(); + announce.injected_transactions = txs; + let hash = db.set_announce(announce.clone()); + (announce, hash) + }; + + let mut compute_service = + ComputeService::new(ComputeConfig::without_quarantine(), db.clone(), processor); + compute_service.compute_announce(announce, PromisePolicy::Enabled); + + loop { + let event = compute_service.next().await.unwrap().unwrap(); + if event == ComputeEvent::AnnounceComputed(announce_hash) { + break; + } + } + } + #[test] fn find_not_computed_announces_work_correctly() { const BLOCKCHAIN_LEN: usize = 10; diff --git a/ethexe/processor/src/handling/run.rs b/ethexe/processor/src/handling/run.rs index d01a0b26edd..9b0032f49d0 100644 --- a/ethexe/processor/src/handling/run.rs +++ b/ethexe/processor/src/handling/run.rs @@ -304,6 +304,7 @@ impl CommonRunContext { pub(crate) async fn run(mut self) -> Result { // Start with injected queues processing. let can_continue = run_for_queue_type(&mut self, MessageType::Injected).await?; + log::error!("can continue = {can_continue}"); if can_continue { // If gas is still left in block, process canonical (Ethereum) queues From f19f762d27c055a8eb8f9d6ee2b48e6439227894 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 2 Mar 2026 14:46:00 +0300 Subject: [PATCH 26/59] fix clippy --- ethexe/compute/src/compute.rs | 17 +++++++++++------ ethexe/processor/src/handling/run.rs | 10 +++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 4f6fbb0d719..9e264c83cee 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -623,9 +623,12 @@ mod tests { announce.parent = parent_announce; let block = announce.block_hash; - let txs = (i != 1) - .then(|| vec![test_utils::injected_tx(ping_id, b"PING".into(), block)]) - .unwrap_or_default(); + let txs = if i != 1 { + vec![test_utils::injected_tx(ping_id, b"PING".into(), block)] + } else { + Default::default() + }; + announce.injected_transactions = txs; announce }; @@ -633,9 +636,11 @@ mod tests { let announce_hash = db.set_announce(announce.clone()); db.mutate_announce_meta(announce_hash, |meta| meta.computed = false); - let mut block_events = (i == 1) - .then(|| test_utils::create_program_events(ping_id, ping_code_id)) - .unwrap_or_default(); + let mut block_events = if i == 1 { + test_utils::create_program_events(ping_id, ping_code_id) + } else { + Default::default() + }; block_events.extend(test_utils::block_events(5, ping_id, b"PING".into())); db.set_block_events(announce.block_hash, &block_events); diff --git a/ethexe/processor/src/handling/run.rs b/ethexe/processor/src/handling/run.rs index 9b0032f49d0..0577e04e021 100644 --- a/ethexe/processor/src/handling/run.rs +++ b/ethexe/processor/src/handling/run.rs @@ -301,10 +301,18 @@ impl CommonRunContext { } } + fn disable_promises(&mut self) { + if self.promise_out_tx.take().is_some() { + log::trace!("dropping the promise sender"); + } + } + pub(crate) async fn run(mut self) -> Result { // Start with injected queues processing. let can_continue = run_for_queue_type(&mut self, MessageType::Injected).await?; - log::error!("can continue = {can_continue}"); + + // Disabling promises after running the injected queue. + self.disable_promises(); if can_continue { // If gas is still left in block, process canonical (Ethereum) queues From ba0df7046097cf34888e28c37a3be65c31d8783b Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 2 Mar 2026 15:43:05 +0300 Subject: [PATCH 27/59] up limits for test --- ethexe/compute/src/compute.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 9e264c83cee..ddc1eb4519d 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -585,7 +585,7 @@ mod tests { } #[tokio::test] - #[ntest::timeout(15000)] + #[ntest::timeout(30000)] async fn test_compute_with_promises() { gear_utils::init_default_logger(); const BLOCKCHAIN_LEN: usize = 10; From 4095212bec6f869129001c87c69bef56b243a943 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 3 Mar 2026 13:07:56 +0300 Subject: [PATCH 28/59] add guard for promise channel drop --- Cargo.lock | 1 + ethexe/processor/Cargo.toml | 1 + ethexe/processor/src/host/mod.rs | 11 +++++------ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f248eba7e9f..31f11592332 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5290,6 +5290,7 @@ dependencies = [ "parity-scale-codec", "rand 0.8.5", "rayon", + "scopeguard", "sp-allocator", "sp-wasm-interface", "thiserror 2.0.17", diff --git a/ethexe/processor/Cargo.toml b/ethexe/processor/Cargo.toml index 3e4a3fe516e..e59b3045b19 100644 --- a/ethexe/processor/Cargo.toml +++ b/ethexe/processor/Cargo.toml @@ -31,6 +31,7 @@ itertools = { workspace = true, features = ["use_std"] } gear-workspace-hack.workspace = true anyhow.workspace = true derive_more.workspace = true +scopeguard.workspace = true [dev-dependencies] anyhow.workspace = true diff --git a/ethexe/processor/src/host/mod.rs b/ethexe/processor/src/host/mod.rs index e93f3341a8e..153d712cdd2 100644 --- a/ethexe/processor/src/host/mod.rs +++ b/ethexe/processor/src/host/mod.rs @@ -175,6 +175,11 @@ impl InstanceWrapper { ) -> Result<(ProgramJournals, H256, u64)> { threads::set(db, ctx.state_root, promise_out_tx.clone()); + // Cleanup the `promise_out_tx` from thread-local to signal receiver that channel is closed. + let _cleanup = scopeguard::guard((), |()| { + threads::clear_promise_out_tx(); + }); + // Pieces of resulting journal. Hack to avoid single allocation limit. let (ptr_lens, gas_spent): (Vec, i64) = self.call("run", ctx.encode())?; @@ -188,12 +193,6 @@ impl InstanceWrapper { let new_state_hash = threads::with_params(|params| params.state_hash); - // Clear the thread-local sender after the injected queue run. If processing stops here - // and we never start the canonical queue, the sender stored in ThreadParams would stay - // alive on the worker thread and keep the promise channel open, so the outer - // AnnouncePromisesStream would never observe completion. - threads::clear_promise_out_tx(); - Ok((mega_journal, new_state_hash, gas_spent as u64)) } From f0cd8444eb1e52e092d20600a9c6a8d9db017007 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 4 Mar 2026 13:33:21 +0300 Subject: [PATCH 29/59] self review small fixes --- ethexe/common/src/primitives.rs | 4 ---- ethexe/processor/src/handling/run.rs | 1 - ethexe/processor/src/tests.rs | 1 - ethexe/service/src/lib.rs | 12 ++++++------ ethexe/service/src/tests/utils/env.rs | 7 +++++-- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/ethexe/common/src/primitives.rs b/ethexe/common/src/primitives.rs index cc6ee1d7697..0c82c5103fa 100644 --- a/ethexe/common/src/primitives.rs +++ b/ethexe/common/src/primitives.rs @@ -139,10 +139,6 @@ pub enum PromisePolicy { Disabled, } -// Producer -> (announce, PromisePolicy::Enabled) -// Subordinate -> (announce, PromisePolicy::Disabled) -// ConnectNode -> (announce, PromisePolicy::Disabled) - #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, Default, Encode, Decode, TypeInfo)] #[cfg_attr(feature = "std", derive(serde::Serialize))] pub struct StateHashWithQueueSize { diff --git a/ethexe/processor/src/handling/run.rs b/ethexe/processor/src/handling/run.rs index 0577e04e021..1ff8331d35b 100644 --- a/ethexe/processor/src/handling/run.rs +++ b/ethexe/processor/src/handling/run.rs @@ -316,7 +316,6 @@ impl CommonRunContext { if can_continue { // If gas is still left in block, process canonical (Ethereum) queues - log::trace!("running for canonical queue..."); let _ = run_for_queue_type(&mut self, MessageType::Canonical).await?; } diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index e3455fa7bcb..f74f5f4a932 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -910,7 +910,6 @@ async fn injected_ping_pong() { .expect("failed to send message"); let injected_tx = injected(actor_id, b"PING", 0); - log::error!("injected tx hash: {:?}", injected_tx.to_hash()); handler .handle_injected_transaction(user_2, injected_tx.clone()) .expect("failed to send message"); diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 94f2d72356a..d5b72defeff 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -114,7 +114,7 @@ pub struct Service { rpc: Option, fast_sync: bool, - validator_pub_key: Option, + validator_address: Option

, #[cfg(test)] sender: tests::utils::TestingEventSender, @@ -288,6 +288,7 @@ impl Service { let validator_pub_key = Self::get_config_public_key(config.node.validator, &signer) .with_context(|| "failed to get validator private key")?; + let validator_address = validator_pub_key.map(|key| key.to_address()); // TODO #4642: use validator session key let _validator_pub_key_session = @@ -390,7 +391,7 @@ impl Service { prometheus, rpc, fast_sync, - validator_pub_key, + validator_address, #[cfg(test)] sender: unreachable!(), }) @@ -418,7 +419,7 @@ impl Service { rpc: Option, sender: tests::utils::TestingEventSender, fast_sync: bool, - validator_pub_key: Option, + validator_address: Option
, ) -> Self { Self { db, @@ -432,7 +433,7 @@ impl Service { rpc, sender, fast_sync, - validator_pub_key, + validator_address, } } @@ -458,11 +459,10 @@ impl Service { mut prometheus, rpc, fast_sync: _, - validator_pub_key, + validator_address, #[cfg(test)] sender, } = self; - let validator_address = validator_pub_key.map(|key| key.to_address()); let (mut rpc_handle, mut rpc) = if let Some(rpc) = rpc { log::info!("🌐 Rpc server starting at: {}", rpc.port()); diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index f9c411f3a15..8b77f32736f 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -948,7 +948,10 @@ impl Node { } }; - let validator_pub_key = self.validator_config.as_ref().map(|c| c.public_key); + let validator_address = self + .validator_config + .as_ref() + .map(|c| c.public_key.to_address()); let (sender, receiver) = events::channel(self.db.clone()); @@ -990,7 +993,7 @@ impl Node { rpc, sender, self.fast_sync, - validator_pub_key, + validator_address, ); let name = self.name.clone(); From 91e780267bd91258d37a41bec5ecaada34efcd60 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Thu, 5 Mar 2026 16:57:56 +0300 Subject: [PATCH 30/59] fix for limited vec in injected transaction --- ethexe/compute/src/compute.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index ddc1eb4519d..08cc757f148 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -520,10 +520,10 @@ mod tests { ) -> SignedInjectedTransaction { let tx = InjectedTransaction { destination, - payload: payload.into(), + payload: payload.try_into().unwrap(), value: 0, reference_block: ref_block, - salt: H256::random().0.to_vec().into(), + salt: H256::random().0.to_vec().try_into().unwrap(), }; let pk = PrivateKey::random(); SignedMessage::create(pk, tx).unwrap() From e9d71b3f3d3622eccce8db058ef23dc097af851e Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 6 Mar 2026 13:51:03 +0300 Subject: [PATCH 31/59] small redesign for CompactSignedPromise --- core/src/rpc.rs | 22 +++ ethexe/common/src/injected.rs | 194 +++++++++++---------- ethexe/common/src/mock.rs | 2 +- ethexe/consensus/src/validator/producer.rs | 18 +- ethexe/network/src/validator/topic.rs | 9 +- ethexe/rpc/src/apis/injected.rs | 2 +- ethexe/rpc/src/tests.rs | 9 +- 7 files changed, 142 insertions(+), 114 deletions(-) diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 74117b44578..0fa12fc4ec6 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -20,11 +20,14 @@ use alloc::vec::Vec; use gear_core_errors::ReplyCode; +use gprimitives::H256; use parity_scale_codec::{Decode, Encode}; use scale_decode::DecodeAsType; use scale_encode::EncodeAsType; use scale_info::TypeInfo; +use crate::utils; + /// Pre-calculated gas consumption estimate for a message. /// /// Intended to be used as a result in `calculateGasFor*` RPC calls. @@ -65,6 +68,25 @@ pub struct ReplyInfo { pub code: ReplyCode, } +impl ReplyInfo { + /// Calculates `blake2b` hash from [`ReplyInfo`]. + pub fn to_hash(&self) -> H256 { + let ReplyInfo { + payload, + value, + code, + } = self; + + let bytes = [ + payload.as_ref(), + value.to_be_bytes().as_ref(), + code.to_bytes().as_ref(), + ] + .concat(); + utils::hash(&bytes).into() + } +} + /// Serializer and deserializer for ReplyCode as 0x-prefixed hex string. #[cfg(feature = "std")] pub(crate) mod serialize_reply_code { diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index db911b1d023..d54971f843b 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -148,71 +148,40 @@ pub type SignedPromise = SignedMessage; impl Promise { /// Calculates the `blake2b` hash from promise's reply. pub fn reply_hash(&self) -> HashOf { - let ReplyInfo { - payload, - code, - value, - } = &self.reply; + // Safety by implementation + unsafe { HashOf::new(self.reply.to_hash()) } + } - let bytes = [ - payload.as_ref(), - code.to_bytes().as_ref(), - value.to_be_bytes().as_ref(), - ] - .concat(); - unsafe { HashOf::new(gear_core::utils::hash(&bytes).into()) } + /// Converts promise to its [`PromiseHashes`]. + pub fn to_hashes(&self) -> PromiseHashes { + PromiseHashes { + tx_hash: self.tx_hash, + reply_hash: self.reply_hash(), + } } } impl ToDigest for Promise { fn update_hasher(&self, hasher: &mut sha3::Keccak256) { - // The hash of `Promise` equals to hash of `CompactPromiseHashes`. - CompactPromiseHashes::from(self).update_hasher(hasher); + self.to_hashes().update_hasher(hasher); } } -/// A wrapper on top of [`CompactPromiseHashes`]. +/// A wrapper on top of [`PromiseHashes`]. /// -/// [`CompactPromiseHashes`] is a lightweight version of [`SignedPromise`], that is +/// [`CompactSignedPromise`] is a lightweight version of [`SignedPromise`], that is /// needed to reduce the amount of data transferred in network between validators. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Deref, derive_more::From)] -pub struct CompactSignedPromise(SignedMessage); - -impl From<&SignedPromise> for CompactSignedPromise { - fn from(signed_promise: &SignedPromise) -> Self { - let hashes = CompactPromiseHashes::from(signed_promise.data()); - let message = SignedMessage::try_from_parts( - hashes, - signed_promise.signature().clone(), - signed_promise.address(), - ); - CompactSignedPromise(message.unwrap()) - } -} - -impl CompactSignedPromise { - pub fn create(private_key: PrivateKey, hashes: CompactPromiseHashes) -> SignResult { - SignedMessage::create(private_key, hashes).map(|message| CompactSignedPromise(message)) - } -} +pub struct CompactSignedPromise(SignedMessage); -/// The hashes of [`Promise`]. +/// The hashes of [`Promise`] parts. #[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)] -pub struct CompactPromiseHashes { +pub struct PromiseHashes { pub tx_hash: HashOf, pub reply_hash: HashOf, } -impl From<&Promise> for CompactPromiseHashes { - fn from(promise: &Promise) -> Self { - Self { - tx_hash: promise.tx_hash, - reply_hash: promise.reply_hash(), - } - } -} - -impl ToDigest for CompactPromiseHashes { +impl ToDigest for PromiseHashes { fn update_hasher(&self, hasher: &mut sha3::Keccak256) { let Self { tx_hash, @@ -224,6 +193,65 @@ impl ToDigest for CompactPromiseHashes { } } +impl CompactSignedPromise { + /// Create the [`CompactSignedPromise`] from private key and hashes. + pub fn create(private_key: PrivateKey, promise_hashes: PromiseHashes) -> SignResult { + SignedMessage::create(private_key, promise_hashes).map(CompactSignedPromise) + } + + pub fn create_from_promise(private_key: PrivateKey, promise: &Promise) -> SignResult { + Self::create(private_key, promise.to_hashes()) + } + + /// Create the [`CompactSignedPromise`] from a valid [`SignedPromise`]. + /// + /// # Panics + /// Panics if the digest of [`Promise`] and [`PromiseHashes`] ever diverge. + /// This must hold by construction; tests enforce the invariant. + pub fn from_signed_promise_unchecked(signed_promise: &SignedPromise) -> Self { + Self::try_from(signed_promise) + .expect("SignedPromise and PromiseHashes must have identical digest") + } +} + +impl TryFrom<&SignedPromise> for CompactSignedPromise { + type Error = &'static str; + + fn try_from(signed_promise: &SignedPromise) -> Result { + SignedMessage::try_from_parts( + signed_promise.data().to_hashes(), + *signed_promise.signature(), + signed_promise.address(), + ) + .map(CompactSignedPromise) + } +} + +/// Encoding and decoding of `LimitedVec` as hex string. +#[cfg(feature = "std")] +mod serde_hex { + pub fn serialize( + data: &super::LimitedVec, + serializer: S, + ) -> Result + where + S: serde::Serializer, + { + alloy_primitives::hex::serialize(data.to_vec(), serializer) + } + + pub fn deserialize<'de, D, const N: usize>( + deserializer: D, + ) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let vec: Vec = alloy_primitives::hex::deserialize(deserializer)?; + super::LimitedVec::::try_from(vec) + .map_err(|_| serde::de::Error::custom("LimitedVec deserialization overflow")) + } +} + #[cfg(all(test, feature = "mock"))] mod tests { use gsigner::PrivateKey; @@ -271,59 +299,41 @@ mod tests { #[test] fn promise_hashes_digest_equal_to_promise_digest() { - let promise = { - let mut promise = Promise::mock(()); - promise.reply.value = 123; - promise.reply.payload = vec![1u8, 2u8, 42u8, 66u8]; - promise - }; - let promise_digest = promise.to_digest(); - let promise_hashes = CompactPromiseHashes::from(&promise); - - let promise_hashes_digest = promise_hashes.to_digest(); - assert_eq!(promise_digest, promise_hashes_digest); + let promise = Promise::mock(()); + + assert_eq!(promise.to_digest(), promise.to_hashes().to_digest()); } #[test] - fn compact_signature_valid_for_promise() { - let pk = PrivateKey::random(); - + fn signatures_equal_for_promise_and_compact_promise() { + let private_key = PrivateKey::random(); let promise = Promise::mock(()); - let promise_hashes = CompactPromiseHashes::from(&promise); - let compact_signed_promise = CompactSignedPromise::create(pk, promise_hashes).unwrap(); - let (signature, address) = ( - *compact_signed_promise.signature(), - compact_signed_promise.address(), - ); + let signed_promise = SignedPromise::create(private_key.clone(), promise.clone()).unwrap(); + let compact_signed_promise = + CompactSignedPromise::create_from_promise(private_key, &promise).unwrap(); - let signed_promise = SignedMessage::try_from_parts(promise.clone(), signature, address) - .expect("SignedMessage was correctly constructed from CompactSignedPromise"); - assert_eq!(signed_promise.into_data(), promise); + assert_eq!(signed_promise.address(), compact_signed_promise.address()); + assert_eq!( + signed_promise.signature().clone(), + compact_signed_promise.signature().clone() + ); } -} -/// Encoding and decoding of `LimitedVec` as hex string. -#[cfg(feature = "std")] -mod serde_hex { - pub fn serialize( - data: &super::LimitedVec, - serializer: S, - ) -> Result - where - S: serde::Serializer, - { - alloy_primitives::hex::serialize(data.to_vec(), serializer) - } + #[test] + fn compact_signed_promise_correctly_builds_from_signed_promise() { + let private_key = PrivateKey::random(); + let promise = Promise::mock(()); - pub fn deserialize<'de, D, const N: usize>( - deserializer: D, - ) -> Result, D::Error> - where - D: serde::Deserializer<'de>, - { - let vec: Vec = alloy_primitives::hex::deserialize(deserializer)?; - super::LimitedVec::::try_from(vec) - .map_err(|_| serde::de::Error::custom("LimitedVec deserialization overflow")) + let signed_promise = SignedPromise::create(private_key.clone(), promise).unwrap(); + + let compact_signed_promise = + CompactSignedPromise::try_from(&signed_promise).expect("valid signed promise"); + + assert_eq!(signed_promise.address(), compact_signed_promise.address()); + assert_eq!( + signed_promise.signature().clone(), + compact_signed_promise.signature().clone() + ); } } diff --git a/ethexe/common/src/mock.rs b/ethexe/common/src/mock.rs index a85ae8c5855..e4dbded6617 100644 --- a/ethexe/common/src/mock.rs +++ b/ethexe/common/src/mock.rs @@ -209,7 +209,7 @@ impl Mock> for Promise { Promise { tx_hash, reply: ReplyInfo { - payload: H256::random().0.to_vec().try_into().unwrap(), + payload: H256::random().0.to_vec(), value: 42, code: ReplyCode::Success(SuccessReplyReason::Manual), }, diff --git a/ethexe/consensus/src/validator/producer.rs b/ethexe/consensus/src/validator/producer.rs index ce44837dc72..f80b4c39a46 100644 --- a/ethexe/consensus/src/validator/producer.rs +++ b/ethexe/consensus/src/validator/producer.rs @@ -30,7 +30,7 @@ use ethexe_common::{ Announce, HashOf, PromisePolicy, SimpleBlockData, ValidatorsVec, db::BlockMetaStorageRO, gear::BatchCommitment, - injected::{CompactPromiseHashes, Promise}, + injected::{CompactSignedPromise, Promise}, network::ValidatorMessage, }; use ethexe_service_utils::Timer; @@ -116,16 +116,16 @@ impl StateHandler for Producer { State::WaitingAnnounceComputed(expected) if *expected == announce_hash => { let tx_hash = promise.tx_hash; - let promise_hashes = CompactPromiseHashes::from(&promise); - let signed_promise = self - .ctx - .core - .signer - .signed_message(self.ctx.core.pub_key, promise_hashes, None)? - .into(); + let signed_promise = + self.ctx + .core + .signer + .signed_message(self.ctx.core.pub_key, promise, None)?; + let compact_signed_promise = + CompactSignedPromise::from_signed_promise_unchecked(&signed_promise); self.ctx - .output(ConsensusEvent::SignedPromise(signed_promise)); + .output(ConsensusEvent::SignedPromise(compact_signed_promise)); tracing::trace!("consensus sign promise for transaction-hash={tx_hash}"); Ok(self.into()) diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index fd890d5ea6f..0729e915d2d 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -330,7 +330,7 @@ mod tests { use assert_matches::assert_matches; use ethexe_common::{ self, Announce, - injected::{CompactPromiseHashes, Promise, SignedPromise}, + injected::{Promise, SignedPromise}, mock::Mock, network::{SignedValidatorMessage, ValidatorMessage}, }; @@ -393,11 +393,8 @@ mod tests { public_key: PublicKey, promise: Promise, ) -> CompactSignedPromise { - let promise_hashes = CompactPromiseHashes::from(&promise); - signer - .signed_message(public_key, promise_hashes, None) - .unwrap() - .into() + let signed_promise = signer.signed_message(public_key, promise, None).unwrap(); + CompactSignedPromise::from_signed_promise_unchecked(&signed_promise) } #[test] diff --git a/ethexe/rpc/src/apis/injected.rs b/ethexe/rpc/src/apis/injected.rs index 79322801ee6..fbdd598d579 100644 --- a/ethexe/rpc/src/apis/injected.rs +++ b/ethexe/rpc/src/apis/injected.rs @@ -338,4 +338,4 @@ impl InjectedApi { } }); } -} +} \ No newline at end of file diff --git a/ethexe/rpc/src/tests.rs b/ethexe/rpc/src/tests.rs index 60369f99be0..ca252eb6514 100644 --- a/ethexe/rpc/src/tests.rs +++ b/ethexe/rpc/src/tests.rs @@ -24,7 +24,7 @@ use ethexe_common::{ db::InjectedStorageRW, ecdsa::PrivateKey, gear::MAX_BLOCK_GAS_LIMIT, - injected::{AddressedInjectedTransaction, CompactPromiseHashes, CompactSignedPromise, Promise}, + injected::{AddressedInjectedTransaction, CompactSignedPromise, Promise}, mock::Mock, }; use ethexe_db::Database; @@ -66,7 +66,7 @@ impl MockService { loop { tokio::select! { _ = tx_batch_interval.tick() => { - let promises = self.create_promises_bundle(tx_batch.drain(..)); + let promises = self.promises_bundle(tx_batch.drain(..)); self.rpc.provide_compact_promises(promises); }, _ = self.handle.clone().stopped() => { @@ -83,7 +83,7 @@ impl MockService { }) } - fn create_promises_bundle( + fn promises_bundle( &self, txs: impl IntoIterator, ) -> Vec { @@ -92,8 +92,7 @@ impl MockService { .map(|tx| { let promise = Promise::mock(tx.tx.data().to_hash()); self.db.set_promise(promise.clone()); - let promise_hashes = CompactPromiseHashes::from(&promise); - CompactSignedPromise::create(pk.clone(), promise_hashes).unwrap() + CompactSignedPromise::create_from_promise(pk.clone(), &promise).unwrap() }) .collect() } From 78fa1bd6af20ef64a3fb5bf0ecb05bb1f0e3cb89 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 9 Mar 2026 13:45:56 +0300 Subject: [PATCH 32/59] RPC redesign | PromiseEmissionMode --- Cargo.lock | 3 + ethexe/common/src/db.rs | 2 +- ethexe/common/src/primitives.rs | 10 + ethexe/consensus/src/connect/mod.rs | 3 +- ethexe/consensus/src/validator/core.rs | 6 +- ethexe/consensus/src/validator/mod.rs | 5 +- ethexe/consensus/src/validator/subordinate.rs | 14 +- ethexe/db/src/database.rs | 2 +- ethexe/rpc/Cargo.toml | 3 + ethexe/rpc/src/apis/injected.rs | 341 ------------------ ethexe/rpc/src/apis/injected/mod.rs | 29 ++ .../rpc/src/apis/injected/promise_manager.rs | 163 +++++++++ ethexe/rpc/src/apis/injected/relay.rs | 79 ++++ ethexe/rpc/src/apis/injected/server.rs | 157 ++++++++ ethexe/rpc/src/apis/injected/spawner.rs | 64 ++++ ethexe/rpc/src/apis/injected/trait.rs | 57 +++ ethexe/rpc/src/apis/mod.rs | 9 +- ethexe/rpc/src/lib.rs | 15 +- ethexe/rpc/src/metrics.rs | 2 + ethexe/rpc/src/tests.rs | 4 +- ethexe/service/src/lib.rs | 23 +- ethexe/service/src/tests/utils/env.rs | 21 +- 22 files changed, 636 insertions(+), 376 deletions(-) delete mode 100644 ethexe/rpc/src/apis/injected.rs create mode 100644 ethexe/rpc/src/apis/injected/mod.rs create mode 100644 ethexe/rpc/src/apis/injected/promise_manager.rs create mode 100644 ethexe/rpc/src/apis/injected/relay.rs create mode 100644 ethexe/rpc/src/apis/injected/server.rs create mode 100644 ethexe/rpc/src/apis/injected/spawner.rs create mode 100644 ethexe/rpc/src/apis/injected/trait.rs diff --git a/Cargo.lock b/Cargo.lock index 5371e19ec9f..f229f967f5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5324,6 +5324,7 @@ dependencies = [ name = "ethexe-rpc" version = "1.10.0" dependencies = [ + "alloy", "anyhow", "dashmap 5.5.3", "ethexe-common", @@ -5340,8 +5341,10 @@ dependencies = [ "metrics-derive", "ntest", "parity-scale-codec", + "scopeguard", "serde", "sp-core", + "thiserror 2.0.17", "tokio", "tower 0.4.13", "tower-http 0.5.2", diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index 716874e4049..42bd95b9004 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -147,7 +147,7 @@ pub trait InjectedStorageRO { pub trait InjectedStorageRW: InjectedStorageRO { fn set_injected_transaction(&self, tx: SignedInjectedTransaction); - fn set_promise(&self, promise: Promise); + fn set_promise(&self, promise: &Promise); fn set_promise_signature( &self, diff --git a/ethexe/common/src/primitives.rs b/ethexe/common/src/primitives.rs index 0c82c5103fa..921bc729a67 100644 --- a/ethexe/common/src/primitives.rs +++ b/ethexe/common/src/primitives.rs @@ -129,6 +129,16 @@ impl ToDigest for Announce { } } +#[derive(Clone, Debug, Copy, Default, PartialEq, Eq, derive_more::IsVariant)] +pub enum PromiseEmissionMode { + /// Node should always emit promises during announces execution. + /// Always set [`PromisePolicy::Enabled`]. + AlwaysEmit, + /// [`PromisePolicy`] is set by consensus service. + #[default] + ConsensusDriven, +} + /// [`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/consensus/src/connect/mod.rs b/ethexe/consensus/src/connect/mod.rs index 081be6187b0..cce7bebb859 100644 --- a/ethexe/consensus/src/connect/mod.rs +++ b/ethexe/consensus/src/connect/mod.rs @@ -163,7 +163,8 @@ impl ConnectService { .push_back(ConsensusEvent::AnnounceAccepted(announce_hash)); self.output.push_back(ConsensusEvent::ComputeAnnounce( announce, - PromisePolicy::Disabled, + // TODO: FIXME + PromisePolicy::Enabled, )); } } diff --git a/ethexe/consensus/src/validator/core.rs b/ethexe/consensus/src/validator/core.rs index ff4c596fde7..2b0b07784bd 100644 --- a/ethexe/consensus/src/validator/core.rs +++ b/ethexe/consensus/src/validator/core.rs @@ -26,7 +26,8 @@ use crate::{ use anyhow::{Context as _, Result, anyhow}; use async_trait::async_trait; use ethexe_common::{ - Address, Announce, Digest, HashOf, ProtocolTimelines, SimpleBlockData, ToDigest, ValidatorsVec, + Address, Announce, Digest, HashOf, PromiseEmissionMode, ProtocolTimelines, SimpleBlockData, + ToDigest, ValidatorsVec, consensus::BatchCommitmentValidationRequest, db::{AnnounceStorageRO, BlockMetaStorageRO, OnChainStorageRO}, ecdsa::{ContractSignature, PublicKey}, @@ -70,6 +71,8 @@ pub struct ValidatorCore { pub commitment_delay_limit: u32, /// Delay before producer starts to creating new announce after block prepared. pub producer_delay: Duration, + /// Promise emission mode for consensus service. + pub promise_emission_mode: PromiseEmissionMode, } impl Clone for ValidatorCore { @@ -89,6 +92,7 @@ impl Clone for ValidatorCore { block_gas_limit: self.block_gas_limit, commitment_delay_limit: self.commitment_delay_limit, producer_delay: self.producer_delay, + promise_emission_mode: self.promise_emission_mode, } } } diff --git a/ethexe/consensus/src/validator/mod.rs b/ethexe/consensus/src/validator/mod.rs index fc1785374ec..e0ffb52b01c 100644 --- a/ethexe/consensus/src/validator/mod.rs +++ b/ethexe/consensus/src/validator/mod.rs @@ -54,7 +54,7 @@ use anyhow::{Result, anyhow}; pub use core::BatchCommitter; use derive_more::{Debug, From}; use ethexe_common::{ - Address, Announce, HashOf, SimpleBlockData, + Address, Announce, HashOf, PromiseEmissionMode, SimpleBlockData, consensus::{VerifiedAnnounce, VerifiedValidationRequest}, db::OnChainStorageRO, ecdsa::PublicKey, @@ -115,6 +115,8 @@ pub struct ValidatorConfig { pub router_address: Address, /// Threshold for producer to submit commitment despite of no transitions pub chain_deepness_threshold: u32, + /// Promise emission mode. + pub promise_emission_mode: PromiseEmissionMode, } impl ValidatorService { @@ -153,6 +155,7 @@ impl ValidatorService { block_gas_limit: config.block_gas_limit, commitment_delay_limit: config.commitment_delay_limit, producer_delay: config.producer_delay, + promise_emission_mode: config.promise_emission_mode, }, pending_events: VecDeque::new(), output: VecDeque::new(), diff --git a/ethexe/consensus/src/validator/subordinate.rs b/ethexe/consensus/src/validator/subordinate.rs index 2c2a550238c..f411c436cf9 100644 --- a/ethexe/consensus/src/validator/subordinate.rs +++ b/ethexe/consensus/src/validator/subordinate.rs @@ -28,7 +28,7 @@ use crate::{ use anyhow::Result; use derive_more::{Debug, Display}; use ethexe_common::{ - Address, Announce, HashOf, PromisePolicy, SimpleBlockData, + Address, Announce, HashOf, PromiseEmissionMode, PromisePolicy, SimpleBlockData, consensus::{VerifiedAnnounce, VerifiedValidationRequest}, }; use std::mem; @@ -173,10 +173,14 @@ impl Subordinate { AnnounceStatus::Accepted(announce_hash) => { self.ctx .output(ConsensusEvent::AnnounceAccepted(announce_hash)); - self.ctx.output(ConsensusEvent::ComputeAnnounce( - announce, - PromisePolicy::Disabled, - )); + + let promise_policy = match self.ctx.core.promise_emission_mode { + PromiseEmissionMode::AlwaysEmit => PromisePolicy::Enabled, + PromiseEmissionMode::ConsensusDriven => PromisePolicy::Disabled, + }; + self.ctx + .output(ConsensusEvent::ComputeAnnounce(announce, promise_policy)); + self.state = State::WaitingAnnounceComputed { announce_hash }; Ok(self.into()) diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index 5859636fd92..facb3c6da9d 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -650,7 +650,7 @@ impl InjectedStorageRW for Database { .put(&Key::InjectedTransaction(tx_hash).to_bytes(), tx.encode()); } - fn set_promise(&self, promise: Promise) { + fn set_promise(&self, promise: &Promise) { tracing::trace!(?promise, "Set promise for injected transaction"); self.kv diff --git a/ethexe/rpc/Cargo.toml b/ethexe/rpc/Cargo.toml index 9801aa2df98..e6e736f1096 100644 --- a/ethexe/rpc/Cargo.toml +++ b/ethexe/rpc/Cargo.toml @@ -31,6 +31,9 @@ dashmap.workspace = true metrics.workspace = true metrics-derive.workspace = true gear-workspace-hack.workspace = true +thiserror.workspace = true +scopeguard.workspace = true +alloy.workspace = true [dev-dependencies] jsonrpsee = { workspace = true, features = ["client"] } diff --git a/ethexe/rpc/src/apis/injected.rs b/ethexe/rpc/src/apis/injected.rs deleted file mode 100644 index fbdd598d579..00000000000 --- a/ethexe/rpc/src/apis/injected.rs +++ /dev/null @@ -1,341 +0,0 @@ -// This file is part of Gear. -// -// Copyright (C) 2025 Gear Technologies Inc. -// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use crate::{RpcEvent, errors, metrics::InjectedApiMetrics}; -use dashmap::DashMap; -use ethexe_common::{ - HashOf, SignedMessage, - db::{InjectedStorageRO, InjectedStorageRW}, - injected::{ - AddressedInjectedTransaction, CompactSignedPromise, InjectedTransaction, - InjectedTransactionAcceptance, Promise, SignedPromise, - }, -}; - -use ethexe_db::Database; -use jsonrpsee::{ - PendingSubscriptionSink, SubscriptionMessage, SubscriptionSink, - core::{RpcResult, SubscriptionResult, async_trait}, - proc_macros::rpc, - types::error::ErrorObjectOwned, -}; -use std::sync::Arc; -use tokio::sync::{mpsc, oneshot}; - -#[cfg_attr(not(feature = "client"), rpc(server, namespace = "injected"))] -#[cfg_attr(feature = "client", rpc(server, client, namespace = "injected"))] -pub trait Injected { - /// Just sends an injected transaction. - #[method(name = "sendTransaction")] - async fn send_transaction( - &self, - transaction: AddressedInjectedTransaction, - ) -> RpcResult; - - /// Sends an injected transaction and subscribes to its promise. - #[subscription( - name = "sendTransactionAndWatch", - unsubscribe = "sendTransactionAndWatchUnsubscribe", - item = SignedPromise - )] - async fn send_transaction_and_watch( - &self, - transaction: AddressedInjectedTransaction, - ) -> SubscriptionResult; - - #[method(name = "getTransactionPromise")] - async fn get_transaction_promise( - &self, - tx_hash: HashOf, - ) -> RpcResult>; -} - -type PromiseWaiters = Arc, oneshot::Sender>>; - -/// Implementation of the injected transactions RPC API. -#[derive(Debug, Clone)] -pub struct InjectedApi { - /// The database for protocol data. - db: Database, - /// Sender to forward RPC events to the main service. - rpc_sender: mpsc::UnboundedSender, - /// Map of promise waiters. - promise_waiters: PromiseWaiters, - /// The mapping from injected transaction hash to its promise signature. - promises_computation_waiting: Arc, CompactSignedPromise>>, - /// The metrics related to [`InjectedApi`] - metrics: InjectedApiMetrics, -} - -#[async_trait] -impl InjectedServer for InjectedApi { - async fn send_transaction( - &self, - transaction: AddressedInjectedTransaction, - ) -> RpcResult { - tracing::trace!( - tx_hash = %transaction.tx.data().to_hash(), - ?transaction, - "Called injected_sendTransaction" - ); - self.forward_transaction(transaction).await - } - - async fn send_transaction_and_watch( - &self, - pending: PendingSubscriptionSink, - transaction: AddressedInjectedTransaction, - ) -> SubscriptionResult { - let tx_hash = transaction.tx.data().to_hash(); - tracing::trace!(%tx_hash, "Called injected_subscribeTransactionPromise"); - self.metrics.send_and_watch_injected_tx_calls.increment(1); - - // Check, that transaction wasn't already send. - if self.promise_waiters.get(&tx_hash).is_some() { - tracing::warn!(tx_hash = ?tx_hash, "transaction was already sent"); - return Err( - format!("transaction with the same hash was already sent: {tx_hash}").into(), - ); - } - - let _acceptance = self.forward_transaction(transaction).await?; - - // Try accept subscription, if some errors occur, just log them and return error to client. - let subscription_sink = pending.accept().await.inspect_err(|err| { - tracing::warn!("failed to accept subscription for injected transaction promise: {err}"); - })?; - - let (promise_sender, promise_receiver) = oneshot::channel(); - self.promise_waiters.insert(tx_hash, promise_sender); - self.spawn_promise_waiter(subscription_sink, promise_receiver, tx_hash); - - Ok(()) - } - - async fn get_transaction_promise( - &self, - tx_hash: HashOf, - ) -> RpcResult> { - let Some(promise) = self.db.promise(tx_hash) else { - tracing::trace!(?tx_hash, "promise not found for injected transaction"); - return Ok(None); - }; - - let Some((signature, address)) = self.db.promise_signature(tx_hash) else { - tracing::trace!( - ?tx_hash, - "promise signature not found for injected transaction" - ); - return Ok(None); - }; - - match SignedMessage::try_from_parts(promise, signature, address) { - Ok(message) => Ok(Some(message)), - Err(err) => { - tracing::trace!( - ?tx_hash, - ?err, - "failed to build signed promise from parts for injected transaction" - ); - Ok(None) - } - } - } -} - -impl InjectedApi { - pub(crate) fn new(db: Database, rpc_sender: mpsc::UnboundedSender) -> Self { - Self { - db, - rpc_sender, - promise_waiters: PromiseWaiters::default(), - promises_computation_waiting: Default::default(), - metrics: InjectedApiMetrics::default(), - } - } - - pub fn receive_compact_promise(&self, compact_promise: CompactSignedPromise) { - match self.db.promise(compact_promise.data().tx_hash) { - Some(promise) => { - tracing::trace!(tx_hash = ?promise.tx_hash, "Promise already computed, sending to user..."); - self.db.set_promise_signature( - promise.tx_hash, - *compact_promise.signature(), - compact_promise.address(), - ); - self.send_promise(promise, compact_promise); - } - None => { - tracing::trace!(tx_hash = ?compact_promise.data().tx_hash, "Promise doesn't compute yet, waiting for producer's signature"); - self.promises_computation_waiting - .insert(compact_promise.data().tx_hash, compact_promise); - } - } - } - - pub fn receive_raw_promise(&self, promise: Promise) { - // In case of `None` nothing to do, because of promise already in RPC database. - if let Some((_, compact_promise)) = - self.promises_computation_waiting.remove(&promise.tx_hash) - { - let (signature, address) = (*compact_promise.signature(), compact_promise.address()); - self.db - .set_promise_signature(promise.tx_hash, signature, address); - self.send_promise(promise, compact_promise); - } - } - - pub fn send_promise(&self, promise: Promise, compact_promise: CompactSignedPromise) { - // Check the promise waiter firstly to avoid unnecessary computation. - let Some((_, promise_sender)) = - self.promise_waiters.remove(&compact_promise.data().tx_hash) - else { - tracing::warn!( - ?promise, - "receive unregistered promise for injected transaction" - ); - return; - }; - - let (address, signature) = (compact_promise.address(), *compact_promise.signature()); - - let Ok(message) = SignedMessage::try_from_parts(promise.clone(), signature, address) else { - tracing::trace!( - ?promise, - ?compact_promise, - "failed to build `SignedMessage` from parts, invalid signature" - ); - return; - }; - self.metrics.injected_tx_active_subscriptions.decrement(1); - - match promise_sender.send(message.clone()) { - Ok(()) => { - self.metrics.injected_tx_promises_given.increment(1); - tracing::trace!(promise = ?promise, "sent promise to subscriber"); - } - Err(promise) => tracing::trace!(promise = ?promise, "rpc promise receiver dropped"), - } - } - - /// Returns the number of current promise subscribers waiting for promises. - #[cfg(test)] - pub fn promise_subscribers_count(&self) -> usize { - self.promise_waiters.len() - } - - /// This function forwards [`RpcOrNetworkInjectedTx`] to main service and waits for its acceptance. - async fn forward_transaction( - &self, - transaction: AddressedInjectedTransaction, - ) -> Result { - let tx_hash = transaction.tx.data().to_hash(); - tracing::trace!(%tx_hash, ?transaction, "Called injected_sendTransaction with vars"); - self.metrics.send_injected_tx_calls.increment(1); - - let (response_sender, response_receiver) = oneshot::channel(); - - if transaction.tx.data().value != 0 { - tracing::warn!( - tx_hash = %tx_hash, - value = transaction.tx.data().value, - "Injected transaction with non-zero value is not supported" - ); - return Err(errors::bad_request( - "Injected transactions with non-zero value are not supported", - )); - } - - let event = RpcEvent::InjectedTransaction { - transaction, - response_sender, - }; - - if let Err(err) = self.rpc_sender.send(event) { - tracing::error!( - "Failed to send `RpcEvent::InjectedTransaction` event task: {err}. \ - The receiving end in the main service might have been dropped." - ); - return Err(errors::internal()); - } - - tracing::trace!(%tx_hash, "Accept transaction, waiting for promise"); - - response_receiver.await.map_err(|e| { - // No panic case, as a responsibility of the RPC API is fulfilled. - // The dropped sender signalizes that the main service has crashed - // or is malformed, so problems should be handled there. - tracing::error!( - "Response sender for the `RpcEvent::InjectedTransaction` was dropped: {e}" - ); - errors::internal() - }) - } - - // Spawns a task that waits for the promise and sends it to the client. - fn spawn_promise_waiter( - &self, - sink: SubscriptionSink, - receiver: oneshot::Receiver, - tx_hash: HashOf, - ) { - // This clone is cheap, as it only increases the ref count. - let promise_waiters = self.promise_waiters.clone(); - self.metrics.injected_tx_active_subscriptions.increment(1); - let metrics = self.metrics.clone(); - - tokio::spawn(async move { - // Waiting for promise or client disconnection. - let promise = tokio::select! { - result = receiver => match result { - Ok(promise) => { - promise_waiters.remove(&tx_hash); - promise - } - Err(_) => { - unreachable!("promise sender is owned by the api; it cannot be dropped before this point") - } - }, - _ = sink.closed() => { - promise_waiters.remove(&tx_hash); - metrics.injected_tx_active_subscriptions.decrement(1); - return; - }, - }; - - let promise_msg = match SubscriptionMessage::from_json(&promise) { - Ok(msg) => msg, - Err(err) => { - tracing::error!( - error = %err, - "failed to create `SubscriptionMessage` from json object" - ); - return; - } - }; - - if let Err(err) = sink.send(promise_msg).await { - tracing::warn!( - tx_hash = ?tx_hash, - error = %err, - "failed to send subscription message" - ); - } - }); - } -} \ No newline at end of file diff --git a/ethexe/rpc/src/apis/injected/mod.rs b/ethexe/rpc/src/apis/injected/mod.rs new file mode 100644 index 00000000000..433b438a4c9 --- /dev/null +++ b/ethexe/rpc/src/apis/injected/mod.rs @@ -0,0 +1,29 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pub use server::InjectedApi; +pub use r#trait::InjectedServer; + +#[cfg(feature = "client")] +pub use r#trait::InjectedClient; + +mod promise_manager; +mod relay; +mod server; +mod spawner; +mod r#trait; diff --git a/ethexe/rpc/src/apis/injected/promise_manager.rs b/ethexe/rpc/src/apis/injected/promise_manager.rs new file mode 100644 index 00000000000..7847bf90041 --- /dev/null +++ b/ethexe/rpc/src/apis/injected/promise_manager.rs @@ -0,0 +1,163 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use anyhow::Result; +use dashmap::{DashMap, mapref::entry::Entry}; +use ethexe_common::{ + HashOf, SignedMessage, + db::InjectedStorageRO, + injected::{CompactSignedPromise, InjectedTransaction, Promise, SignedPromise}, +}; +use ethexe_db::Database; +use std::{sync::Arc, time::Duration}; +use tokio::sync::oneshot; + +const MAX_PROMISE_WAITING_SECS: u64 = alloy::eips::merge::SLOT_DURATION_SECS * 5; + +// TODO !!!!!!!!!!!!!! +// I forgot about the problem - now promises produced only by producer node +// (only it compute announce with PromisePolicy::Enabled) :(( +// +// Hmm, looks like `PromisePolicy` is useless, but maybe it will be helpful in cases when +// somebody wants to host only connect node without promises. + +type PromiseSubscribers = Arc, oneshot::Sender>>; +type PromisesComputationWaiting = Arc, CompactSignedPromise>>; + +/// The manager for promise subscriptions. +#[derive(Debug, Clone)] +pub struct PromiseSubscriptionManager { + db: Database, + subscribers: PromiseSubscribers, + + waiting_for_compute: PromisesComputationWaiting, +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum RegisterWatcherError { + #[error("Subscriber for this transaction already exists, tx_hash={0}")] + AlreadyRegistered(HashOf), +} + +type TimeoutReceiver = tokio::time::Timeout>; + +pub struct PendingSubscription { + tx_hash: HashOf, + receiver: TimeoutReceiver, +} + +impl PendingSubscription { + pub fn new(tx_hash: HashOf, receiver: TimeoutReceiver) -> Self { + Self { tx_hash, receiver } + } + + pub fn into_parts(self) -> (HashOf, TimeoutReceiver) { + (self.tx_hash, self.receiver) + } +} + +impl PromiseSubscriptionManager { + pub fn new(db: Database) -> Self { + Self { + db, + subscribers: PromiseSubscribers::default(), + waiting_for_compute: PromisesComputationWaiting::default(), + } + } + + pub fn watchers(&self) -> PromiseSubscribers { + self.subscribers.clone() + } + + pub fn try_register_watcher( + &self, + tx_hash: HashOf, + ) -> Result { + match self.subscribers.entry(tx_hash) { + Entry::Occupied(_) => Err(RegisterWatcherError::AlreadyRegistered(tx_hash)), + Entry::Vacant(entry) => { + let (sender, receiver) = oneshot::channel(); + let receiver = + tokio::time::timeout(Duration::from_secs(MAX_PROMISE_WAITING_SECS), receiver); + + entry.insert(sender); + Ok(PendingSubscription::new(tx_hash, receiver)) + } + } + } + + pub fn cancel_registration( + &self, + tx_hash: HashOf, + ) -> Option> { + self.subscribers.remove(&tx_hash).map(|(_, v)| v) + } + + pub fn on_compact_promise(&self, compact: CompactSignedPromise) { + let tx_hash = compact.data().tx_hash; + match self.db.promise(tx_hash) { + Some(promise) => match utils::try_build_signed_promise(promise, &compact) { + Ok(signed_promise) => self.dispatch_promise(signed_promise), + Err(_err) => todo!(), + }, + None => { + tracing::trace!("not found promise in database, waiting for computation..."); + self.waiting_for_compute.insert(tx_hash, compact); + } + } + } + + pub fn on_computed_promise(&self, promise_tx_hash: HashOf) { + if let Some((_, compact_promise)) = self.waiting_for_compute.remove(&promise_tx_hash) { + let Some(promise) = self.db.promise(promise_tx_hash) else { + unreachable!("promise must be computed and set to database") + }; + + match utils::try_build_signed_promise(promise, &compact_promise) { + Ok(signed_promise) => self.dispatch_promise(signed_promise), + Err(_err) => {} // handle error, maybe reinsert to map. + } + } + } + + #[cfg(test)] + pub fn subscribers_count(&self) -> usize { + self.subscribers.len() + } + + fn dispatch_promise(&self, promise: SignedPromise) { + if let Some((_, sender)) = self.subscribers.remove(&promise.data().tx_hash) + && let Err(unsent_promise) = sender.send(promise) + { + tracing::trace!("failed to send promise to subscriber, promise={unsent_promise:?}"); + } + } +} + +mod utils { + use super::*; + + pub fn try_build_signed_promise( + promise: Promise, + compact: &CompactSignedPromise, + ) -> Result { + let address = compact.address(); + let signature = *compact.signature(); + SignedMessage::try_from_parts(promise, signature, address) + } +} diff --git a/ethexe/rpc/src/apis/injected/relay.rs b/ethexe/rpc/src/apis/injected/relay.rs new file mode 100644 index 00000000000..62396635163 --- /dev/null +++ b/ethexe/rpc/src/apis/injected/relay.rs @@ -0,0 +1,79 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::{RpcEvent, errors}; +use ethexe_common::injected::{AddressedInjectedTransaction, InjectedTransactionAcceptance}; +use jsonrpsee::core::RpcResult; +use tokio::sync::{mpsc, oneshot}; + +#[derive(Clone)] +pub struct TransactionsRelayer { + rpc_sender: mpsc::UnboundedSender, +} + +impl TransactionsRelayer { + pub fn new(rpc_sender: mpsc::UnboundedSender) -> Self { + Self { rpc_sender } + } + + pub async fn relay( + &self, + transaction: AddressedInjectedTransaction, + ) -> RpcResult { + let tx_hash = transaction.tx.data().to_hash(); + tracing::trace!(%tx_hash, ?transaction, "Called injected_sendTransaction with vars"); + // self.metrics.send_injected_tx_calls.increment(1); + + let (response_sender, response_receiver) = oneshot::channel(); + + let event = RpcEvent::InjectedTransaction { + transaction, + response_sender, + }; + + // TODO: maybe should implement the transaction validator. + // if transaction.tx.data().value != 0 { + // tracing::warn!( + // tx_hash = %tx_hash, + // value = transaction.tx.data().value, + // "Injected transaction with non-zero value is not supported" + // ); + // return Err(errors::bad_request( + // "Injected transactions with non-zero value are not supported", + // )); + // } + + if let Err(err) = self.rpc_sender.send(event) { + tracing::error!( + "Failed to send `RpcEvent::InjectedTransaction` event task: {err}. \ + The receiving end in the main service might have been dropped." + ); + return Err(errors::internal()); + } + + tracing::trace!(%tx_hash, "Accept transaction, waiting for promise"); + + response_receiver.await.map_err(|err| { + // Expecting no errors here, because the rpc channel is owned by main server. + tracing::error!( + "Response sender for the `RpcEvent::InjectedTransaction` was dropped: {err}" + ); + errors::internal() + }) + } +} diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs new file mode 100644 index 00000000000..4054d8327be --- /dev/null +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -0,0 +1,157 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::{RpcEvent, errors}; + +use super::{ + InjectedServer, promise_manager::PromiseSubscriptionManager, relay::TransactionsRelayer, + spawner, +}; +use ethexe_common::{ + HashOf, + injected::{ + AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, + SignedPromise, + }, +}; +use ethexe_db::Database; +use jsonrpsee::{ + core::{RpcResult, SubscriptionResult, async_trait}, + server::PendingSubscriptionSink, +}; +use std::ops::Deref; +use tokio::sync::mpsc; + +#[derive(Clone)] +pub struct InjectedApi { + manager: PromiseSubscriptionManager, + relayer: TransactionsRelayer, +} + +#[async_trait] +impl InjectedServer for InjectedApi { + async fn send_transaction( + &self, + transaction: AddressedInjectedTransaction, + ) -> RpcResult { + self.send_transaction(transaction).await + } + + async fn send_transaction_and_watch( + &self, + pending: PendingSubscriptionSink, + transaction: AddressedInjectedTransaction, + ) -> SubscriptionResult { + self.send_transaction_and_watch(pending, transaction).await + } + + async fn get_transaction_promise( + &self, + tx_hash: HashOf, + ) -> RpcResult> { + self.get_transaction_promise(tx_hash).await + } +} + +impl Deref for InjectedApi { + type Target = PromiseSubscriptionManager; + + fn deref(&self) -> &Self::Target { + &self.manager + } +} + +impl InjectedApi { + pub fn new(db: Database, rpc_sender: mpsc::UnboundedSender) -> Self { + Self { + manager: PromiseSubscriptionManager::new(db), + relayer: TransactionsRelayer::new(rpc_sender), + } + } +} + +// RPC API implementation. +impl InjectedApi { + async fn send_transaction( + &self, + transaction: AddressedInjectedTransaction, + ) -> RpcResult { + self.relayer.relay(transaction).await + } + + async fn send_transaction_and_watch( + &self, + pending: PendingSubscriptionSink, + transaction: AddressedInjectedTransaction, + ) -> SubscriptionResult { + let tx_hash = transaction.tx.data().to_hash(); + + let pending_watcher = match self.manager.try_register_watcher(tx_hash) { + Ok(watcher) => watcher, + Err(err) => { + self.manager.cancel_registration(tx_hash); + return Err(errors::bad_request(err).into()); + } + }; + + let sink = match self.relayer.relay(transaction).await? { + InjectedTransactionAcceptance::Accept => pending.accept().await?, + InjectedTransactionAcceptance::Reject { reason } => { + self.manager.cancel_registration(tx_hash); + return Err(reason.into()); + } + }; + + let watchers = self.manager.watchers(); + spawner::spawn_pending_subscription(sink, pending_watcher, move |tx_hash| { + watchers.remove(&tx_hash); + }); + Ok(()) + } + + async fn get_transaction_promise( + &self, + _tx_hash: HashOf, + ) -> RpcResult> { + // let Some(promise) = self.db.promise(tx_hash) else { + // tracing::trace!(?tx_hash, "promise not found for injected transaction"); + // return Ok(None); + // }; + + // let Some((signature, address)) = self.db.promise_signature(tx_hash) else { + // tracing::trace!( + // ?tx_hash, + // "promise signature not found for injected transaction" + // ); + // return Ok(None); + // }; + + // match SignedMessage::try_from_parts(promise, signature, address) { + // Ok(message) => Ok(Some(message)), + // Err(err) => { + // tracing::trace!( + // ?tx_hash, + // ?err, + // "failed to build signed promise from parts for injected transaction" + // ); + // Ok(None) + // } + // } + todo!() + } +} diff --git a/ethexe/rpc/src/apis/injected/spawner.rs b/ethexe/rpc/src/apis/injected/spawner.rs new file mode 100644 index 00000000000..76940f658ec --- /dev/null +++ b/ethexe/rpc/src/apis/injected/spawner.rs @@ -0,0 +1,64 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use super::promise_manager::PendingSubscription; +use ethexe_common::{HashOf, injected::InjectedTransaction}; +use jsonrpsee::{SubscriptionMessage, SubscriptionSink}; + +/// Spawns the transaction's promise watcher. +/// +/// On task finishing applies the cleanup function that is need to drop some data. +pub fn spawn_pending_subscription( + sink: SubscriptionSink, + watcher: PendingSubscription, + on_finish: F, +) where + F: FnOnce(HashOf) + std::marker::Send + 'static, +{ + let (tx_hash, receiver) = watcher.into_parts(); + + tokio::spawn(async move { + let _guard = scopeguard::guard(tx_hash, on_finish); + + // Waiting for one from: promise, timeout_err, client disconnect error. + let promise = tokio::select! { + result = receiver => match result { + Ok(promise_result) => match promise_result { + Ok(promise) => promise, + Err(_err) => { + unreachable!("promise sender is owned by the api; it cannot be dropped before this point"); + } + }, + Err(_) => { + tracing::warn!("promise wasn't received in time, finish waiting"); + return; + } + }, + _ = sink.closed() => { + tracing::trace!("subscription closed by user, stop background task"); + return; + } + }; + + // TODO: remove unwrap here + let message = SubscriptionMessage::from_json(&promise).unwrap(); + if let Err(err) = sink.send(message).await { + tracing::trace!("failed to send promise, client disconnected: err={err}"); + } + }); +} diff --git a/ethexe/rpc/src/apis/injected/trait.rs b/ethexe/rpc/src/apis/injected/trait.rs new file mode 100644 index 00000000000..b52fb14b726 --- /dev/null +++ b/ethexe/rpc/src/apis/injected/trait.rs @@ -0,0 +1,57 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethexe_common::{ + HashOf, + injected::{ + AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, + SignedPromise, + }, +}; +use jsonrpsee::{ + core::{RpcResult, SubscriptionResult}, + proc_macros::rpc, +}; + +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "injected"))] +#[cfg_attr(feature = "client", rpc(server, client, namespace = "injected"))] +pub trait Injected { + /// Just sends an injected transaction. + #[method(name = "sendTransaction")] + async fn send_transaction( + &self, + transaction: AddressedInjectedTransaction, + ) -> RpcResult; + + /// Sends an injected transaction and subscribes to its promise. + #[subscription( + name = "sendTransactionAndWatch", + unsubscribe = "sendTransactionAndWatchUnsubscribe", + item = SignedPromise + )] + async fn send_transaction_and_watch( + &self, + transaction: AddressedInjectedTransaction, + ) -> SubscriptionResult; + + #[method(name = "getTransactionPromise")] + async fn get_transaction_promise( + &self, + tx_hash: HashOf, + ) -> RpcResult>; +} diff --git a/ethexe/rpc/src/apis/mod.rs b/ethexe/rpc/src/apis/mod.rs index 8ed642f4ca1..66fc328eeb0 100644 --- a/ethexe/rpc/src/apis/mod.rs +++ b/ethexe/rpc/src/apis/mod.rs @@ -21,12 +21,11 @@ mod code; mod injected; mod program; -pub use block::{BlockApi, BlockServer}; -pub use code::{CodeApi, CodeServer}; -pub use injected::{InjectedApi, InjectedServer}; -pub use program::{FullProgramState, ProgramApi, ProgramServer}; - #[cfg(feature = "client")] pub use crate::apis::{ block::BlockClient, code::CodeClient, injected::InjectedClient, program::ProgramClient, }; +pub use block::{BlockApi, BlockServer}; +pub use code::{CodeApi, CodeServer}; +pub use injected::{InjectedApi, InjectedServer}; +pub use program::{FullProgramState, ProgramApi, ProgramServer}; diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index 8e8eebe022b..b56a413cf92 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -24,8 +24,12 @@ use apis::{ BlockApi, BlockServer, CodeApi, CodeServer, InjectedApi, InjectedServer, ProgramApi, ProgramServer, }; -use ethexe_common::injected::{ - AddressedInjectedTransaction, CompactSignedPromise, InjectedTransactionAcceptance, Promise, +use ethexe_common::{ + HashOf, + injected::{ + AddressedInjectedTransaction, CompactSignedPromise, InjectedTransaction, + InjectedTransactionAcceptance, + }, }; use ethexe_db::Database; use ethexe_processor::{Processor, ProcessorConfig}; @@ -152,14 +156,15 @@ impl RpcService { } } - pub fn receive_raw_promise(&self, promise: Promise) { - self.injected_api.receive_raw_promise(promise); + pub fn provide_promise_computed(&self, promise_tx_hash: HashOf) { + self.injected_api.on_computed_promise(promise_tx_hash); } pub fn provide_compact_promise(&self, compact_promise: CompactSignedPromise) { - self.injected_api.receive_compact_promise(compact_promise); + self.injected_api.on_compact_promise(compact_promise); } + #[cfg(test)] pub fn provide_compact_promises(&self, compact_promises: Vec) { compact_promises .into_iter() diff --git a/ethexe/rpc/src/metrics.rs b/ethexe/rpc/src/metrics.rs index 4c05d353e6c..4cff4afb3a4 100644 --- a/ethexe/rpc/src/metrics.rs +++ b/ethexe/rpc/src/metrics.rs @@ -22,6 +22,8 @@ use metrics::{Counter, Gauge}; // TODO kuzmindev: add metrics for all RPC apis, e.g number of calls, latency, errors, etc. +// TODO: remove this unused +#[allow(unused)] /// Metrics for the Injected RPC API. #[derive(Clone, metrics_derive::Metrics)] #[metrics(scope = "ethexe_rpc_injected_api")] diff --git a/ethexe/rpc/src/tests.rs b/ethexe/rpc/src/tests.rs index ca252eb6514..81a932c7c68 100644 --- a/ethexe/rpc/src/tests.rs +++ b/ethexe/rpc/src/tests.rs @@ -91,7 +91,7 @@ impl MockService { txs.into_iter() .map(|tx| { let promise = Promise::mock(tx.tx.data().to_hash()); - self.db.set_promise(promise.clone()); + self.db.set_promise(&promise); CompactSignedPromise::create_from_promise(pk.clone(), &promise).unwrap() }) .collect() @@ -114,7 +114,7 @@ async fn start_new_server(listen_addr: SocketAddr, db: Database) -> (ServerHandl /// This helper function waits until all promise subscriptions being closed and cleaned up. async fn wait_for_closed_subscriptions(injected_api: InjectedApi) { - while injected_api.promise_subscribers_count() > 0 { + while injected_api.subscribers_count() > 0 { tokio::time::sleep(std::time::Duration::from_millis(10)).await; } } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 9b1422d854b..8acbf73c234 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -24,7 +24,10 @@ use alloy::{ use anyhow::{Context, Result}; use async_trait::async_trait; use ethexe_blob_loader::{BlobLoader, BlobLoaderEvent, BlobLoaderService, ConsensusLayerConfig}; -use ethexe_common::{COMMITMENT_DELAY_LIMIT, gear::CodeState, network::VerifiedValidatorMessage}; +use ethexe_common::{ + COMMITMENT_DELAY_LIMIT, PromiseEmissionMode, db::InjectedStorageRW, gear::CodeState, + network::VerifiedValidatorMessage, +}; use ethexe_compute::{ComputeConfig, ComputeEvent, ComputeService}; use ethexe_consensus::{ ConnectService, ConsensusEvent, ConsensusService, ValidatorConfig, ValidatorService, @@ -238,6 +241,17 @@ impl Service { None }; + let rpc = config + .rpc + .as_ref() + .map(|config| RpcServer::new(config.clone(), db.clone())); + + let promise_emission_mode = if rpc.is_some() { + PromiseEmissionMode::AlwaysEmit + } else { + PromiseEmissionMode::ConsensusDriven + }; + let observer = ObserverService::new(&config.ethereum, config.node.eth_max_sync_depth, db.clone()) .await @@ -320,6 +334,7 @@ impl Service { producer_delay: Duration::ZERO, router_address: config.ethereum.router_address, chain_deepness_threshold: config.node.chain_deepness_threshold, + promise_emission_mode, }, )?) } else { @@ -365,11 +380,6 @@ impl Service { None }; - let rpc = config - .rpc - .as_ref() - .map(|config| RpcServer::new(config.clone(), db.clone())); - let compute_config = ComputeConfig::new(config.node.canonical_quarantine); let processor_config = ProcessorConfig { chunk_size: config.node.chunk_processing_threads, @@ -549,6 +559,7 @@ impl Service { // Nothing } ComputeEvent::Promise(promise, announce_hash) => { + self.db.set_promise(&promise); consensus.receive_promise_for_signing(promise, announce_hash)?; } }, diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index 8b77f32736f..b97d4c7b233 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -31,8 +31,8 @@ use alloy::{ use anyhow::Context; use ethexe_blob_loader::{BlobLoader, BlobLoaderService, ConsensusLayerConfig}; use ethexe_common::{ - Address, COMMITMENT_DELAY_LIMIT, CodeAndId, DEFAULT_BLOCK_GAS_LIMIT, SimpleBlockData, ToDigest, - ValidatorsVec, + Address, COMMITMENT_DELAY_LIMIT, CodeAndId, DEFAULT_BLOCK_GAS_LIMIT, PromiseEmissionMode, + SimpleBlockData, ToDigest, ValidatorsVec, consensus::DEFAULT_CHAIN_DEEPNESS_THRESHOLD, ecdsa::{PrivateKey, PublicKey, SignedData}, events::{ @@ -889,6 +889,17 @@ impl Node { let processor = Processor::new(self.db.clone()).unwrap(); let compute = ComputeService::new(self.compute_config, self.db.clone(), processor); + let rpc = self + .service_rpc_config + .as_ref() + .map(|service_rpc_config| RpcServer::new(service_rpc_config.clone(), self.db.clone())); + + let promise_emission_mode = if rpc.is_some() { + PromiseEmissionMode::AlwaysEmit + } else { + PromiseEmissionMode::ConsensusDriven + }; + let observer = ObserverService::new(&self.eth_cfg, u32::MAX, self.db.clone()) .await .unwrap(); @@ -935,6 +946,7 @@ impl Node { producer_delay: self.block_time / 6, router_address: self.eth_cfg.router_address, chain_deepness_threshold: DEFAULT_CHAIN_DEEPNESS_THRESHOLD, + promise_emission_mode, }, ) .unwrap(), @@ -974,11 +986,6 @@ impl Node { self.multiaddr = Some(format!("{addr}/p2p/{peer_id}")); } - let rpc = self - .service_rpc_config - .as_ref() - .map(|service_rpc_config| RpcServer::new(service_rpc_config.clone(), self.db.clone())); - self.receiver = Some(receiver); let service = Service::new_from_parts( From c79e07be773a2d8fdab8a6353ae89e02e9a7b0ab Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 9 Mar 2026 15:56:59 +0300 Subject: [PATCH 33/59] remove store promise in db from service to rpc --- ethexe/common/src/primitives.rs | 3 +- ethexe/compute/src/compute.rs | 5 -- ethexe/consensus/src/validator/mock.rs | 1 + .../rpc/src/apis/injected/promise_manager.rs | 11 ++-- ethexe/rpc/src/apis/injected/server.rs | 61 +++++++++++-------- ethexe/rpc/src/lib.rs | 21 ++----- ethexe/rpc/src/tests.rs | 4 +- ethexe/service/src/lib.rs | 12 ++-- 8 files changed, 56 insertions(+), 62 deletions(-) diff --git a/ethexe/common/src/primitives.rs b/ethexe/common/src/primitives.rs index 921bc729a67..b69d1e7eca8 100644 --- a/ethexe/common/src/primitives.rs +++ b/ethexe/common/src/primitives.rs @@ -129,13 +129,12 @@ impl ToDigest for Announce { } } -#[derive(Clone, Debug, Copy, Default, PartialEq, Eq, derive_more::IsVariant)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, derive_more::IsVariant)] pub enum PromiseEmissionMode { /// Node should always emit promises during announces execution. /// Always set [`PromisePolicy::Enabled`]. AlwaysEmit, /// [`PromisePolicy`] is set by consensus service. - #[default] ConsensusDriven, } diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 61729ffbb11..08cc757f148 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -189,11 +189,6 @@ impl ComputeSubService

{ }) .ok_or(ComputeError::LatestDataNotFound)?; - // TODO: store injected promises in db - // promises.clone().into_iter().for_each(|promise| { - // db.set_promise(promise); - // }); - Ok(announce_hash) } } diff --git a/ethexe/consensus/src/validator/mock.rs b/ethexe/consensus/src/validator/mock.rs index b503c1e0f23..9cd39f57e8e 100644 --- a/ethexe/consensus/src/validator/mock.rs +++ b/ethexe/consensus/src/validator/mock.rs @@ -165,6 +165,7 @@ pub fn mock_validator_context() -> (ValidatorContext, Vec, MockEthere chain_deepness_threshold: DEFAULT_CHAIN_DEEPNESS_THRESHOLD, commitment_delay_limit: COMMITMENT_DELAY_LIMIT, producer_delay: Duration::from_millis(1), + promise_emission_mode: PromiseEmissionMode::ConsensusDriven, }, pending_events: VecDeque::new(), output: VecDeque::new(), diff --git a/ethexe/rpc/src/apis/injected/promise_manager.rs b/ethexe/rpc/src/apis/injected/promise_manager.rs index 7847bf90041..aa3b6fa1b03 100644 --- a/ethexe/rpc/src/apis/injected/promise_manager.rs +++ b/ethexe/rpc/src/apis/injected/promise_manager.rs @@ -20,7 +20,7 @@ use anyhow::Result; use dashmap::{DashMap, mapref::entry::Entry}; use ethexe_common::{ HashOf, SignedMessage, - db::InjectedStorageRO, + db::{InjectedStorageRO, InjectedStorageRW}, injected::{CompactSignedPromise, InjectedTransaction, Promise, SignedPromise}, }; use ethexe_db::Database; @@ -122,12 +122,11 @@ impl PromiseSubscriptionManager { } } - pub fn on_computed_promise(&self, promise_tx_hash: HashOf) { - if let Some((_, compact_promise)) = self.waiting_for_compute.remove(&promise_tx_hash) { - let Some(promise) = self.db.promise(promise_tx_hash) else { - unreachable!("promise must be computed and set to database") - }; + pub fn on_computed_promise(&self, promise: Promise) { + // Set computed promise to RPC database + self.db.set_promise(&promise); + if let Some((_, compact_promise)) = self.waiting_for_compute.remove(&promise.tx_hash) { match utils::try_build_signed_promise(promise, &compact_promise) { Ok(signed_promise) => self.dispatch_promise(signed_promise), Err(_err) => {} // handle error, maybe reinsert to map. diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index 4054d8327be..28ea6dd5332 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -23,7 +23,8 @@ use super::{ spawner, }; use ethexe_common::{ - HashOf, + HashOf, SignedMessage, + db::InjectedStorageRO, injected::{ AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, SignedPromise, @@ -39,6 +40,7 @@ use tokio::sync::mpsc; #[derive(Clone)] pub struct InjectedApi { + db: Database, manager: PromiseSubscriptionManager, relayer: TransactionsRelayer, } @@ -79,6 +81,7 @@ impl Deref for InjectedApi { impl InjectedApi { pub fn new(db: Database, rpc_sender: mpsc::UnboundedSender) -> Self { Self { + db: db.clone(), manager: PromiseSubscriptionManager::new(db), relayer: TransactionsRelayer::new(rpc_sender), } @@ -126,32 +129,36 @@ impl InjectedApi { async fn get_transaction_promise( &self, - _tx_hash: HashOf, + tx_hash: HashOf, ) -> RpcResult> { - // let Some(promise) = self.db.promise(tx_hash) else { - // tracing::trace!(?tx_hash, "promise not found for injected transaction"); - // return Ok(None); - // }; - - // let Some((signature, address)) = self.db.promise_signature(tx_hash) else { - // tracing::trace!( - // ?tx_hash, - // "promise signature not found for injected transaction" - // ); - // return Ok(None); - // }; - - // match SignedMessage::try_from_parts(promise, signature, address) { - // Ok(message) => Ok(Some(message)), - // Err(err) => { - // tracing::trace!( - // ?tx_hash, - // ?err, - // "failed to build signed promise from parts for injected transaction" - // ); - // Ok(None) - // } - // } - todo!() + if self.db.injected_transaction(tx_hash).is_some() { + // TODO: add error message here + return Err(errors::bad_request("")); + } + + let Some(promise) = self.db.promise(tx_hash) else { + tracing::trace!(?tx_hash, "promise not found for injected transaction"); + return Ok(None); + }; + + let Some((signature, address)) = self.db.promise_signature(tx_hash) else { + tracing::trace!( + ?tx_hash, + "promise signature not found for injected transaction" + ); + return Ok(None); + }; + + match SignedMessage::try_from_parts(promise, signature, address) { + Ok(message) => Ok(Some(message)), + Err(err) => { + tracing::trace!( + ?tx_hash, + ?err, + "failed to build signed promise from parts for injected transaction" + ); + Ok(None) + } + } } } diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index b56a413cf92..2aaa5540c7f 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -24,12 +24,8 @@ use apis::{ BlockApi, BlockServer, CodeApi, CodeServer, InjectedApi, InjectedServer, ProgramApi, ProgramServer, }; -use ethexe_common::{ - HashOf, - injected::{ - AddressedInjectedTransaction, CompactSignedPromise, InjectedTransaction, - InjectedTransactionAcceptance, - }, +use ethexe_common::injected::{ + AddressedInjectedTransaction, CompactSignedPromise, InjectedTransactionAcceptance, Promise, }; use ethexe_db::Database; use ethexe_processor::{Processor, ProcessorConfig}; @@ -156,20 +152,13 @@ impl RpcService { } } - pub fn provide_promise_computed(&self, promise_tx_hash: HashOf) { - self.injected_api.on_computed_promise(promise_tx_hash); + pub fn receive_computed_promise(&self, promise: Promise) { + self.injected_api.on_computed_promise(promise); } - pub fn provide_compact_promise(&self, compact_promise: CompactSignedPromise) { + pub fn receive_compact_promise(&self, compact_promise: CompactSignedPromise) { self.injected_api.on_compact_promise(compact_promise); } - - #[cfg(test)] - pub fn provide_compact_promises(&self, compact_promises: Vec) { - compact_promises - .into_iter() - .for_each(|compact_promise| self.provide_compact_promise(compact_promise)); - } } impl Stream for RpcService { diff --git a/ethexe/rpc/src/tests.rs b/ethexe/rpc/src/tests.rs index 81a932c7c68..9764f1da864 100644 --- a/ethexe/rpc/src/tests.rs +++ b/ethexe/rpc/src/tests.rs @@ -67,7 +67,9 @@ impl MockService { tokio::select! { _ = tx_batch_interval.tick() => { let promises = self.promises_bundle(tx_batch.drain(..)); - self.rpc.provide_compact_promises(promises); + promises.into_iter().for_each(|promise| { + self.rpc.receive_compact_promise(promise); + }); }, _ = self.handle.clone().stopped() => { unreachable!("RPC server should not be stopped during the test") diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 8acbf73c234..e7c577647c6 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -25,8 +25,7 @@ use anyhow::{Context, Result}; use async_trait::async_trait; use ethexe_blob_loader::{BlobLoader, BlobLoaderEvent, BlobLoaderService, ConsensusLayerConfig}; use ethexe_common::{ - COMMITMENT_DELAY_LIMIT, PromiseEmissionMode, db::InjectedStorageRW, gear::CodeState, - network::VerifiedValidatorMessage, + COMMITMENT_DELAY_LIMIT, PromiseEmissionMode, gear::CodeState, network::VerifiedValidatorMessage, }; use ethexe_compute::{ComputeConfig, ComputeEvent, ComputeService}; use ethexe_consensus::{ @@ -559,7 +558,10 @@ impl Service { // Nothing } ComputeEvent::Promise(promise, announce_hash) => { - self.db.set_promise(&promise); + if let Some(ref rpc) = rpc { + rpc.receive_computed_promise(promise.clone()); + } + consensus.receive_promise_for_signing(promise, announce_hash)?; } }, @@ -608,7 +610,7 @@ impl Service { }, NetworkEvent::PromiseMessage(compact_promise) => { if let Some(rpc) = &rpc { - rpc.provide_compact_promise(compact_promise); + rpc.receive_compact_promise(compact_promise); } } NetworkEvent::ValidatorIdentityUpdated(_) @@ -662,7 +664,7 @@ impl Service { } if let Some(rpc) = &rpc { - rpc.provide_compact_promise(compact_promise.clone()); + rpc.receive_compact_promise(compact_promise.clone()); } if let Some(network) = &mut network { From 5d884e415456fcdc4e50094cf6de789716569771 Mon Sep 17 00:00:00 2001 From: Dmitri Date: Thu, 19 Mar 2026 12:44:51 +0100 Subject: [PATCH 34/59] feat(ethexe/rpc): add method to get injected transaction (#5233) --- ethexe/rpc/src/apis/injected/server.rs | 20 +++++++++++++++++++- ethexe/rpc/src/apis/injected/trait.rs | 8 +++++++- ethexe/rpc/src/errors.rs | 4 ++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index 28ea6dd5332..7f99d10350e 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -27,7 +27,7 @@ use ethexe_common::{ db::InjectedStorageRO, injected::{ AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, - SignedPromise, + SignedInjectedTransaction, SignedPromise, }, }; use ethexe_db::Database; @@ -68,6 +68,13 @@ impl InjectedServer for InjectedApi { ) -> RpcResult> { self.get_transaction_promise(tx_hash).await } + + async fn get_transaction( + &self, + tx_hash: HashOf, + ) -> RpcResult { + self.get_transaction(tx_hash).await + } } impl Deref for InjectedApi { @@ -161,4 +168,15 @@ impl InjectedApi { } } } + + async fn get_transaction( + &self, + tx_hash: HashOf, + ) -> RpcResult { + let Some(tx) = self.db.injected_transaction(tx_hash) else { + return Err(errors::not_found()); + }; + + Ok(tx) + } } diff --git a/ethexe/rpc/src/apis/injected/trait.rs b/ethexe/rpc/src/apis/injected/trait.rs index b52fb14b726..da2af5dc3bb 100644 --- a/ethexe/rpc/src/apis/injected/trait.rs +++ b/ethexe/rpc/src/apis/injected/trait.rs @@ -20,7 +20,7 @@ use ethexe_common::{ HashOf, injected::{ AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, - SignedPromise, + SignedInjectedTransaction, SignedPromise, }, }; use jsonrpsee::{ @@ -54,4 +54,10 @@ pub trait Injected { &self, tx_hash: HashOf, ) -> RpcResult>; + + #[method(name = "getTransaction")] + async fn get_transaction( + &self, + tx_hash: HashOf, + ) -> RpcResult; } diff --git a/ethexe/rpc/src/errors.rs b/ethexe/rpc/src/errors.rs index e36010a1587..b2ccf1e19c7 100644 --- a/ethexe/rpc/src/errors.rs +++ b/ethexe/rpc/src/errors.rs @@ -32,6 +32,10 @@ pub fn bad_request(err: impl ToString) -> ErrorObject<'static> { ErrorObject::owned(8000, "Bad request", Some(err.to_string())) } +pub fn not_found() -> ErrorObject<'static> { + ErrorObject::owned(8000, "Not found", None::<&str>) +} + pub fn internal() -> ErrorObject<'static> { ErrorObject::owned(8000, "Internal error", None::<&str>) } From af5babcbec3549fd42e7c286b8dc7c416873ac72 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Thu, 9 Apr 2026 17:30:42 +0300 Subject: [PATCH 35/59] small log fixes --- .../rpc/src/apis/injected/promise_manager.rs | 6 ++-- ethexe/rpc/src/apis/injected/relay.rs | 31 +++++++++---------- ethexe/rpc/src/apis/injected/server.rs | 12 +++---- ethexe/rpc/src/apis/injected/spawner.rs | 18 ++++++----- 4 files changed, 33 insertions(+), 34 deletions(-) diff --git a/ethexe/rpc/src/apis/injected/promise_manager.rs b/ethexe/rpc/src/apis/injected/promise_manager.rs index aa3b6fa1b03..4d4dc28f0d2 100644 --- a/ethexe/rpc/src/apis/injected/promise_manager.rs +++ b/ethexe/rpc/src/apis/injected/promise_manager.rs @@ -26,6 +26,7 @@ use ethexe_common::{ use ethexe_db::Database; use std::{sync::Arc, time::Duration}; use tokio::sync::oneshot; +use tracing::trace; const MAX_PROMISE_WAITING_SECS: u64 = alloy::eips::merge::SLOT_DURATION_SECS * 5; @@ -116,7 +117,7 @@ impl PromiseSubscriptionManager { Err(_err) => todo!(), }, None => { - tracing::trace!("not found promise in database, waiting for computation..."); + trace!("not found promise in database, waiting for computation..."); self.waiting_for_compute.insert(tx_hash, compact); } } @@ -143,7 +144,7 @@ impl PromiseSubscriptionManager { if let Some((_, sender)) = self.subscribers.remove(&promise.data().tx_hash) && let Err(unsent_promise) = sender.send(promise) { - tracing::trace!("failed to send promise to subscriber, promise={unsent_promise:?}"); + trace!("failed to send promise to subscriber, promise={unsent_promise:?}"); } } } @@ -157,6 +158,7 @@ mod utils { ) -> Result { let address = compact.address(); let signature = *compact.signature(); + SignedMessage::try_from_parts(promise, signature, address) } } diff --git a/ethexe/rpc/src/apis/injected/relay.rs b/ethexe/rpc/src/apis/injected/relay.rs index 62396635163..e7a523ad2a7 100644 --- a/ethexe/rpc/src/apis/injected/relay.rs +++ b/ethexe/rpc/src/apis/injected/relay.rs @@ -20,6 +20,7 @@ use crate::{RpcEvent, errors}; use ethexe_common::injected::{AddressedInjectedTransaction, InjectedTransactionAcceptance}; use jsonrpsee::core::RpcResult; use tokio::sync::{mpsc, oneshot}; +use tracing::{error, trace, warn}; #[derive(Clone)] pub struct TransactionsRelayer { @@ -36,7 +37,7 @@ impl TransactionsRelayer { transaction: AddressedInjectedTransaction, ) -> RpcResult { let tx_hash = transaction.tx.data().to_hash(); - tracing::trace!(%tx_hash, ?transaction, "Called injected_sendTransaction with vars"); + trace!(%tx_hash, ?transaction, "Called injected_sendTransaction with vars"); // self.metrics.send_injected_tx_calls.increment(1); let (response_sender, response_receiver) = oneshot::channel(); @@ -47,32 +48,30 @@ impl TransactionsRelayer { }; // TODO: maybe should implement the transaction validator. - // if transaction.tx.data().value != 0 { - // tracing::warn!( - // tx_hash = %tx_hash, - // value = transaction.tx.data().value, - // "Injected transaction with non-zero value is not supported" - // ); - // return Err(errors::bad_request( - // "Injected transactions with non-zero value are not supported", - // )); - // } + if transaction.tx.data().value != 0 { + warn!( + tx_hash = %tx_hash, + value = transaction.tx.data().value, + "Injected transaction with non-zero value is not supported" + ); + return Err(errors::bad_request( + "Injected transactions with non-zero value are not supported", + )); + } if let Err(err) = self.rpc_sender.send(event) { - tracing::error!( + error!( "Failed to send `RpcEvent::InjectedTransaction` event task: {err}. \ The receiving end in the main service might have been dropped." ); return Err(errors::internal()); } - tracing::trace!(%tx_hash, "Accept transaction, waiting for promise"); + trace!(%tx_hash, "Accept transaction, waiting for promise"); response_receiver.await.map_err(|err| { // Expecting no errors here, because the rpc channel is owned by main server. - tracing::error!( - "Response sender for the `RpcEvent::InjectedTransaction` was dropped: {err}" - ); + error!("Response sender for the `RpcEvent::InjectedTransaction` was dropped: {err}"); errors::internal() }) } diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index 28ea6dd5332..4f532fabb1c 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -37,6 +37,7 @@ use jsonrpsee::{ }; use std::ops::Deref; use tokio::sync::mpsc; +use tracing::trace; #[derive(Clone)] pub struct InjectedApi { @@ -131,18 +132,13 @@ impl InjectedApi { &self, tx_hash: HashOf, ) -> RpcResult> { - if self.db.injected_transaction(tx_hash).is_some() { - // TODO: add error message here - return Err(errors::bad_request("")); - } - let Some(promise) = self.db.promise(tx_hash) else { - tracing::trace!(?tx_hash, "promise not found for injected transaction"); + trace!(?tx_hash, "promise not found for injected transaction"); return Ok(None); }; let Some((signature, address)) = self.db.promise_signature(tx_hash) else { - tracing::trace!( + trace!( ?tx_hash, "promise signature not found for injected transaction" ); @@ -152,7 +148,7 @@ impl InjectedApi { match SignedMessage::try_from_parts(promise, signature, address) { Ok(message) => Ok(Some(message)), Err(err) => { - tracing::trace!( + trace!( ?tx_hash, ?err, "failed to build signed promise from parts for injected transaction" diff --git a/ethexe/rpc/src/apis/injected/spawner.rs b/ethexe/rpc/src/apis/injected/spawner.rs index 76940f658ec..14fc5594908 100644 --- a/ethexe/rpc/src/apis/injected/spawner.rs +++ b/ethexe/rpc/src/apis/injected/spawner.rs @@ -19,10 +19,11 @@ use super::promise_manager::PendingSubscription; use ethexe_common::{HashOf, injected::InjectedTransaction}; use jsonrpsee::{SubscriptionMessage, SubscriptionSink}; +use tracing::{warn, trace}; -/// Spawns the transaction's promise watcher. +/// Spawns [PendingSubscription] in tokio runtime. /// -/// On task finishing applies the cleanup function that is need to drop some data. +/// On task finishing applies the `on_finish` function that is need to drop some data. pub fn spawn_pending_subscription( sink: SubscriptionSink, watcher: PendingSubscription, @@ -32,25 +33,26 @@ pub fn spawn_pending_subscription( { let (tx_hash, receiver) = watcher.into_parts(); - tokio::spawn(async move { + // TODO: think about using this handle for aborting runtime tasks in case of long waiting. + let _handle = tokio::spawn(async move { let _guard = scopeguard::guard(tx_hash, on_finish); - // Waiting for one from: promise, timeout_err, client disconnect error. + // Waiting for the first one: promise, timeout_err, client disconnect error. let promise = tokio::select! { result = receiver => match result { Ok(promise_result) => match promise_result { Ok(promise) => promise, Err(_err) => { - unreachable!("promise sender is owned by the api; it cannot be dropped before this point"); + unreachable!("promise sender is owned by the server; it cannot be dropped before this point"); } }, Err(_) => { - tracing::warn!("promise wasn't received in time, finish waiting"); + warn!("promise wasn't received in time, finish waiting"); return; } }, _ = sink.closed() => { - tracing::trace!("subscription closed by user, stop background task"); + trace!("subscription closed by user, stop background task"); return; } }; @@ -58,7 +60,7 @@ pub fn spawn_pending_subscription( // TODO: remove unwrap here let message = SubscriptionMessage::from_json(&promise).unwrap(); if let Err(err) = sink.send(message).await { - tracing::trace!("failed to send promise, client disconnected: err={err}"); + trace!("failed to send promise, client disconnected: err={err}"); } }); } From c0ac4e53cb0394328a2f165387ad303322be27b5 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 10 Apr 2026 11:48:47 +0300 Subject: [PATCH 36/59] feat: implement PromiseEmissionMode --- Cargo.lock | 26 ++++++ Cargo.toml | 1 + ethexe/common/src/primitives.rs | 11 +++ ethexe/compute/Cargo.toml | 1 + ethexe/compute/src/compute.rs | 80 ++++++++++--------- ethexe/compute/src/service.rs | 17 ++-- ethexe/compute/src/tests.rs | 18 +---- .../rpc/src/apis/injected/promise_manager.rs | 8 +- ethexe/rpc/src/apis/injected/relay.rs | 13 ++- ethexe/rpc/src/apis/injected/spawner.rs | 2 +- ethexe/service/src/lib.rs | 15 +++- ethexe/service/src/tests/mod.rs | 8 +- ethexe/service/src/tests/utils/env.rs | 10 ++- 13 files changed, 125 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74aafd76548..b6b4c2cec76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2157,6 +2157,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bon" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" +dependencies = [ + "darling 0.23.0", + "ident_case", + "prettyplease 0.2.37", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.114", +] + [[package]] name = "borsh" version = "1.6.0" @@ -5161,6 +5186,7 @@ dependencies = [ name = "ethexe-compute" version = "1.10.0" dependencies = [ + "bon", "demo-ping", "derive_more 2.1.1", "ethexe-common", diff --git a/Cargo.toml b/Cargo.toml index 6cd4d89fbc9..8d1d76b4829 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -227,6 +227,7 @@ tap = "1.0.1" ntest = "0.9.3" dashmap = "5.5.3" delegate = "0.13.5" +bon = "3.9.1" # metrics metrics = "0.24.0" diff --git a/ethexe/common/src/primitives.rs b/ethexe/common/src/primitives.rs index ea275a31f21..c81092b90ab 100644 --- a/ethexe/common/src/primitives.rs +++ b/ethexe/common/src/primitives.rs @@ -161,6 +161,17 @@ pub enum PromisePolicy { Disabled, } +/// The [PromiseEmissionMode] tells setups the promises mode in ethexe node. +#[derive(Debug, Copy, Clone, PartialEq, Eq, derive_more::IsVariant, Default)] +pub enum PromiseEmissionMode { + /// Node should always emit promises during announces execution. + /// Always set [`PromisePolicy::Enabled`]. + AlwaysEmit, + /// [`PromisePolicy`] is set by consensus service. + #[default] + ConsensusDriven, +} + #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, Default, Encode, Decode, TypeInfo)] #[cfg_attr(feature = "std", derive(serde::Serialize))] pub struct StateHashWithQueueSize { diff --git a/ethexe/compute/Cargo.toml b/ethexe/compute/Cargo.toml index a964902cc3d..dd81df62952 100644 --- a/ethexe/compute/Cargo.toml +++ b/ethexe/compute/Cargo.toml @@ -22,6 +22,7 @@ derive_more.workspace = true log.workspace = true gear-workspace-hack.workspace = true future-timing.workspace = true +bon.workspace = true # metrics metrics.workspace = true diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index cb665ec9e99..f8de98e80be 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -18,7 +18,7 @@ use crate::{ComputeError, ComputeEvent, ProcessorExt, Result, service::SubService}; use ethexe_common::{ - Announce, HashOf, PromisePolicy, SimpleBlockData, + Announce, HashOf, PromiseEmissionMode, PromisePolicy, SimpleBlockData, db::{ AnnounceStorageRO, AnnounceStorageRW, BlockMetaStorageRO, CodesStorageRW, ConfigStorageRO, GlobalsStorageRW, OnChainStorageRO, @@ -37,12 +37,6 @@ use std::{ }; use tokio::sync::mpsc; -#[derive(Debug, Clone, Copy)] -pub struct ComputeConfig { - /// The delay in **blocks** in which events from Ethereum will be apply. - canonical_quarantine: u8, -} - /// Metrics for the [`ComputeSubService`]. #[derive(Clone, metrics_derive::Metrics)] #[metrics(scope = "ethexe_compute_compute")] @@ -51,25 +45,24 @@ struct Metrics { announce_processing_latency: metrics::Histogram, } -impl ComputeConfig { - /// Constructs [`ComputeConfig`] with provided `canonical_quarantine`. - /// In production builds `canonical_quarantine` should be equal [`ethexe_common::gear::CANONICAL_QUARANTINE`]. - pub fn new(canonical_quarantine: u8) -> Self { - Self { - canonical_quarantine, - } - } - - /// Must use only in testing purposes. - pub fn without_quarantine() -> Self { - Self { - canonical_quarantine: 0, - } - } +/// Configuration for [ComputeSubService]. +#[derive(Debug, Clone, Copy, bon::Builder)] +#[cfg_attr(test, derive(Default))] +pub struct ComputeConfig { + /// The delay in **blocks** in which events from Ethereum will be apply. + canonical_quarantine: u8, + /// The promises emission rule. + promises_mode: PromiseEmissionMode, +} +impl ComputeConfig { pub fn canonical_quarantine(&self) -> u8 { self.canonical_quarantine } + + pub fn promises_mode(&self) -> PromiseEmissionMode { + self.promises_mode + } } /// Type alias for computation future with timing. @@ -201,18 +194,19 @@ impl SubService for ComputeSubService

{ && self.promises_stream.is_none() && let Some((announce, promise_policy)) = self.input.pop_front() { - let maybe_promise_out_tx = match promise_policy { - PromisePolicy::Enabled => { - let (sender, receiver) = mpsc::unbounded_channel(); - self.promises_stream = Some(utils::AnnouncePromisesStream::new( - receiver, - announce.to_hash(), - )); - - Some(sender) - } - PromisePolicy::Disabled => None, - }; + let maybe_promise_out_tx = + match utils::resolve_promise_policy(promise_policy, self.config.promises_mode()) { + PromisePolicy::Enabled => { + let (sender, receiver) = mpsc::unbounded_channel(); + self.promises_stream = Some(utils::AnnouncePromisesStream::new( + receiver, + announce.to_hash(), + )); + + Some(sender) + } + PromisePolicy::Disabled => None, + }; self.computation = Some(future_timing::timed( Self::compute( @@ -274,6 +268,18 @@ pub(crate) mod utils { use futures::Stream; use std::pin::Pin; + /// Resolves [PromisePolicy] with consensus provided policy and global + /// [PromiseEmissionMode] set for node. + pub(super) fn resolve_promise_policy( + consensus_policy: PromisePolicy, + mode: PromiseEmissionMode, + ) -> PromisePolicy { + match mode { + PromiseEmissionMode::AlwaysEmit => PromisePolicy::Enabled, + PromiseEmissionMode::ConsensusDriven => consensus_policy, + } + } + /// The stream of promises from announce execution. pub(super) struct AnnouncePromisesStream { receiver: mpsc::UnboundedReceiver, @@ -533,7 +539,7 @@ mod tests { let db = Database::memory(); let block_hash = BlockChain::mock(1).setup(&db).blocks[1].hash; - let config = ComputeConfig::without_quarantine(); + let config = ComputeConfig::default(); let mut service = ComputeSubService::new( config, db.clone(), @@ -637,7 +643,7 @@ mod tests { .collect::>(); let mut compute_service = - ComputeService::new(ComputeConfig::without_quarantine(), db.clone(), processor); + ComputeService::new(ComputeConfig::default(), db.clone(), processor); // Send announces for computation. compute_service.compute_announce( @@ -733,7 +739,7 @@ mod tests { }; let mut compute_service = - ComputeService::new(ComputeConfig::without_quarantine(), db.clone(), processor); + ComputeService::new(ComputeConfig::default(), db.clone(), processor); compute_service.compute_announce(announce, PromisePolicy::Enabled); loop { diff --git a/ethexe/compute/src/service.rs b/ethexe/compute/src/service.rs index 5b96f0256a0..7d9ef81f0a6 100644 --- a/ethexe/compute/src/service.rs +++ b/ethexe/compute/src/service.rs @@ -53,9 +53,9 @@ impl ComputeService

{ #[cfg(test)] impl ComputeService { - /// Creates the processor with default [`ComputeConfig::without_quarantine`] and [`Processor`] with default config. + /// Creates the processor with default [`ComputeConfig`] and [`Processor`] with default config. pub fn new_with_defaults(db: Database) -> Self { - let config = ComputeConfig::without_quarantine(); + let config = ComputeConfig::default(); let processor = Processor::new(db.clone()).unwrap(); Self::new(config, db, processor) } @@ -64,11 +64,7 @@ impl ComputeService { #[cfg(test)] impl ComputeService { pub fn new_mock_processor(db: Database) -> Self { - Self::new( - ComputeConfig::without_quarantine(), - db, - MockProcessor::default(), - ) + Self::new(ComputeConfig::default(), db, MockProcessor::default()) } } @@ -211,11 +207,8 @@ mod tests { let db = DB::memory(); let processor = MockProcessor::with_default_valid_code() .tap_mut(|p| p.process_codes_result.as_mut().unwrap().code_id = code_id); - let mut service = ComputeService::new( - ComputeConfig::without_quarantine(), - db.clone(), - processor.clone(), - ); + let mut service = + ComputeService::new(ComputeConfig::default(), db.clone(), processor.clone()); // Create test code diff --git a/ethexe/compute/src/tests.rs b/ethexe/compute/src/tests.rs index 9f7567c6d30..fc21f9a3619 100644 --- a/ethexe/compute/src/tests.rs +++ b/ethexe/compute/src/tests.rs @@ -349,11 +349,7 @@ async fn code_validation_request_for_already_processed_code_does_not_request_loa let db = Database::memory(); let processor = MockProcessor::default(); - let mut compute = ComputeService::new( - ComputeConfig::without_quarantine(), - db.clone(), - processor.clone(), - ); + let mut compute = ComputeService::new(ComputeConfig::default(), db.clone(), processor.clone()); let code = create_new_code(1); let code_id = db.set_original_code(&code); @@ -414,11 +410,7 @@ async fn code_validation_request_for_non_validated_code_requests_loading() -> Re let db = Database::memory(); let processor = MockProcessor::default(); - let mut compute = ComputeService::new( - ComputeConfig::without_quarantine(), - db.clone(), - processor.clone(), - ); + let mut compute = ComputeService::new(ComputeConfig::default(), db.clone(), processor.clone()); let code = create_new_code(1); let code_id = db.set_original_code(&code); @@ -467,11 +459,7 @@ async fn process_code_for_already_processed_valid_code_emits_code_processed() -> let db = Database::memory(); let processor = MockProcessor::default(); - let mut compute = ComputeService::new( - ComputeConfig::without_quarantine(), - db.clone(), - processor.clone(), - ); + let mut compute = ComputeService::new(ComputeConfig::default(), db.clone(), processor.clone()); let code = create_new_code(2); let code_id = db.set_original_code(&code); diff --git a/ethexe/rpc/src/apis/injected/promise_manager.rs b/ethexe/rpc/src/apis/injected/promise_manager.rs index 4d4dc28f0d2..34dd20902b5 100644 --- a/ethexe/rpc/src/apis/injected/promise_manager.rs +++ b/ethexe/rpc/src/apis/injected/promise_manager.rs @@ -30,12 +30,8 @@ use tracing::trace; const MAX_PROMISE_WAITING_SECS: u64 = alloy::eips::merge::SLOT_DURATION_SECS * 5; -// TODO !!!!!!!!!!!!!! -// I forgot about the problem - now promises produced only by producer node -// (only it compute announce with PromisePolicy::Enabled) :(( -// -// Hmm, looks like `PromisePolicy` is useless, but maybe it will be helpful in cases when -// somebody wants to host only connect node without promises. +// TODO idea: implement `PromisesHandle` that provides two methods: `on_computed_promise` and `on_compact_promise`. +// And provide this handle outside using `fn handle(&self) -> &PromiseHandle{}` to handle events in server. type PromiseSubscribers = Arc, oneshot::Sender>>; type PromisesComputationWaiting = Arc, CompactSignedPromise>>; diff --git a/ethexe/rpc/src/apis/injected/relay.rs b/ethexe/rpc/src/apis/injected/relay.rs index e7a523ad2a7..2e6a9ab66ca 100644 --- a/ethexe/rpc/src/apis/injected/relay.rs +++ b/ethexe/rpc/src/apis/injected/relay.rs @@ -40,13 +40,6 @@ impl TransactionsRelayer { trace!(%tx_hash, ?transaction, "Called injected_sendTransaction with vars"); // self.metrics.send_injected_tx_calls.increment(1); - let (response_sender, response_receiver) = oneshot::channel(); - - let event = RpcEvent::InjectedTransaction { - transaction, - response_sender, - }; - // TODO: maybe should implement the transaction validator. if transaction.tx.data().value != 0 { warn!( @@ -59,6 +52,12 @@ impl TransactionsRelayer { )); } + let (response_sender, response_receiver) = oneshot::channel(); + let event = RpcEvent::InjectedTransaction { + transaction, + response_sender, + }; + if let Err(err) = self.rpc_sender.send(event) { error!( "Failed to send `RpcEvent::InjectedTransaction` event task: {err}. \ diff --git a/ethexe/rpc/src/apis/injected/spawner.rs b/ethexe/rpc/src/apis/injected/spawner.rs index 14fc5594908..bee62dc8daa 100644 --- a/ethexe/rpc/src/apis/injected/spawner.rs +++ b/ethexe/rpc/src/apis/injected/spawner.rs @@ -19,7 +19,7 @@ use super::promise_manager::PendingSubscription; use ethexe_common::{HashOf, injected::InjectedTransaction}; use jsonrpsee::{SubscriptionMessage, SubscriptionSink}; -use tracing::{warn, trace}; +use tracing::{trace, warn}; /// Spawns [PendingSubscription] in tokio runtime. /// diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index ceccbaf5ac5..df38194f311 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -55,7 +55,7 @@ use anyhow::{Context, Result}; use async_trait::async_trait; use ethexe_blob_loader::{BlobLoader, BlobLoaderEvent, BlobLoaderService, ConsensusLayerConfig}; use ethexe_common::{ - COMMITMENT_DELAY_LIMIT, gear::CodeState, network::VerifiedValidatorMessage, + COMMITMENT_DELAY_LIMIT, PromiseEmissionMode, gear::CodeState, network::VerifiedValidatorMessage, }; use ethexe_compute::{ComputeConfig, ComputeEvent, ComputeService}; use ethexe_consensus::{ @@ -415,7 +415,15 @@ impl Service { None }; - let compute_config = ComputeConfig::new(config.node.canonical_quarantine); + // RPC-node always requires promises + let promises_mode = match rpc.is_some() { + true => PromiseEmissionMode::AlwaysEmit, + false => PromiseEmissionMode::ConsensusDriven, + }; + let compute_config = ComputeConfig::builder() + .canonical_quarantine(config.node.canonical_quarantine) + .promises_mode(promises_mode) + .build(); let processor_config = ProcessorConfig { chunk_size: config.node.chunk_processing_threads, }; @@ -598,6 +606,9 @@ impl Service { rpc.receive_computed_promise(promise.clone()); } + // TODO: validator+rpc subordinate nodes can compute promises locally for RPC + // recovery, but they must not sign/publish them to the network unless they + // are responsible for the announce as producer. consensus.receive_promise_for_signing(promise, announce_hash)?; } }, diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index ebcf9cc5f03..8895146a2d7 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -2044,8 +2044,12 @@ async fn validators_election() { async fn execution_with_canonical_events_quarantine() { init_logger(); + let compute_config = ComputeConfig::builder() + .canonical_quarantine(CANONICAL_QUARANTINE) + .promises_mode(Default::default()) + .build(); let config = TestEnvConfig { - compute_config: ComputeConfig::new(CANONICAL_QUARANTINE), + compute_config, ..Default::default() }; let mut env = TestEnv::new(config).await.unwrap(); @@ -2490,7 +2494,6 @@ async fn injected_tx_fungible_token() { let env_config = TestEnvConfig { network: EnvNetworkConfig::Enabled, - compute_config: ComputeConfig::without_quarantine(), ..Default::default() }; @@ -2721,7 +2724,6 @@ async fn injected_tx_fungible_token_over_network() { let env_config = TestEnvConfig { network: EnvNetworkConfig::Enabled, - compute_config: ComputeConfig::without_quarantine(), ..Default::default() }; diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index 44a2fede87b..f66a4a0af57 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -806,7 +806,10 @@ impl Default for TestEnvConfig { network: EnvNetworkConfig::Disabled, deploy_params: Default::default(), commitment_delay_limit: COMMITMENT_DELAY_LIMIT, - compute_config: ComputeConfig::without_quarantine(), + compute_config: ComputeConfig::builder() + .canonical_quarantine(Default::default()) + .promises_mode(Default::default()) + .build(), } } } @@ -1041,7 +1044,10 @@ impl Node { self.multiaddr = Some(format!("{addr}/p2p/{peer_id}")); } - let rpc = self.service_rpc_config.clone().map(|config| RpcServer::new(config, self.db.clone())); + let rpc = self + .service_rpc_config + .clone() + .map(|config| RpcServer::new(config, self.db.clone())); self.receiver = Some(receiver); From 40678d23cc6173e5892c2ab24108944485ef34fc Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 10 Apr 2026 12:28:28 +0300 Subject: [PATCH 37/59] fix: all tests works correctly --- ethexe/consensus/src/connect/mod.rs | 22 ++++++++++++---------- ethexe/consensus/src/validator/producer.rs | 3 --- ethexe/service/src/lib.rs | 9 --------- ethexe/service/src/tests/mod.rs | 9 +++++++-- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/ethexe/consensus/src/connect/mod.rs b/ethexe/consensus/src/connect/mod.rs index bb007c0e279..9b9233ed05f 100644 --- a/ethexe/consensus/src/connect/mod.rs +++ b/ethexe/consensus/src/connect/mod.rs @@ -282,17 +282,19 @@ impl ConsensusService for ConnectService { fn receive_promise_for_signing( &mut self, - promise: Promise, - announce_hash: HashOf, + _promise: Promise, + _announce_hash: HashOf, ) -> Result<()> { - tracing::error!( - "Connected consensus node receives the promise for signing, but it not responsible for promises providing: \ - promise={promise:?}, announce_hash={announce_hash}" - ); - debug_assert!( - false, - "Connect node received the promise for signing, this should never happen" - ); + // TODO: remove this + + // tracing::error!( + // "Connect consensus node receives the promise for signing, but it not responsible for promises providing: \ + // promise={promise:?}, announce_hash={announce_hash}" + // ); + // debug_assert!( + // false, + // "Connect node received the promise for signing, this should never happen" + // ); Ok(()) } diff --git a/ethexe/consensus/src/validator/producer.rs b/ethexe/consensus/src/validator/producer.rs index d0ba859497d..7d5ef7d6512 100644 --- a/ethexe/consensus/src/validator/producer.rs +++ b/ethexe/consensus/src/validator/producer.rs @@ -127,9 +127,6 @@ impl StateHandler for Producer { self.ctx .output(ConsensusEvent::PublishPromise(compact_signed_promise)); - // ======= - // self.ctx.output(signed_promise); - // >>>>>>> master tracing::trace!("consensus sign promise for transaction-hash={tx_hash}"); Ok(self.into()) diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index df38194f311..121c2a8236c 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -606,9 +606,6 @@ impl Service { rpc.receive_computed_promise(promise.clone()); } - // TODO: validator+rpc subordinate nodes can compute promises locally for RPC - // recovery, but they must not sign/publish them to the network unless they - // are responsible for the announce as producer. consensus.receive_promise_for_signing(promise, announce_hash)?; } }, @@ -718,12 +715,6 @@ impl Service { if let Some(network) = &mut network { network.publish_promise(compact_promise); } - // rpc.provide_promise(signed_promise.clone()); - // } - - // if let Some(network) = &mut network { - // network.publish_promise(signed_promise); - // } } ConsensusEvent::PublishMessage(message) => { let Some(network) = network.as_mut() else { diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 8895146a2d7..c6fe7e70a17 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -30,7 +30,7 @@ use alloy::{ providers::{Provider as _, WalletProvider, ext::AnvilApi}, }; use ethexe_common::{ - Announce, HashOf, ScheduledTask, ToDigest, + Announce, HashOf, PromiseEmissionMode, ScheduledTask, ToDigest, db::*, ecdsa::ContractSignature, events::{ @@ -2718,12 +2718,17 @@ async fn injected_tx_fungible_token() { } #[tokio::test] -#[ntest::timeout(60_000)] +// TODO: up me back to 60s +#[ntest::timeout(15_000)] async fn injected_tx_fungible_token_over_network() { init_logger(); let env_config = TestEnvConfig { network: EnvNetworkConfig::Enabled, + compute_config: ComputeConfig::builder() + .canonical_quarantine(Default::default()) + .promises_mode(PromiseEmissionMode::AlwaysEmit) + .build(), ..Default::default() }; From 9bc28c15cef38c97dfa5c23537514c892dd51e53 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 10 Apr 2026 14:53:25 +0300 Subject: [PATCH 38/59] feat: add docs | make implementation clear --- ethexe/rpc/src/apis/injected/mod.rs | 38 +++++++++-- .../rpc/src/apis/injected/promise_manager.rs | 63 +++++++++---------- ethexe/rpc/src/apis/injected/relay.rs | 1 - ethexe/rpc/src/apis/injected/server.rs | 11 ++-- ethexe/rpc/src/apis/injected/spawner.rs | 10 +-- 5 files changed, 74 insertions(+), 49 deletions(-) diff --git a/ethexe/rpc/src/apis/injected/mod.rs b/ethexe/rpc/src/apis/injected/mod.rs index 433b438a4c9..12b7d67d91d 100644 --- a/ethexe/rpc/src/apis/injected/mod.rs +++ b/ethexe/rpc/src/apis/injected/mod.rs @@ -16,14 +16,40 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +//! # RPC Server Injected API +//! +//! ## Promises Flow +//! [promise_manager::PromiseSubscriptionManager] is the main entity that is responsible for +//! promises handling. +//! Internally it maintains single-promise subscribers. +//! +//! After the manager successfully registers a subscriber for +//! [ethexe_common::injected::SignedPromise], it creates the +//! [promise_manager::PendingSubscriber] and spawns it using +//! [spawner::spawn_pending_subscriber]. +//! +//! **Important:** the pending subscriber will be dropped after +//! [promise_manager::MAX_PROMISE_WAITING_SECS] seconds to avoid dead subscribers. +//! +//! [promise_manager::PromiseSubscriptionManager] provides two methods for receiving promises: +//! - [promise_manager::PromiseSubscriptionManager::on_compact_promise] receives the promise +//! signature from the producer. If it matches a promise already stored in the database, it is +//! sent to the subscriber. +//! - [promise_manager::PromiseSubscriptionManager::on_computed_promise] receives the promise +//! body. When RPC receives the corresponding promise signature, it sends the signed promise to +//! the subscriber. + +pub(crate) mod promise_manager; + +pub(crate) mod relay; + +pub(crate) mod server; pub use server::InjectedApi; + +pub(crate) mod spawner; + +mod r#trait; pub use r#trait::InjectedServer; #[cfg(feature = "client")] pub use r#trait::InjectedClient; - -mod promise_manager; -mod relay; -mod server; -mod spawner; -mod r#trait; diff --git a/ethexe/rpc/src/apis/injected/promise_manager.rs b/ethexe/rpc/src/apis/injected/promise_manager.rs index 34dd20902b5..bd88c834a67 100644 --- a/ethexe/rpc/src/apis/injected/promise_manager.rs +++ b/ethexe/rpc/src/apis/injected/promise_manager.rs @@ -28,15 +28,13 @@ use std::{sync::Arc, time::Duration}; use tokio::sync::oneshot; use tracing::trace; -const MAX_PROMISE_WAITING_SECS: u64 = alloy::eips::merge::SLOT_DURATION_SECS * 5; - -// TODO idea: implement `PromisesHandle` that provides two methods: `on_computed_promise` and `on_compact_promise`. -// And provide this handle outside using `fn handle(&self) -> &PromiseHandle{}` to handle events in server. +pub const MAX_PROMISE_WAITING: Duration = + Duration::from_secs(alloy::eips::merge::SLOT_DURATION_SECS * 20); type PromiseSubscribers = Arc, oneshot::Sender>>; type PromisesComputationWaiting = Arc, CompactSignedPromise>>; -/// The manager for promise subscriptions. +/// The manager for promise subscribers. #[derive(Debug, Clone)] pub struct PromiseSubscriptionManager { db: Database, @@ -46,20 +44,30 @@ pub struct PromiseSubscriptionManager { } #[derive(Debug, Clone, thiserror::Error)] -pub enum RegisterWatcherError { +pub enum RegisterSubscriberError { #[error("Subscriber for this transaction already exists, tx_hash={0}")] AlreadyRegistered(HashOf), } type TimeoutReceiver = tokio::time::Timeout>; -pub struct PendingSubscription { +/// The pending [SignedPromise] subscriber. +/// Subscriber will be spawned in separate tokio runtime task and will wait for promise. +/// +/// Important: to avoid infinite waiting we wrap [oneshot::Receiver] into [tokio::time::timeout]. +pub struct PendingSubscriber { + /// Tx hash waiting promise for. tx_hash: HashOf, + /// Wrapped promise [oneshot::Receiver]. receiver: TimeoutReceiver, } -impl PendingSubscription { - pub fn new(tx_hash: HashOf, receiver: TimeoutReceiver) -> Self { +impl PendingSubscriber { + pub fn new( + tx_hash: HashOf, + receiver: oneshot::Receiver, + ) -> Self { + let receiver = tokio::time::timeout(MAX_PROMISE_WAITING, receiver); Self { tx_hash, receiver } } @@ -77,23 +85,16 @@ impl PromiseSubscriptionManager { } } - pub fn watchers(&self) -> PromiseSubscribers { - self.subscribers.clone() - } - - pub fn try_register_watcher( + pub fn try_register_subscriber( &self, tx_hash: HashOf, - ) -> Result { + ) -> Result { match self.subscribers.entry(tx_hash) { - Entry::Occupied(_) => Err(RegisterWatcherError::AlreadyRegistered(tx_hash)), + Entry::Occupied(_) => Err(RegisterSubscriberError::AlreadyRegistered(tx_hash)), Entry::Vacant(entry) => { let (sender, receiver) = oneshot::channel(); - let receiver = - tokio::time::timeout(Duration::from_secs(MAX_PROMISE_WAITING_SECS), receiver); - entry.insert(sender); - Ok(PendingSubscription::new(tx_hash, receiver)) + Ok(PendingSubscriber::new(tx_hash, receiver)) } } } @@ -108,7 +109,7 @@ impl PromiseSubscriptionManager { pub fn on_compact_promise(&self, compact: CompactSignedPromise) { let tx_hash = compact.data().tx_hash; match self.db.promise(tx_hash) { - Some(promise) => match utils::try_build_signed_promise(promise, &compact) { + Some(promise) => match utils::try_signed_promise_from_parts(promise, &compact) { Ok(signed_promise) => self.dispatch_promise(signed_promise), Err(_err) => todo!(), }, @@ -124,18 +125,13 @@ impl PromiseSubscriptionManager { self.db.set_promise(&promise); if let Some((_, compact_promise)) = self.waiting_for_compute.remove(&promise.tx_hash) { - match utils::try_build_signed_promise(promise, &compact_promise) { + match utils::try_signed_promise_from_parts(promise, &compact_promise) { Ok(signed_promise) => self.dispatch_promise(signed_promise), Err(_err) => {} // handle error, maybe reinsert to map. } } } - #[cfg(test)] - pub fn subscribers_count(&self) -> usize { - self.subscribers.len() - } - fn dispatch_promise(&self, promise: SignedPromise) { if let Some((_, sender)) = self.subscribers.remove(&promise.data().tx_hash) && let Err(unsent_promise) = sender.send(promise) @@ -143,18 +139,21 @@ impl PromiseSubscriptionManager { trace!("failed to send promise to subscriber, promise={unsent_promise:?}"); } } + + #[cfg(test)] + pub fn subscribers_count(&self) -> usize { + self.subscribers.len() + } } mod utils { use super::*; - pub fn try_build_signed_promise( + /// Tries build [SignedPromise] from its parts: [CompactSignedPromise] and [Promise]. + pub fn try_signed_promise_from_parts( promise: Promise, compact: &CompactSignedPromise, ) -> Result { - let address = compact.address(); - let signature = *compact.signature(); - - SignedMessage::try_from_parts(promise, signature, address) + SignedMessage::try_from_parts(promise, *compact.signature(), compact.address()) } } diff --git a/ethexe/rpc/src/apis/injected/relay.rs b/ethexe/rpc/src/apis/injected/relay.rs index 2e6a9ab66ca..35f5d262558 100644 --- a/ethexe/rpc/src/apis/injected/relay.rs +++ b/ethexe/rpc/src/apis/injected/relay.rs @@ -38,7 +38,6 @@ impl TransactionsRelayer { ) -> RpcResult { let tx_hash = transaction.tx.data().to_hash(); trace!(%tx_hash, ?transaction, "Called injected_sendTransaction with vars"); - // self.metrics.send_injected_tx_calls.increment(1); // TODO: maybe should implement the transaction validator. if transaction.tx.data().value != 0 { diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index 44a85939623..f86a6ab6b4c 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -46,6 +46,7 @@ pub struct InjectedApi { relayer: TransactionsRelayer, } +// TODO: add metrics middleware for InjectedApi #[async_trait] impl InjectedServer for InjectedApi { async fn send_transaction( @@ -112,8 +113,8 @@ impl InjectedApi { ) -> SubscriptionResult { let tx_hash = transaction.tx.data().to_hash(); - let pending_watcher = match self.manager.try_register_watcher(tx_hash) { - Ok(watcher) => watcher, + let pending_subscriber = match self.manager.try_register_subscriber(tx_hash) { + Ok(subscriber) => subscriber, Err(err) => { self.manager.cancel_registration(tx_hash); return Err(errors::bad_request(err).into()); @@ -128,9 +129,9 @@ impl InjectedApi { } }; - let watchers = self.manager.watchers(); - spawner::spawn_pending_subscription(sink, pending_watcher, move |tx_hash| { - watchers.remove(&tx_hash); + let manager = self.manager.clone(); + spawner::spawn_pending_subscriber(sink, pending_subscriber, move |tx_hash| { + manager.cancel_registration(tx_hash); }); Ok(()) } diff --git a/ethexe/rpc/src/apis/injected/spawner.rs b/ethexe/rpc/src/apis/injected/spawner.rs index bee62dc8daa..73b00f73051 100644 --- a/ethexe/rpc/src/apis/injected/spawner.rs +++ b/ethexe/rpc/src/apis/injected/spawner.rs @@ -16,22 +16,22 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use super::promise_manager::PendingSubscription; +use super::promise_manager::PendingSubscriber; use ethexe_common::{HashOf, injected::InjectedTransaction}; use jsonrpsee::{SubscriptionMessage, SubscriptionSink}; use tracing::{trace, warn}; -/// Spawns [PendingSubscription] in tokio runtime. +/// Spawns [PendingSubscriber] in tokio runtime. /// /// On task finishing applies the `on_finish` function that is need to drop some data. -pub fn spawn_pending_subscription( +pub fn spawn_pending_subscriber( sink: SubscriptionSink, - watcher: PendingSubscription, + subscriber: PendingSubscriber, on_finish: F, ) where F: FnOnce(HashOf) + std::marker::Send + 'static, { - let (tx_hash, receiver) = watcher.into_parts(); + let (tx_hash, receiver) = subscriber.into_parts(); // TODO: think about using this handle for aborting runtime tasks in case of long waiting. let _handle = tokio::spawn(async move { From e5b849726d02b520bb0b8d4e997dbbd5241697e8 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 10 Apr 2026 16:29:10 +0300 Subject: [PATCH 39/59] fix: claude review --- ethexe/consensus/src/connect/mod.rs | 13 +++---------- .../rpc/src/apis/injected/promise_manager.rs | 14 +++++++++++--- ethexe/rpc/src/apis/injected/server.rs | 1 - ethexe/rpc/src/apis/injected/spawner.rs | 19 ++++++++++++++----- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/ethexe/consensus/src/connect/mod.rs b/ethexe/consensus/src/connect/mod.rs index 9b9233ed05f..315b25836a9 100644 --- a/ethexe/consensus/src/connect/mod.rs +++ b/ethexe/consensus/src/connect/mod.rs @@ -285,16 +285,9 @@ impl ConsensusService for ConnectService { _promise: Promise, _announce_hash: HashOf, ) -> Result<()> { - // TODO: remove this - - // tracing::error!( - // "Connect consensus node receives the promise for signing, but it not responsible for promises providing: \ - // promise={promise:?}, announce_hash={announce_hash}" - // ); - // debug_assert!( - // false, - // "Connect node received the promise for signing, this should never happen" - // ); + // Nothing to do. + // This case is not error because connect node can be also RPC node that produce promises, + // to send them for external users. Ok(()) } diff --git a/ethexe/rpc/src/apis/injected/promise_manager.rs b/ethexe/rpc/src/apis/injected/promise_manager.rs index bd88c834a67..eedaa2cd39a 100644 --- a/ethexe/rpc/src/apis/injected/promise_manager.rs +++ b/ethexe/rpc/src/apis/injected/promise_manager.rs @@ -107,11 +107,17 @@ impl PromiseSubscriptionManager { } pub fn on_compact_promise(&self, compact: CompactSignedPromise) { + trace!(?compact, "received new compact promise"); let tx_hash = compact.data().tx_hash; + match self.db.promise(tx_hash) { Some(promise) => match utils::try_signed_promise_from_parts(promise, &compact) { Ok(signed_promise) => self.dispatch_promise(signed_promise), - Err(_err) => todo!(), + Err(_err) => { + trace!( + ?compact, %tx_hash, "failed to create signed promise from parts, producer send invalid signature: compact_promise={compact:?}" + ); + } }, None => { trace!("not found promise in database, waiting for computation..."); @@ -121,13 +127,15 @@ impl PromiseSubscriptionManager { } pub fn on_computed_promise(&self, promise: Promise) { - // Set computed promise to RPC database + trace!(?promise, "received new computed promise"); self.db.set_promise(&promise); if let Some((_, compact_promise)) = self.waiting_for_compute.remove(&promise.tx_hash) { match utils::try_signed_promise_from_parts(promise, &compact_promise) { Ok(signed_promise) => self.dispatch_promise(signed_promise), - Err(_err) => {} // handle error, maybe reinsert to map. + Err(_err) => { + trace!(?compact_promise, tx_hash=?compact_promise.data().tx_hash, "failed to create signed promise from parts"); + } } } } diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index f86a6ab6b4c..f8bab5772ee 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -116,7 +116,6 @@ impl InjectedApi { let pending_subscriber = match self.manager.try_register_subscriber(tx_hash) { Ok(subscriber) => subscriber, Err(err) => { - self.manager.cancel_registration(tx_hash); return Err(errors::bad_request(err).into()); } }; diff --git a/ethexe/rpc/src/apis/injected/spawner.rs b/ethexe/rpc/src/apis/injected/spawner.rs index 73b00f73051..73f3702d814 100644 --- a/ethexe/rpc/src/apis/injected/spawner.rs +++ b/ethexe/rpc/src/apis/injected/spawner.rs @@ -19,7 +19,7 @@ use super::promise_manager::PendingSubscriber; use ethexe_common::{HashOf, injected::InjectedTransaction}; use jsonrpsee::{SubscriptionMessage, SubscriptionSink}; -use tracing::{trace, warn}; +use tracing::{error, trace, warn}; /// Spawns [PendingSubscriber] in tokio runtime. /// @@ -57,10 +57,19 @@ pub fn spawn_pending_subscriber( } }; - // TODO: remove unwrap here - let message = SubscriptionMessage::from_json(&promise).unwrap(); - if let Err(err) = sink.send(message).await { - trace!("failed to send promise, client disconnected: err={err}"); + match SubscriptionMessage::from_json(&promise) { + Ok(message) => { + if let Err(err) = sink.send(message).await { + trace!("failed to send promise, client disconnected: err={err}"); + } + } + Err(err) => { + error!( + ?promise, + ?err, + "serialization error: failed create `SubscriptionMessage` from promise; this must never happen" + ); + } } }); } From ebc38a853d3114e575713c46feafc2fab368819a Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 10 Apr 2026 18:35:07 +0300 Subject: [PATCH 40/59] feat: fix bug with compact promise store in db --- Cargo.lock | 1 + ethexe/common/Cargo.toml | 1 + ethexe/common/src/db.rs | 12 +- ethexe/common/src/injected.rs | 29 ++++ ethexe/db/src/database.rs | 46 +++--- .../rpc/src/apis/injected/promise_manager.rs | 33 ++-- ethexe/rpc/src/apis/injected/relay.rs | 144 +++++++++++++++++- ethexe/rpc/src/apis/injected/server.rs | 25 +-- 8 files changed, 224 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e5a93f7b15..1a5c9b9a459 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5180,6 +5180,7 @@ dependencies = [ "sha3", "sp-core", "tap", + "thiserror 2.0.17", ] [[package]] diff --git a/ethexe/common/Cargo.toml b/ethexe/common/Cargo.toml index 4a815e7db5d..df8d31f593f 100644 --- a/ethexe/common/Cargo.toml +++ b/ethexe/common/Cargo.toml @@ -28,6 +28,7 @@ gsigner = { workspace = true, default-features = false, features = [ sha3.workspace = true k256 = { version = "0.13.4", features = ["ecdsa"], default-features = false } nonempty.workspace = true +thiserror.workspace = true # mock deps itertools = { workspace = true, optional = true } diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index 1f5cf453912..c28aa2bc2f4 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -23,7 +23,7 @@ use crate::{ Schedule, SimpleBlockData, ValidatorsVec, events::BlockEvent, gear::StateTransition, - injected::{InjectedTransaction, Promise, SignedInjectedTransaction}, + injected::{CompactSignedPromise, InjectedTransaction, Promise, SignedInjectedTransaction}, }; use alloc::{ collections::{BTreeSet, VecDeque}, @@ -34,7 +34,6 @@ use gear_core::{ ids::{ActorId, CodeId}, }; use gprimitives::H256; -use gsigner::secp256k1::Signature; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; @@ -138,7 +137,7 @@ pub trait InjectedStorageRO { /// Returns the promise by its transaction hash. fn promise(&self, hash: HashOf) -> Option; - fn promise_signature(&self, hash: HashOf) -> Option<(Signature, Address)>; + fn compact_promise(&self, hash: HashOf) -> Option; } #[auto_impl::auto_impl(&)] @@ -147,12 +146,7 @@ pub trait InjectedStorageRW: InjectedStorageRO { fn set_promise(&self, promise: &Promise); - fn set_promise_signature( - &self, - hash: HashOf, - signature: Signature, - address: Address, - ); + fn set_compact_promise(&self, promise: &CompactSignedPromise); } #[derive(Debug, Clone, Default, Encode, Decode, TypeInfo, PartialEq, Eq, Hash)] diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index d54971f843b..e0ebe8fef08 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -227,6 +227,35 @@ impl TryFrom<&SignedPromise> for CompactSignedPromise { } } +/// Restores the [SignedPromise] from parts: [Promise], [CompactSignedPromise]. +pub fn restore_signed_promise( + promise: Promise, + compact: &CompactSignedPromise, +) -> Result { + if promise.tx_hash != compact.data().tx_hash { + return Err(RestorePromiseError::HashesMismatch { + promise_tx_hash: promise.tx_hash, + compact_tx_hash: compact.data().tx_hash, + }); + } + + SignedMessage::try_from_parts(promise, *compact.signature(), compact.address()) + .map_err(RestorePromiseError::InvalidSignature) +} + +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum RestorePromiseError { + #[error( + "promise and compact promise has different tx hashes: promise_tx_hash={promise_tx_hash:?}, compact_tx_hash={compact_tx_hash:?}" + )] + HashesMismatch { + promise_tx_hash: HashOf, + compact_tx_hash: HashOf, + }, + #[error("compact promise signature do not match promise: {0}")] + InvalidSignature(&'static str), +} + /// Encoding and decoding of `LimitedVec` as hex string. #[cfg(feature = "std")] mod serde_hex { diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index 4c92fbc2861..6678c879c70 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -34,7 +34,7 @@ use ethexe_common::{ }, events::BlockEvent, gear::StateTransition, - injected::{InjectedTransaction, Promise, SignedInjectedTransaction}, + injected::{CompactSignedPromise, InjectedTransaction, Promise, SignedInjectedTransaction}, }; use ethexe_runtime_common::state::{ Allocations, DispatchStash, Mailbox, MemoryPages, MemoryPagesRegion, MessageQueue, @@ -47,7 +47,6 @@ use gear_core::{ memory::PageBuf, }; use gprimitives::H256; -use gsigner::{Address, secp256k1::Signature}; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; use std::{ @@ -85,7 +84,7 @@ enum Key { Announces(HashOf) = 17, Promise(HashOf) = 18, - PromiseSignature(HashOf) = 19, + CompactPromise(HashOf) = 19, } impl Key { @@ -117,9 +116,9 @@ impl Key { | Self::AnnounceSchedule(hash) | Self::AnnounceMeta(hash) => bytes.extend(hash.as_ref()), - Self::InjectedTransaction(hash) - | Self::Promise(hash) - | Self::PromiseSignature(hash) => bytes.extend(hash.inner().as_ref()), + Self::InjectedTransaction(hash) | Self::Promise(hash) | Self::CompactPromise(hash) => { + bytes.extend(hash.inner().as_ref()) + } Self::ProgramToCodeId(program_id) => bytes.extend(program_id.as_ref()), @@ -716,12 +715,15 @@ impl InjectedStorageRO for RawDatabase { }) } - fn promise_signature(&self, hash: HashOf) -> Option<(Signature, Address)> { + fn compact_promise( + &self, + tx_hash: HashOf, + ) -> Option { self.kv - .get(&Key::PromiseSignature(hash).to_bytes()) + .get(&Key::CompactPromise(tx_hash).to_bytes()) .map(|data| { - <(Signature, Address)>::decode(&mut data.as_slice()) - .expect("Failed to decode data into `(Signature, Address)`") + CompactSignedPromise::decode(&mut data.as_slice()) + .expect("Failed to decode data into CompactSignedPromise") }) } } @@ -742,18 +744,12 @@ impl InjectedStorageRW for RawDatabase { .put(&Key::Promise(promise.tx_hash).to_bytes(), promise.encode()) } - fn set_promise_signature( - &self, - hash: HashOf, - signature: Signature, - address: Address, - ) { - tracing::trace!(tx_hash = ?hash, ?signature, ?address, "Set signature for injected transaction promise"); + fn set_compact_promise(&self, promise: &CompactSignedPromise) { + let tx_hash = promise.data().tx_hash; + tracing::trace!(?promise, "Set compact promise for injected transaction"); - self.kv.put( - &Key::PromiseSignature(hash).to_bytes(), - (signature, address).encode(), - ); + self.kv + .put(&Key::CompactPromise(tx_hash).to_bytes(), promise.encode()) } } @@ -955,7 +951,7 @@ impl InjectedStorageRO for Database { delegate!(to self.raw { fn injected_transaction(&self, hash: HashOf) -> Option; fn promise(&self, hash: HashOf) -> Option; - fn promise_signature(&self, hash: HashOf) -> Option<(Signature, Address)>; + fn compact_promise(&self, hash: HashOf) -> Option; }); } @@ -963,11 +959,7 @@ impl InjectedStorageRW for Database { delegate!(to self.raw { fn set_injected_transaction(&self, tx: SignedInjectedTransaction); fn set_promise(&self, promise: &Promise); - fn set_promise_signature(&self, - hash: HashOf, - signature: Signature, - address: Address, - ); + fn set_compact_promise(&self, promise: &CompactSignedPromise); }); } diff --git a/ethexe/rpc/src/apis/injected/promise_manager.rs b/ethexe/rpc/src/apis/injected/promise_manager.rs index eedaa2cd39a..d55f454eef7 100644 --- a/ethexe/rpc/src/apis/injected/promise_manager.rs +++ b/ethexe/rpc/src/apis/injected/promise_manager.rs @@ -19,9 +19,11 @@ use anyhow::Result; use dashmap::{DashMap, mapref::entry::Entry}; use ethexe_common::{ - HashOf, SignedMessage, + HashOf, db::{InjectedStorageRO, InjectedStorageRW}, - injected::{CompactSignedPromise, InjectedTransaction, Promise, SignedPromise}, + injected::{ + CompactSignedPromise, InjectedTransaction, Promise, SignedPromise, restore_signed_promise, + }, }; use ethexe_db::Database; use std::{sync::Arc, time::Duration}; @@ -111,8 +113,12 @@ impl PromiseSubscriptionManager { let tx_hash = compact.data().tx_hash; match self.db.promise(tx_hash) { - Some(promise) => match utils::try_signed_promise_from_parts(promise, &compact) { - Ok(signed_promise) => self.dispatch_promise(signed_promise), + Some(promise) => match restore_signed_promise(promise, &compact) { + Ok(signed_promise) => { + self.db.set_compact_promise(&compact); + self.dispatch_promise(signed_promise); + } + Err(_err) => { trace!( ?compact, %tx_hash, "failed to create signed promise from parts, producer send invalid signature: compact_promise={compact:?}" @@ -131,8 +137,11 @@ impl PromiseSubscriptionManager { self.db.set_promise(&promise); if let Some((_, compact_promise)) = self.waiting_for_compute.remove(&promise.tx_hash) { - match utils::try_signed_promise_from_parts(promise, &compact_promise) { - Ok(signed_promise) => self.dispatch_promise(signed_promise), + match restore_signed_promise(promise, &compact_promise) { + Ok(signed_promise) => { + self.db.set_compact_promise(&compact_promise); + self.dispatch_promise(signed_promise); + } Err(_err) => { trace!(?compact_promise, tx_hash=?compact_promise.data().tx_hash, "failed to create signed promise from parts"); } @@ -153,15 +162,3 @@ impl PromiseSubscriptionManager { self.subscribers.len() } } - -mod utils { - use super::*; - - /// Tries build [SignedPromise] from its parts: [CompactSignedPromise] and [Promise]. - pub fn try_signed_promise_from_parts( - promise: Promise, - compact: &CompactSignedPromise, - ) -> Result { - SignedMessage::try_from_parts(promise, *compact.signature(), compact.address()) - } -} diff --git a/ethexe/rpc/src/apis/injected/relay.rs b/ethexe/rpc/src/apis/injected/relay.rs index 35f5d262558..5a52fa8d2a6 100644 --- a/ethexe/rpc/src/apis/injected/relay.rs +++ b/ethexe/rpc/src/apis/injected/relay.rs @@ -17,7 +17,11 @@ // along with this program. If not, see . use crate::{RpcEvent, errors}; -use ethexe_common::injected::{AddressedInjectedTransaction, InjectedTransactionAcceptance}; +use ethexe_common::{ + Address, + injected::{AddressedInjectedTransaction, InjectedTransactionAcceptance}, +}; +use ethexe_db::Database; use jsonrpsee::core::RpcResult; use tokio::sync::{mpsc, oneshot}; use tracing::{error, trace, warn}; @@ -25,16 +29,17 @@ use tracing::{error, trace, warn}; #[derive(Clone)] pub struct TransactionsRelayer { rpc_sender: mpsc::UnboundedSender, + db: Database, } impl TransactionsRelayer { - pub fn new(rpc_sender: mpsc::UnboundedSender) -> Self { - Self { rpc_sender } + pub fn new(rpc_sender: mpsc::UnboundedSender, db: Database) -> Self { + Self { rpc_sender, db } } pub async fn relay( &self, - transaction: AddressedInjectedTransaction, + mut transaction: AddressedInjectedTransaction, ) -> RpcResult { let tx_hash = transaction.tx.data().to_hash(); trace!(%tx_hash, ?transaction, "Called injected_sendTransaction with vars"); @@ -51,6 +56,10 @@ impl TransactionsRelayer { )); } + if transaction.recipient == Address::default() { + utils::route_transaction(&self.db, &mut transaction)?; + } + let (response_sender, response_receiver) = oneshot::channel(); let event = RpcEvent::InjectedTransaction { transaction, @@ -74,3 +83,130 @@ impl TransactionsRelayer { }) } } + +mod utils { + use super::*; + use anyhow::{Context as _, Result}; + use ethexe_common::{ + Address, + db::{ConfigStorageRO, OnChainStorageRO}, + }; + use std::time::{Duration, SystemTime, SystemTimeError}; + use tracing::{error, trace}; + + pub(super) const NEXT_PRODUCER_THRESHOLD_MS: u64 = 50; + + pub fn route_transaction( + db: &Database, + tx: &mut AddressedInjectedTransaction, + ) -> RpcResult<()> { + let now = now_since_unix_epoch().map_err(|err| { + error!("system clock error: {err}"); + crate::errors::internal() + })?; + + let next_producer = calculate_next_producer(db, now).map_err(|err| { + trace!("calculate next producer error: {err}"); + crate::errors::internal() + })?; + tx.recipient = next_producer; + + Ok(()) + } + + /// Calculates the producer address to route an injected transaction to. + pub(super) fn calculate_next_producer(db: &Database, now: Duration) -> Result

{ + let timelines = db.config().timelines; + + // Calculate target timestamp, taking into account possible delays, so we append NEXT_PRODUCER_THRESHOLD_MS. + // The transaction should be included by the next producer, so we add `slot_duration` to the current time. + let target_timestamp = now + .checked_add(Duration::from_millis(NEXT_PRODUCER_THRESHOLD_MS)) + .context("current time is too close to u64::MAX, cannot calculate next producer")? + .as_secs() + .checked_add(timelines.slot) + .context("current time is too close to u64::MAX, cannot calculate next producer")?; + + let era = timelines.era_from_ts(target_timestamp); + + let validators = db + .validators(era) + .with_context(|| format!("validators not found for era={era}"))?; + + Ok(timelines.block_producer_at(&validators, target_timestamp)) + } + + /// Returns the current time since [SystemTime::UNIX_EPOCH]. + fn now_since_unix_epoch() -> Result { + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) + } +} + +#[cfg(test)] +mod tests { + use super::utils; + use ethexe_common::{ + Address, ProtocolTimelines, ValidatorsVec, + db::{ConfigStorageRO, OnChainStorageRW, SetConfig}, + }; + use ethexe_db::Database; + use gear_core::pages::num_traits::ToPrimitive; + use std::{ops::Sub, time::Duration}; + + const SLOT: u64 = 10; + const ERA: u64 = 1000; + + fn setup_db(db: &Database) -> ValidatorsVec { + let validators = ValidatorsVec::from_iter((0..10u64).map(Address::from)); + + let timelines = ProtocolTimelines { + slot: SLOT, + era: ERA, + ..Default::default() + }; + db.set_validators(0, validators.clone()); + let mut config = db.config().clone(); + config.timelines = timelines; + db.set_config(config); + validators + } + + #[test] + fn test_calculate_next_producer_return_next() { + let db = Database::memory(); + let validators = setup_db(&db); + + let now = Duration::from_secs(SLOT / 2); + let producer = utils::calculate_next_producer(&db, now).unwrap(); + + assert_eq!(validators[1], producer); + } + + #[test] + fn test_calculate_next_producer_return_next_next() { + let db = Database::memory(); + let validators = setup_db(&db); + + let half_threshold = utils::NEXT_PRODUCER_THRESHOLD_MS.to_u64().unwrap(); + let now = Duration::from_secs(SLOT).sub(Duration::from_millis(half_threshold)); + let producer = utils::calculate_next_producer(&db, now).unwrap(); + + assert_eq!(validators[2], producer); + } + + #[test] + fn test_calculate_next_producer_in_next_era() { + let db = Database::memory(); + let validators = setup_db(&db); + + // Prepare next era validators + let mut next_era_validators = validators.clone(); + next_era_validators[0] = validators[9]; + db.set_validators(1, next_era_validators.clone()); + + let now = Duration::from_secs(ERA).sub(Duration::from_secs(1)); + let producer = utils::calculate_next_producer(&db, now).unwrap(); + + assert_eq!(next_era_validators[0], producer); + } +} diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index f8bab5772ee..003b0ad6b3c 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -23,11 +23,11 @@ use super::{ spawner, }; use ethexe_common::{ - HashOf, SignedMessage, + HashOf, db::InjectedStorageRO, injected::{ AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, - SignedInjectedTransaction, SignedPromise, + SignedInjectedTransaction, SignedPromise, restore_signed_promise, }, }; use ethexe_db::Database; @@ -91,8 +91,8 @@ impl InjectedApi { pub fn new(db: Database, rpc_sender: mpsc::UnboundedSender) -> Self { Self { db: db.clone(), - manager: PromiseSubscriptionManager::new(db), - relayer: TransactionsRelayer::new(rpc_sender), + manager: PromiseSubscriptionManager::new(db.clone()), + relayer: TransactionsRelayer::new(rpc_sender, db), } } } @@ -120,8 +120,15 @@ impl InjectedApi { } }; - let sink = match self.relayer.relay(transaction).await? { - InjectedTransactionAcceptance::Accept => pending.accept().await?, + let acceptance = self.relayer.relay(transaction).await.inspect_err(|_err| { + self.manager.cancel_registration(tx_hash); + })?; + let sink = match acceptance { + InjectedTransactionAcceptance::Accept => { + pending.accept().await.inspect_err(|_err| { + self.manager.cancel_registration(tx_hash); + })? + } InjectedTransactionAcceptance::Reject { reason } => { self.manager.cancel_registration(tx_hash); return Err(reason.into()); @@ -144,15 +151,15 @@ impl InjectedApi { return Ok(None); }; - let Some((signature, address)) = self.db.promise_signature(tx_hash) else { + let Some(compact) = self.db.compact_promise(tx_hash) else { trace!( ?tx_hash, - "promise signature not found for injected transaction" + "compact promise not found for injected transaction" ); return Ok(None); }; - match SignedMessage::try_from_parts(promise, signature, address) { + match restore_signed_promise(promise, &compact) { Ok(message) => Ok(Some(message)), Err(err) => { trace!( From d811e4511ead82e217439ca4e707d5d5e77f51c8 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Thu, 16 Apr 2026 12:11:02 +0300 Subject: [PATCH 41/59] fix: claude small review comments --- ethexe/common/src/db.rs | 1 + ethexe/network/src/validator/topic.rs | 1 - ethexe/rpc/src/apis/injected/relay.rs | 3 +-- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index c28aa2bc2f4..663bad009f9 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -137,6 +137,7 @@ pub trait InjectedStorageRO { /// Returns the promise by its transaction hash. fn promise(&self, hash: HashOf) -> Option; + /// Returns the compact promise by its transaction hash. fn compact_promise(&self, hash: HashOf) -> Option; } diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index 0729e915d2d..0083431ab48 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -681,7 +681,6 @@ mod tests { assert_eq!(promise, None); } - #[ignore = "TODO"] #[tokio::test] async fn verify_promise_ok() { let (pubkey, signer) = signer_with_pubkey(); diff --git a/ethexe/rpc/src/apis/injected/relay.rs b/ethexe/rpc/src/apis/injected/relay.rs index 5a52fa8d2a6..b61a083592f 100644 --- a/ethexe/rpc/src/apis/injected/relay.rs +++ b/ethexe/rpc/src/apis/injected/relay.rs @@ -92,7 +92,6 @@ mod utils { db::{ConfigStorageRO, OnChainStorageRO}, }; use std::time::{Duration, SystemTime, SystemTimeError}; - use tracing::{error, trace}; pub(super) const NEXT_PRODUCER_THRESHOLD_MS: u64 = 50; @@ -106,7 +105,7 @@ mod utils { })?; let next_producer = calculate_next_producer(db, now).map_err(|err| { - trace!("calculate next producer error: {err}"); + warn!(transaction=?tx, "calculate next producer error: {err}"); crate::errors::internal() })?; tx.recipient = next_producer; From 77e05e936c309e3050c94304bf4aa242c7595a1f Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 17 Apr 2026 10:41:53 +0300 Subject: [PATCH 42/59] fix: CompactSignedPromise -> SignedCompactPromise | fix tests | update docs --- ethexe/common/src/db.rs | 6 +- ethexe/common/src/injected.rs | 64 ++++++++----------- ethexe/common/src/primitives.rs | 15 ++++- ethexe/consensus/src/lib.rs | 4 +- ethexe/consensus/src/validator/producer.rs | 4 +- ethexe/db/src/database.rs | 16 ++--- ethexe/network/src/gossipsub.rs | 6 +- ethexe/network/src/lib.rs | 7 +- ethexe/network/src/validator/topic.rs | 14 ++-- .../rpc/src/apis/injected/promise_manager.rs | 27 +++++--- ethexe/rpc/src/lib.rs | 4 +- ethexe/rpc/src/tests.rs | 6 +- ethexe/service/src/tests/mod.rs | 3 +- ethexe/service/src/tests/utils/events.rs | 6 +- 14 files changed, 96 insertions(+), 86 deletions(-) diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index 663bad009f9..767f1441e81 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -23,7 +23,7 @@ use crate::{ Schedule, SimpleBlockData, ValidatorsVec, events::BlockEvent, gear::StateTransition, - injected::{CompactSignedPromise, InjectedTransaction, Promise, SignedInjectedTransaction}, + injected::{InjectedTransaction, Promise, SignedCompactPromise, SignedInjectedTransaction}, }; use alloc::{ collections::{BTreeSet, VecDeque}, @@ -138,7 +138,7 @@ pub trait InjectedStorageRO { fn promise(&self, hash: HashOf) -> Option; /// Returns the compact promise by its transaction hash. - fn compact_promise(&self, hash: HashOf) -> Option; + fn compact_promise(&self, hash: HashOf) -> Option; } #[auto_impl::auto_impl(&)] @@ -147,7 +147,7 @@ pub trait InjectedStorageRW: InjectedStorageRO { fn set_promise(&self, promise: &Promise); - fn set_compact_promise(&self, promise: &CompactSignedPromise); + fn set_compact_promise(&self, promise: &SignedCompactPromise); } #[derive(Debug, Clone, Default, Encode, Decode, TypeInfo, PartialEq, Eq, Hash)] diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index e0ebe8fef08..b584509fe50 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -152,9 +152,9 @@ impl Promise { unsafe { HashOf::new(self.reply.to_hash()) } } - /// Converts promise to its [`PromiseHashes`]. - pub fn to_hashes(&self) -> PromiseHashes { - PromiseHashes { + /// Converts promise to its compact version. + pub fn to_compact(&self) -> CompactPromise { + CompactPromise { tx_hash: self.tx_hash, reply_hash: self.reply_hash(), } @@ -163,25 +163,25 @@ impl Promise { impl ToDigest for Promise { fn update_hasher(&self, hasher: &mut sha3::Keccak256) { - self.to_hashes().update_hasher(hasher); + self.to_compact().update_hasher(hasher); } } -/// A wrapper on top of [`PromiseHashes`]. +/// A signed wrapper on top of [`CompactPromise`]. /// -/// [`CompactSignedPromise`] is a lightweight version of [`SignedPromise`], that is +/// [`SignedCompactPromise`] is a lightweight version of [`SignedPromise`], that is /// needed to reduce the amount of data transferred in network between validators. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Deref, derive_more::From)] -pub struct CompactSignedPromise(SignedMessage); +pub struct SignedCompactPromise(SignedMessage); /// The hashes of [`Promise`] parts. #[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)] -pub struct PromiseHashes { +pub struct CompactPromise { pub tx_hash: HashOf, pub reply_hash: HashOf, } -impl ToDigest for PromiseHashes { +impl ToDigest for CompactPromise { fn update_hasher(&self, hasher: &mut sha3::Keccak256) { let Self { tx_hash, @@ -193,44 +193,35 @@ impl ToDigest for PromiseHashes { } } -impl CompactSignedPromise { - /// Create the [`CompactSignedPromise`] from private key and hashes. - pub fn create(private_key: PrivateKey, promise_hashes: PromiseHashes) -> SignResult { - SignedMessage::create(private_key, promise_hashes).map(CompactSignedPromise) +impl SignedCompactPromise { + /// Create the [`SignedCompactPromise`] from private key and hashes. + pub fn create(private_key: PrivateKey, promise_hashes: CompactPromise) -> SignResult { + SignedMessage::create(private_key, promise_hashes).map(SignedCompactPromise) } pub fn create_from_promise(private_key: PrivateKey, promise: &Promise) -> SignResult { - Self::create(private_key, promise.to_hashes()) + Self::create(private_key, promise.to_compact()) } - /// Create the [`CompactSignedPromise`] from a valid [`SignedPromise`]. + /// Create the [`SignedCompactPromise`] from a valid [`SignedPromise`]. /// /// # Panics - /// Panics if the digest of [`Promise`] and [`PromiseHashes`] ever diverge. + /// Panics if the digest of [`Promise`] and [`CompactPromise`] ever diverge. /// This must hold by construction; tests enforce the invariant. - pub fn from_signed_promise_unchecked(signed_promise: &SignedPromise) -> Self { - Self::try_from(signed_promise) - .expect("SignedPromise and PromiseHashes must have identical digest") - } -} + pub fn from_signed_promise(signed_promise: &SignedPromise) -> Self { + let compact = signed_promise.data().to_compact(); + let (signature, address) = (*signed_promise.signature(), signed_promise.address()); -impl TryFrom<&SignedPromise> for CompactSignedPromise { - type Error = &'static str; - - fn try_from(signed_promise: &SignedPromise) -> Result { - SignedMessage::try_from_parts( - signed_promise.data().to_hashes(), - *signed_promise.signature(), - signed_promise.address(), - ) - .map(CompactSignedPromise) + let signed_compact = SignedMessage::try_from_parts(compact, signature, address) + .expect("SignedPromise and CompactPromise must have identical digest"); + Self(signed_compact) } } -/// Restores the [SignedPromise] from parts: [Promise], [CompactSignedPromise]. +/// Restores the [SignedPromise] from parts: [Promise], [SignedCompactPromise]. pub fn restore_signed_promise( promise: Promise, - compact: &CompactSignedPromise, + compact: &SignedCompactPromise, ) -> Result { if promise.tx_hash != compact.data().tx_hash { return Err(RestorePromiseError::HashesMismatch { @@ -330,7 +321,7 @@ mod tests { fn promise_hashes_digest_equal_to_promise_digest() { let promise = Promise::mock(()); - assert_eq!(promise.to_digest(), promise.to_hashes().to_digest()); + assert_eq!(promise.to_digest(), promise.to_compact().to_digest()); } #[test] @@ -340,7 +331,7 @@ mod tests { let signed_promise = SignedPromise::create(private_key.clone(), promise.clone()).unwrap(); let compact_signed_promise = - CompactSignedPromise::create_from_promise(private_key, &promise).unwrap(); + SignedCompactPromise::create_from_promise(private_key, &promise).unwrap(); assert_eq!(signed_promise.address(), compact_signed_promise.address()); assert_eq!( @@ -356,8 +347,7 @@ mod tests { let signed_promise = SignedPromise::create(private_key.clone(), promise).unwrap(); - let compact_signed_promise = - CompactSignedPromise::try_from(&signed_promise).expect("valid signed promise"); + let compact_signed_promise = SignedCompactPromise::from_signed_promise(&signed_promise); assert_eq!(signed_promise.address(), compact_signed_promise.address()); assert_eq!( diff --git a/ethexe/common/src/primitives.rs b/ethexe/common/src/primitives.rs index c81092b90ab..ad1f99188da 100644 --- a/ethexe/common/src/primitives.rs +++ b/ethexe/common/src/primitives.rs @@ -161,7 +161,7 @@ pub enum PromisePolicy { Disabled, } -/// The [PromiseEmissionMode] tells setups the promises mode in ethexe node. +/// The [PromiseEmissionMode] configures the promise emission mode for the ethexe node #[derive(Debug, Copy, Clone, PartialEq, Eq, derive_more::IsVariant, Default)] pub enum PromiseEmissionMode { /// Node should always emit promises during announces execution. @@ -251,7 +251,7 @@ impl CodeAndId { /// /// TODO(kuzmindev): `ProtocolTimelines` can store more protocol parameters, /// for example `max_validators` in election. -#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Encode, Decode, TypeInfo)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, TypeInfo)] pub struct ProtocolTimelines { // The genesis timestamp of the GearExe network in seconds. pub genesis_ts: u64, @@ -265,6 +265,17 @@ pub struct ProtocolTimelines { pub slot: u64, } +impl Default for ProtocolTimelines { + fn default() -> Self { + Self { + genesis_ts: 0, + era: 10_000, + election: 200, + slot: 2, + } + } +} + // TODO: #5290 remove panics here impl ProtocolTimelines { /// Returns the era index for the given timestamp. Eras starts from 0. diff --git a/ethexe/consensus/src/lib.rs b/ethexe/consensus/src/lib.rs index 719c10f2a29..dfa1901f818 100644 --- a/ethexe/consensus/src/lib.rs +++ b/ethexe/consensus/src/lib.rs @@ -36,7 +36,7 @@ use anyhow::Result; use ethexe_common::{ Announce, Digest, HashOf, PromisePolicy, SimpleBlockData, consensus::{BatchCommitmentValidationReply, VerifiedAnnounce, VerifiedValidationRequest}, - injected::{CompactSignedPromise, Promise, SignedInjectedTransaction}, + injected::{Promise, SignedCompactPromise, SignedInjectedTransaction}, network::{AnnouncesRequest, AnnouncesResponse, SignedValidatorMessage}, }; use futures::{Stream, stream::FusedStream}; @@ -120,7 +120,7 @@ pub enum ConsensusEvent { #[from] PublishMessage(SignedValidatorMessage), #[from] - PublishPromise(CompactSignedPromise), + PublishPromise(SignedCompactPromise), /// Outer service have to request announces #[from] RequestAnnounces(AnnouncesRequest), diff --git a/ethexe/consensus/src/validator/producer.rs b/ethexe/consensus/src/validator/producer.rs index 7d5ef7d6512..2f3add62cbb 100644 --- a/ethexe/consensus/src/validator/producer.rs +++ b/ethexe/consensus/src/validator/producer.rs @@ -30,7 +30,7 @@ use ethexe_common::{ Announce, HashOf, PromisePolicy, SimpleBlockData, ValidatorsVec, db::BlockMetaStorageRO, gear::BatchCommitment, - injected::{CompactSignedPromise, Promise}, + injected::{Promise, SignedCompactPromise}, network::ValidatorMessage, }; use ethexe_service_utils::Timer; @@ -123,7 +123,7 @@ impl StateHandler for Producer { .signer .signed_message(self.ctx.core.pub_key, promise, None)?; let compact_signed_promise = - CompactSignedPromise::from_signed_promise_unchecked(&signed_promise); + SignedCompactPromise::from_signed_promise(&signed_promise); self.ctx .output(ConsensusEvent::PublishPromise(compact_signed_promise)); diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index 4158a75f00c..5770619523a 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -34,7 +34,7 @@ use ethexe_common::{ }, events::BlockEvent, gear::StateTransition, - injected::{CompactSignedPromise, InjectedTransaction, Promise, SignedInjectedTransaction}, + injected::{InjectedTransaction, Promise, SignedCompactPromise, SignedInjectedTransaction}, }; use ethexe_runtime_common::state::{ Allocations, DispatchStash, Mailbox, MemoryPages, MemoryPagesRegion, MessageQueue, @@ -117,7 +117,7 @@ impl Key { | Self::AnnounceMeta(hash) => bytes.extend(hash.as_ref()), Self::InjectedTransaction(hash) | Self::Promise(hash) | Self::CompactPromise(hash) => { - bytes.extend(hash.inner().as_ref()) + bytes.extend(hash.as_ref()) } Self::ProgramToCodeId(program_id) => bytes.extend(program_id.as_ref()), @@ -718,12 +718,12 @@ impl InjectedStorageRO for RawDatabase { fn compact_promise( &self, tx_hash: HashOf, - ) -> Option { + ) -> Option { self.kv .get(&Key::CompactPromise(tx_hash).to_bytes()) .map(|data| { - CompactSignedPromise::decode(&mut data.as_slice()) - .expect("Failed to decode data into CompactSignedPromise") + SignedCompactPromise::decode(&mut data.as_slice()) + .expect("Failed to decode data into SignedCompactPromise") }) } } @@ -744,7 +744,7 @@ impl InjectedStorageRW for RawDatabase { .put(&Key::Promise(promise.tx_hash).to_bytes(), promise.encode()) } - fn set_compact_promise(&self, promise: &CompactSignedPromise) { + fn set_compact_promise(&self, promise: &SignedCompactPromise) { let tx_hash = promise.data().tx_hash; tracing::trace!(?promise, "Set compact promise for injected transaction"); @@ -960,7 +960,7 @@ impl InjectedStorageRO for Database { delegate!(to self.raw { fn injected_transaction(&self, hash: HashOf) -> Option; fn promise(&self, hash: HashOf) -> Option; - fn compact_promise(&self, hash: HashOf) -> Option; + fn compact_promise(&self, hash: HashOf) -> Option; }); } @@ -968,7 +968,7 @@ impl InjectedStorageRW for Database { delegate!(to self.raw { fn set_injected_transaction(&self, tx: SignedInjectedTransaction); fn set_promise(&self, promise: &Promise); - fn set_compact_promise(&self, promise: &CompactSignedPromise); + fn set_compact_promise(&self, promise: &SignedCompactPromise); }); } diff --git a/ethexe/network/src/gossipsub.rs b/ethexe/network/src/gossipsub.rs index 6e9aabefb95..cf1ee3e5bbc 100644 --- a/ethexe/network/src/gossipsub.rs +++ b/ethexe/network/src/gossipsub.rs @@ -23,7 +23,7 @@ use crate::{ peer_score, }; use anyhow::anyhow; -use ethexe_common::{Address, injected::CompactSignedPromise, network::SignedValidatorMessage}; +use ethexe_common::{Address, injected::SignedCompactPromise, network::SignedValidatorMessage}; use libp2p::{ core::{Endpoint, transport::PortUse}, gossipsub, @@ -46,7 +46,7 @@ use std::{ pub enum Message { // TODO: rename to `Validators` Commitments(SignedValidatorMessage), - Promise(CompactSignedPromise), + Promise(SignedCompactPromise), } impl Message { @@ -190,7 +190,7 @@ impl Behaviour { let res = if topic == self.commitments_topic.hash() { SignedValidatorMessage::decode(&mut &data[..]).map(Message::Commitments) } else if topic == self.promises_topic.hash() { - CompactSignedPromise::decode(&mut &data[..]).map(Message::Promise) + SignedCompactPromise::decode(&mut &data[..]).map(Message::Promise) } else { unreachable!("topic we never subscribed to: {topic:?}"); }; diff --git a/ethexe/network/src/lib.rs b/ethexe/network/src/lib.rs index 533e8aa2d13..23f8aebcae5 100644 --- a/ethexe/network/src/lib.rs +++ b/ethexe/network/src/lib.rs @@ -59,7 +59,7 @@ use ethexe_common::{ Address, BlockHeader, ValidatorsVec, db::ConfigStorageRO, ecdsa::PublicKey, - injected::{AddressedInjectedTransaction, CompactSignedPromise}, + injected::{AddressedInjectedTransaction, SignedCompactPromise}, network::{SignedValidatorMessage, VerifiedValidatorMessage}, }; use ethexe_db::Database; @@ -110,7 +110,7 @@ pub enum NetworkEvent { /// A validator-signed message from the validator gossipsub topic. ValidatorMessage(VerifiedValidatorMessage), /// A public promise observed on the promise gossipsub topic. - PromiseMessage(CompactSignedPromise), + PromiseMessage(SignedCompactPromise), /// Validator discovery learned or refreshed the network identity of the /// given validator address. ValidatorIdentityUpdated(Address), @@ -667,7 +667,8 @@ impl NetworkService { .send_transaction(behaviour.validator_discovery.identities(), data) } - pub fn publish_promise(&mut self, compact_promise: CompactSignedPromise) { + /// Publish a signed promise to the public promise gossipsub topic. + pub fn publish_promise(&mut self, compact_promise: SignedCompactPromise) { self.swarm .behaviour_mut() .gossipsub diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index 0083431ab48..bd5c8ecd6db 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -25,7 +25,7 @@ use crate::{ }; use ethexe_common::{ Address, HashOf, - injected::{CompactSignedPromise, InjectedTransaction}, + injected::{InjectedTransaction, SignedCompactPromise}, network::VerifiedValidatorMessage, }; use lru::LruCache; @@ -290,8 +290,8 @@ impl ValidatorTopic { fn inner_verify_promise( &self, _source: PeerId, - compact_promise: CompactSignedPromise, - ) -> Result { + compact_promise: SignedCompactPromise, + ) -> Result { let address = compact_promise.address(); if !self.snapshot.contains(address) { return Err(VerifyPromiseError::UnknownValidator { @@ -307,8 +307,8 @@ impl ValidatorTopic { pub fn verify_promise( &self, source: PeerId, - compact_promise: CompactSignedPromise, - ) -> (MessageAcceptance, Option) { + compact_promise: SignedCompactPromise, + ) -> (MessageAcceptance, Option) { match self.inner_verify_promise(source, compact_promise) { Ok(compact_promise) => (MessageAcceptance::Accept, Some(compact_promise)), Err(err) => { @@ -392,9 +392,9 @@ mod tests { signer: &Signer, public_key: PublicKey, promise: Promise, - ) -> CompactSignedPromise { + ) -> SignedCompactPromise { let signed_promise = signer.signed_message(public_key, promise, None).unwrap(); - CompactSignedPromise::from_signed_promise_unchecked(&signed_promise) + SignedCompactPromise::from_signed_promise(&signed_promise) } #[test] diff --git a/ethexe/rpc/src/apis/injected/promise_manager.rs b/ethexe/rpc/src/apis/injected/promise_manager.rs index d55f454eef7..9f4a752b181 100644 --- a/ethexe/rpc/src/apis/injected/promise_manager.rs +++ b/ethexe/rpc/src/apis/injected/promise_manager.rs @@ -22,19 +22,16 @@ use ethexe_common::{ HashOf, db::{InjectedStorageRO, InjectedStorageRW}, injected::{ - CompactSignedPromise, InjectedTransaction, Promise, SignedPromise, restore_signed_promise, + InjectedTransaction, Promise, SignedCompactPromise, SignedPromise, restore_signed_promise, }, }; use ethexe_db::Database; -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; use tokio::sync::oneshot; use tracing::trace; -pub const MAX_PROMISE_WAITING: Duration = - Duration::from_secs(alloy::eips::merge::SLOT_DURATION_SECS * 20); - type PromiseSubscribers = Arc, oneshot::Sender>>; -type PromisesComputationWaiting = Arc, CompactSignedPromise>>; +type PromisesComputationWaiting = Arc, SignedCompactPromise>>; /// The manager for promise subscribers. #[derive(Debug, Clone)] @@ -66,10 +63,12 @@ pub struct PendingSubscriber { impl PendingSubscriber { pub fn new( + db: &Database, tx_hash: HashOf, receiver: oneshot::Receiver, ) -> Self { - let receiver = tokio::time::timeout(MAX_PROMISE_WAITING, receiver); + let timeout_duration = utils::promise_waiting_timeout(db); + let receiver = tokio::time::timeout(timeout_duration, receiver); Self { tx_hash, receiver } } @@ -96,7 +95,7 @@ impl PromiseSubscriptionManager { Entry::Vacant(entry) => { let (sender, receiver) = oneshot::channel(); entry.insert(sender); - Ok(PendingSubscriber::new(tx_hash, receiver)) + Ok(PendingSubscriber::new(&self.db, tx_hash, receiver)) } } } @@ -108,7 +107,7 @@ impl PromiseSubscriptionManager { self.subscribers.remove(&tx_hash).map(|(_, v)| v) } - pub fn on_compact_promise(&self, compact: CompactSignedPromise) { + pub fn on_compact_promise(&self, compact: SignedCompactPromise) { trace!(?compact, "received new compact promise"); let tx_hash = compact.data().tx_hash; @@ -162,3 +161,13 @@ impl PromiseSubscriptionManager { self.subscribers.len() } } + +mod utils { + use ethexe_common::db::ConfigStorageRO; + + /// Returns the maximum time that spawned [super::PendingSubscriber] will wait for promise. + pub fn promise_waiting_timeout(db: &DB) -> std::time::Duration { + let slot_duration_secs = db.config().timelines.slot; + std::time::Duration::from_secs(slot_duration_secs * 20) + } +} diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index eadd894dcca..38c50678a70 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -58,7 +58,7 @@ use apis::{ ProgramApi, ProgramServer, }; use ethexe_common::injected::{ - AddressedInjectedTransaction, CompactSignedPromise, InjectedTransactionAcceptance, Promise, + AddressedInjectedTransaction, InjectedTransactionAcceptance, Promise, SignedCompactPromise, }; use ethexe_db::Database; use ethexe_processor::{Processor, ProcessorConfig}; @@ -196,7 +196,7 @@ impl RpcService { self.injected_api.on_computed_promise(promise); } - pub fn receive_compact_promise(&self, compact_promise: CompactSignedPromise) { + pub fn receive_compact_promise(&self, compact_promise: SignedCompactPromise) { self.injected_api.on_compact_promise(compact_promise); } } diff --git a/ethexe/rpc/src/tests.rs b/ethexe/rpc/src/tests.rs index b73be48bf8a..1bab4b43134 100644 --- a/ethexe/rpc/src/tests.rs +++ b/ethexe/rpc/src/tests.rs @@ -24,7 +24,7 @@ use ethexe_common::{ db::InjectedStorageRW, ecdsa::PrivateKey, gear::MAX_BLOCK_GAS_LIMIT, - injected::{AddressedInjectedTransaction, CompactSignedPromise, Promise}, + injected::{AddressedInjectedTransaction, Promise, SignedCompactPromise}, mock::Mock, }; use ethexe_db::Database; @@ -88,13 +88,13 @@ impl MockService { fn promises_bundle( &self, txs: impl IntoIterator, - ) -> Vec { + ) -> Vec { let pk = PrivateKey::random(); txs.into_iter() .map(|tx| { let promise = Promise::mock(tx.tx.data().to_hash()); self.db.set_promise(&promise); - CompactSignedPromise::create_from_promise(pk.clone(), &promise).unwrap() + SignedCompactPromise::create_from_promise(pk.clone(), &promise).unwrap() }) .collect() } diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index f64f9f036a9..5d51442a622 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -2719,8 +2719,7 @@ async fn injected_tx_fungible_token() { } #[tokio::test] -// TODO: up me back to 60s -#[ntest::timeout(15_000)] +#[ntest::timeout(60_000)] async fn injected_tx_fungible_token_over_network() { init_logger(); diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index a28947b5c6e..4f7f4239f71 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -26,8 +26,8 @@ use ethexe_common::{ db::*, events::BlockEvent, injected::{ - AddressedInjectedTransaction, CompactSignedPromise, InjectedTransaction, - InjectedTransactionAcceptance, SignedInjectedTransaction, + AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, + SignedCompactPromise, SignedInjectedTransaction, }, network::VerifiedValidatorMessage, }; @@ -85,7 +85,7 @@ impl TestingNetworkInjectedEvent { #[derive(Debug, Clone, Eq, PartialEq)] pub enum TestingNetworkEvent { ValidatorMessage(VerifiedValidatorMessage), - PromiseMessage(CompactSignedPromise), + PromiseMessage(SignedCompactPromise), ValidatorIdentityUpdated(Address), InjectedTransaction(TestingNetworkInjectedEvent), PeerBlocked(PeerId), From 5fd60d978b6af1641669d3dcbbf3c3bcf5cf5372 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 17 Apr 2026 11:34:08 +0300 Subject: [PATCH 43/59] chore: fix doc in rpc/src/apis/injected/mod.rs --- ethexe/rpc/src/apis/injected/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethexe/rpc/src/apis/injected/mod.rs b/ethexe/rpc/src/apis/injected/mod.rs index 12b7d67d91d..c1cffbf6785 100644 --- a/ethexe/rpc/src/apis/injected/mod.rs +++ b/ethexe/rpc/src/apis/injected/mod.rs @@ -29,7 +29,7 @@ //! [spawner::spawn_pending_subscriber]. //! //! **Important:** the pending subscriber will be dropped after -//! [promise_manager::MAX_PROMISE_WAITING_SECS] seconds to avoid dead subscribers. +//! waiting for **20 * Ethereum slot** seconds to avoid dead subscribers. //! //! [promise_manager::PromiseSubscriptionManager] provides two methods for receiving promises: //! - [promise_manager::PromiseSubscriptionManager::on_compact_promise] receives the promise From 0962a7cbe5cb64856594a806624e2eca0e55e5fa Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 17 Apr 2026 11:39:31 +0300 Subject: [PATCH 44/59] fix promise emission for not computed chain --- Cargo.lock | 1 - ethexe/compute/src/compute.rs | 19 ++++++++++++++++--- ethexe/rpc/Cargo.toml | 1 - 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bde8f48670c..ace6145e040 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5462,7 +5462,6 @@ dependencies = [ name = "ethexe-rpc" version = "1.10.0" dependencies = [ - "alloy", "anyhow", "dashmap 5.5.3", "ethexe-common", diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index f8de98e80be..52421eb519e 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -125,10 +125,23 @@ impl ComputeSubService

{ not_computed_announces.len(), ); + let promise_tx = match config.promises_mode() { + // If AlwaysEmit promises mode - we pass promises tx also for not computed chain. + PromiseEmissionMode::AlwaysEmit => promise_out_tx.clone(), + // Set the promise_out_tx = None, because in this case we want to receive promises only from target announce. + PromiseEmissionMode::ConsensusDriven => None, + }; + for (announce_hash, announce) in not_computed_announces { - // Set the promise_out_tx = None, because we want to receive the promises only from target announce. - Self::compute_one(&db, &mut processor, config, announce_hash, announce, None) - .await?; + Self::compute_one( + &db, + &mut processor, + config, + announce_hash, + announce, + promise_tx.clone(), + ) + .await?; } } diff --git a/ethexe/rpc/Cargo.toml b/ethexe/rpc/Cargo.toml index a4b74116b25..5b66f50c660 100644 --- a/ethexe/rpc/Cargo.toml +++ b/ethexe/rpc/Cargo.toml @@ -33,7 +33,6 @@ metrics-derive.workspace = true gear-workspace-hack.workspace = true thiserror.workspace = true scopeguard.workspace = true -alloy.workspace = true [dev-dependencies] jsonrpsee = { workspace = true, features = ["client"] } From 4d331974673e9f3fdfd5f7212c2cfda3b5507fae Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 20 Apr 2026 16:54:07 +0300 Subject: [PATCH 45/59] feat: add metrics to new server --- ethexe/rpc/src/apis/injected/server.rs | 8 +++++++- ethexe/rpc/src/lib.rs | 2 ++ ethexe/rpc/src/metrics.rs | 2 -- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index 117aed18e57..71de85f7ced 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -16,7 +16,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::{RpcEvent, errors}; +use crate::{RpcEvent, errors, metrics::InjectedApiMetrics}; use super::{ InjectedServer, promise_manager::PromiseSubscriptionManager, relay::TransactionsRelayer, @@ -46,6 +46,7 @@ pub struct InjectedApi { db: Database, manager: PromiseSubscriptionManager, relayer: TransactionsRelayer, + metrics: InjectedApiMetrics, } // TODO: add metrics middleware for InjectedApi @@ -95,6 +96,7 @@ impl InjectedApi { db: db.clone(), manager: PromiseSubscriptionManager::new(db.clone()), relayer: TransactionsRelayer::new(rpc_sender, db), + metrics: InjectedApiMetrics::default(), } } } @@ -105,6 +107,8 @@ impl InjectedApi { &self, transaction: AddressedInjectedTransaction, ) -> RpcResult { + self.metrics.send_injected_tx_calls.increment(1); + self.relayer.relay(transaction).await } @@ -113,6 +117,8 @@ impl InjectedApi { pending: PendingSubscriptionSink, transaction: AddressedInjectedTransaction, ) -> SubscriptionResult { + self.metrics.send_and_watch_injected_tx_calls.increment(1); + let tx_hash = transaction.tx.data().to_hash(); let pending_subscriber = match self.manager.try_register_subscriber(tx_hash) { diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index 38c50678a70..295061fc9dc 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -227,6 +227,8 @@ impl RpcServerApis { pub fn into_methods(self) -> jsonrpsee::server::RpcModule<()> { let mut module = JsonrpcModule::new(()); + // let rpc = self.block.into_rpc(); + // let callbacks = rpc.method_names(); module .merge(BlockServer::into_rpc(self.block)) .expect("No conflicts"); diff --git a/ethexe/rpc/src/metrics.rs b/ethexe/rpc/src/metrics.rs index 4cff4afb3a4..4c05d353e6c 100644 --- a/ethexe/rpc/src/metrics.rs +++ b/ethexe/rpc/src/metrics.rs @@ -22,8 +22,6 @@ use metrics::{Counter, Gauge}; // TODO kuzmindev: add metrics for all RPC apis, e.g number of calls, latency, errors, etc. -// TODO: remove this unused -#[allow(unused)] /// Metrics for the Injected RPC API. #[derive(Clone, metrics_derive::Metrics)] #[metrics(scope = "ethexe_rpc_injected_api")] From 30d4f3ab50616a05a1ff7c6635d18e24d9d8cd4b Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 20 Apr 2026 17:40:43 +0300 Subject: [PATCH 46/59] chore: add TODO comments for future refactoring --- ethexe/rpc/src/apis/injected/promise_manager.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ethexe/rpc/src/apis/injected/promise_manager.rs b/ethexe/rpc/src/apis/injected/promise_manager.rs index 9f4a752b181..56e6aeeed33 100644 --- a/ethexe/rpc/src/apis/injected/promise_manager.rs +++ b/ethexe/rpc/src/apis/injected/promise_manager.rs @@ -30,6 +30,10 @@ use std::sync::Arc; use tokio::sync::oneshot; use tracing::trace; +// TODO (kuzmindev): Currently, PromiseSubscriptionManager do not check, that transaction was +// sent by validator, so there must be pre-validation for data received from network (SignedCompactPromise). + +// TODO (kuzmindev): think about using `moka::sync::Cache` instead of DashMap type PromiseSubscribers = Arc, oneshot::Sender>>; type PromisesComputationWaiting = Arc, SignedCompactPromise>>; From 5bd3edf36478f0fe2f652971ec1dccc4ed54a7df Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 21 Apr 2026 14:54:46 +0300 Subject: [PATCH 47/59] ai: generate first prototype for metrics layer --- ethexe/rpc/src/apis/injected/server.rs | 22 ++-- ethexe/rpc/src/apis/injected/spawner.rs | 4 + ethexe/rpc/src/lib.rs | 3 + ethexe/rpc/src/metrics.rs | 161 ++++++++++++++++++++++-- 4 files changed, 174 insertions(+), 16 deletions(-) diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index 71de85f7ced..31b8b19eb40 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -49,7 +49,6 @@ pub struct InjectedApi { metrics: InjectedApiMetrics, } -// TODO: add metrics middleware for InjectedApi #[async_trait] impl InjectedServer for InjectedApi { async fn send_transaction( @@ -107,8 +106,6 @@ impl InjectedApi { &self, transaction: AddressedInjectedTransaction, ) -> RpcResult { - self.metrics.send_injected_tx_calls.increment(1); - self.relayer.relay(transaction).await } @@ -117,8 +114,6 @@ impl InjectedApi { pending: PendingSubscriptionSink, transaction: AddressedInjectedTransaction, ) -> SubscriptionResult { - self.metrics.send_and_watch_injected_tx_calls.increment(1); - let tx_hash = transaction.tx.data().to_hash(); let pending_subscriber = match self.manager.try_register_subscriber(tx_hash) { @@ -127,26 +122,37 @@ impl InjectedApi { return Err(errors::bad_request(err).into()); } }; + self.metrics.injected_tx_active_subscriptions.increment(1); let acceptance = self.relayer.relay(transaction).await.inspect_err(|_err| { + self.metrics.injected_tx_active_subscriptions.decrement(1); self.manager.cancel_registration(tx_hash); })?; let sink = match acceptance { InjectedTransactionAcceptance::Accept => { pending.accept().await.inspect_err(|_err| { + self.metrics.injected_tx_active_subscriptions.decrement(1); self.manager.cancel_registration(tx_hash); })? } InjectedTransactionAcceptance::Reject { reason } => { + self.metrics.injected_tx_active_subscriptions.decrement(1); self.manager.cancel_registration(tx_hash); return Err(reason.into()); } }; let manager = self.manager.clone(); - spawner::spawn_pending_subscriber(sink, pending_subscriber, move |tx_hash| { - manager.cancel_registration(tx_hash); - }); + let metrics = self.metrics.clone(); + spawner::spawn_pending_subscriber( + sink, + pending_subscriber, + metrics.clone(), + move |tx_hash| { + metrics.injected_tx_active_subscriptions.decrement(1); + manager.cancel_registration(tx_hash); + }, + ); Ok(()) } diff --git a/ethexe/rpc/src/apis/injected/spawner.rs b/ethexe/rpc/src/apis/injected/spawner.rs index 73f3702d814..984db10e137 100644 --- a/ethexe/rpc/src/apis/injected/spawner.rs +++ b/ethexe/rpc/src/apis/injected/spawner.rs @@ -17,6 +17,7 @@ // along with this program. If not, see . use super::promise_manager::PendingSubscriber; +use crate::metrics::InjectedApiMetrics; use ethexe_common::{HashOf, injected::InjectedTransaction}; use jsonrpsee::{SubscriptionMessage, SubscriptionSink}; use tracing::{error, trace, warn}; @@ -27,6 +28,7 @@ use tracing::{error, trace, warn}; pub fn spawn_pending_subscriber( sink: SubscriptionSink, subscriber: PendingSubscriber, + metrics: InjectedApiMetrics, on_finish: F, ) where F: FnOnce(HashOf) + std::marker::Send + 'static, @@ -61,6 +63,8 @@ pub fn spawn_pending_subscriber( Ok(message) => { if let Err(err) = sink.send(message).await { trace!("failed to send promise, client disconnected: err={err}"); + } else { + metrics.injected_tx_promises_given.increment(1); } } Err(err) => { diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index 295061fc9dc..09922586293 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -68,6 +68,7 @@ use jsonrpsee::{ RpcModule as JsonrpcModule, server::{PingConfig, Server, ServerHandle}, }; +use metrics::{DEFAULT_TRACKED_METHODS, RpcMetricsRegistry}; use std::{ net::SocketAddr, pin::Pin, @@ -129,9 +130,11 @@ impl RpcServer { let cors_layer = self.cors_layer()?; let http_middleware = tower::ServiceBuilder::new().layer(cors_layer); + let rpc_middleware = RpcMetricsRegistry::new(DEFAULT_TRACKED_METHODS).middleware(); let server = Server::builder() .set_http_middleware(http_middleware) + .set_rpc_middleware(rpc_middleware) // Setup WebSocket pings to detect dead connections. // Now it is set to default: ping_interval = 30s, inactive_limit = 40s .enable_ws_ping(PingConfig::default()) diff --git a/ethexe/rpc/src/metrics.rs b/ethexe/rpc/src/metrics.rs index 4c05d353e6c..c981bfa73c4 100644 --- a/ethexe/rpc/src/metrics.rs +++ b/ethexe/rpc/src/metrics.rs @@ -1,6 +1,6 @@ // This file is part of Gear. // -// Copyright (C) 2025 Gear Technologies Inc. +// Copyright (C) 2025-2026 Gear Technologies Inc. // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 // // This program is free software: you can redistribute it and/or modify @@ -18,20 +18,165 @@ //! Metrics for the RPC server. -use metrics::{Counter, Gauge}; +use futures::future::BoxFuture; +use jsonrpsee::{ + server::{MethodResponse, RpcServiceBuilder, middleware::rpc::RpcServiceT}, + types::Request, +}; +use metrics::{Counter, Gauge, Histogram, counter, gauge, histogram}; +use std::{collections::HashMap, sync::Arc, time::Instant}; +use tower::{ + Layer, + layer::util::{Identity, Stack}, +}; -// TODO kuzmindev: add metrics for all RPC apis, e.g number of calls, latency, errors, etc. +/// Methods tracked by the generic RPC middleware. +pub const DEFAULT_TRACKED_METHODS: &[&str] = &[ + "injected_sendTransaction", + "injected_sendTransactionAndWatch", + "program_calculateReplyForHandle", +]; -/// Metrics for the Injected RPC API. +/// Metrics for the Injected RPC API lifecycle. #[derive(Clone, metrics_derive::Metrics)] #[metrics(scope = "ethexe_rpc_injected_api")] pub struct InjectedApiMetrics { - /// The number of calls to `injected_sendTransaction`. - pub send_injected_tx_calls: Counter, - /// The number of calls to `injected_subscribeTransactionPromise`. - pub send_and_watch_injected_tx_calls: Counter, /// The number of active injected transaction promises subscriptions. pub injected_tx_active_subscriptions: Gauge, /// The total number of injected transaction promises given to subscribers. pub injected_tx_promises_given: Counter, } + +#[derive(Clone)] +pub struct RpcMetricsRegistry { + methods: Arc>, +} + +impl RpcMetricsRegistry { + pub fn new(methods: &'static [&'static str]) -> Self { + let methods = methods + .iter() + .copied() + .map(|method| (method, MethodMetrics::new(method))) + .collect(); + + Self { + methods: Arc::new(methods), + } + } + + fn get(&self, method: &str) -> Option<&MethodMetrics> { + self.methods.get(method) + } + + pub fn middleware(self) -> RpcServiceBuilder> { + RpcServiceBuilder::new().layer(RpcMetricsLayer::new(self)) + } +} + +#[derive(Clone)] +pub struct RpcMetricsLayer { + registry: RpcMetricsRegistry, +} + +impl RpcMetricsLayer { + fn new(registry: RpcMetricsRegistry) -> Self { + Self { registry } + } +} + +impl Layer for RpcMetricsLayer { + type Service = RpcMetricsService; + + fn layer(&self, service: S) -> Self::Service { + RpcMetricsService { + service, + registry: self.registry.clone(), + } + } +} + +#[derive(Clone)] +struct MethodMetrics { + calls_started: Counter, + calls_finished_ok: Counter, + calls_finished_err: Counter, + calls_latency_seconds: Histogram, + calls_response_size_bytes: Histogram, + calls_in_flight: Gauge, +} + +impl MethodMetrics { + fn new(method: &'static str) -> Self { + Self { + calls_started: counter!("ethexe_rpc_calls_started_total", "method" => method), + calls_finished_ok: counter!( + "ethexe_rpc_calls_finished_total", + "method" => method, + "status" => "ok" + ), + calls_finished_err: counter!( + "ethexe_rpc_calls_finished_total", + "method" => method, + "status" => "error" + ), + calls_latency_seconds: histogram!( + "ethexe_rpc_call_duration_seconds", + "method" => method + ), + calls_response_size_bytes: histogram!( + "ethexe_rpc_response_size_bytes", + "method" => method + ), + calls_in_flight: gauge!("ethexe_rpc_calls_in_flight", "method" => method), + } + } +} + +#[derive(Clone)] +pub struct RpcMetricsService { + service: S, + registry: RpcMetricsRegistry, +} + +impl<'a, S> RpcServiceT<'a> for RpcMetricsService +where + S: RpcServiceT<'a> + Send + Sync, + S::Future: Send + 'a, +{ + type Future = BoxFuture<'a, MethodResponse>; + + fn call(&self, request: Request<'a>) -> Self::Future { + let metrics = self.registry.get(request.method_name()).cloned(); + let future = self.service.call(request); + + Box::pin(async move { + let Some(metrics) = metrics else { + return future.await; + }; + + metrics.calls_started.increment(1); + metrics.calls_in_flight.increment(1); + let _in_flight_guard = scopeguard::guard(metrics.calls_in_flight.clone(), |gauge| { + gauge.decrement(1); + }); + let started_at = Instant::now(); + + let response = future.await; + + metrics + .calls_latency_seconds + .record(started_at.elapsed().as_secs_f64()); + metrics + .calls_response_size_bytes + .record(response.as_result().len() as f64); + + if response.is_success() { + metrics.calls_finished_ok.increment(1); + } else { + metrics.calls_finished_err.increment(1); + } + response + }) + } +} From 80863eb92395d666268a01c68de646f481dc5a8d Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 21 Apr 2026 20:24:28 +0300 Subject: [PATCH 48/59] feat: complete metrics middleware --- ethexe/rpc/src/apis/injected/server.rs | 21 +- ethexe/rpc/src/apis/injected/spawner.rs | 4 - ethexe/rpc/src/lib.rs | 28 +-- ethexe/rpc/src/metrics.rs | 295 +++++++++++++----------- 4 files changed, 181 insertions(+), 167 deletions(-) diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index 31b8b19eb40..8d1a7d8d675 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -122,37 +122,28 @@ impl InjectedApi { return Err(errors::bad_request(err).into()); } }; - self.metrics.injected_tx_active_subscriptions.increment(1); let acceptance = self.relayer.relay(transaction).await.inspect_err(|_err| { - self.metrics.injected_tx_active_subscriptions.decrement(1); self.manager.cancel_registration(tx_hash); })?; let sink = match acceptance { InjectedTransactionAcceptance::Accept => { pending.accept().await.inspect_err(|_err| { - self.metrics.injected_tx_active_subscriptions.decrement(1); self.manager.cancel_registration(tx_hash); })? } InjectedTransactionAcceptance::Reject { reason } => { - self.metrics.injected_tx_active_subscriptions.decrement(1); self.manager.cancel_registration(tx_hash); return Err(reason.into()); } }; - let manager = self.manager.clone(); - let metrics = self.metrics.clone(); - spawner::spawn_pending_subscriber( - sink, - pending_subscriber, - metrics.clone(), - move |tx_hash| { - metrics.injected_tx_active_subscriptions.decrement(1); - manager.cancel_registration(tx_hash); - }, - ); + self.metrics.injected_tx_active_subscriptions.increment(1); + let (manager, metrics) = (self.manager.clone(), self.metrics.clone()); + spawner::spawn_pending_subscriber(sink, pending_subscriber, move |tx_hash| { + manager.cancel_registration(tx_hash); + metrics.injected_tx_active_subscriptions.decrement(1); + }); Ok(()) } diff --git a/ethexe/rpc/src/apis/injected/spawner.rs b/ethexe/rpc/src/apis/injected/spawner.rs index 984db10e137..73f3702d814 100644 --- a/ethexe/rpc/src/apis/injected/spawner.rs +++ b/ethexe/rpc/src/apis/injected/spawner.rs @@ -17,7 +17,6 @@ // along with this program. If not, see . use super::promise_manager::PendingSubscriber; -use crate::metrics::InjectedApiMetrics; use ethexe_common::{HashOf, injected::InjectedTransaction}; use jsonrpsee::{SubscriptionMessage, SubscriptionSink}; use tracing::{error, trace, warn}; @@ -28,7 +27,6 @@ use tracing::{error, trace, warn}; pub fn spawn_pending_subscriber( sink: SubscriptionSink, subscriber: PendingSubscriber, - metrics: InjectedApiMetrics, on_finish: F, ) where F: FnOnce(HashOf) + std::marker::Send + 'static, @@ -63,8 +61,6 @@ pub fn spawn_pending_subscriber( Ok(message) => { if let Err(err) = sink.send(message).await { trace!("failed to send promise, client disconnected: err={err}"); - } else { - metrics.injected_tx_promises_given.increment(1); } } Err(err) => { diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index 09922586293..92e5da28010 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -66,9 +66,9 @@ use futures::{Stream, stream::FusedStream}; use hyper::header::HeaderValue; use jsonrpsee::{ RpcModule as JsonrpcModule, - server::{PingConfig, Server, ServerHandle}, + server::{PingConfig, RpcServiceBuilder, Server, ServerHandle}, }; -use metrics::{DEFAULT_TRACKED_METHODS, RpcMetricsRegistry}; +use metrics::{RpcMetricsLayer, RpcMetricsRegistry}; use std::{ net::SocketAddr, pin::Pin, @@ -130,16 +130,10 @@ impl RpcServer { let cors_layer = self.cors_layer()?; let http_middleware = tower::ServiceBuilder::new().layer(cors_layer); - let rpc_middleware = RpcMetricsRegistry::new(DEFAULT_TRACKED_METHODS).middleware(); - let server = Server::builder() - .set_http_middleware(http_middleware) - .set_rpc_middleware(rpc_middleware) - // Setup WebSocket pings to detect dead connections. - // Now it is set to default: ping_interval = 30s, inactive_limit = 40s - .enable_ws_ping(PingConfig::default()) - .build(self.config.listen_addr) - .await?; + let rpc_metrics_registry = RpcMetricsRegistry::default(); + let rpc_middleware = + RpcServiceBuilder::new().layer(RpcMetricsLayer::from_registry(rpc_metrics_registry)); let processor = Processor::with_config( ProcessorConfig { @@ -161,9 +155,17 @@ impl RpcServer { }; let injected_api = server_apis.injected.clone(); - let handle = server.start(server_apis.into_methods()); + let server_handle = Server::builder() + .set_http_middleware(http_middleware) + .set_rpc_middleware(rpc_middleware) + // Setup WebSocket pings to detect dead connections. + // Now it is set to default: ping_interval = 30s, inactive_limit = 40s + .enable_ws_ping(PingConfig::default()) + .build(self.config.listen_addr) + .await? + .start(server_apis.into_methods()); - Ok((handle, RpcService::new(rpc_receiver, injected_api))) + Ok((server_handle, RpcService::new(rpc_receiver, injected_api))) } fn cors_layer(&self) -> Result { diff --git a/ethexe/rpc/src/metrics.rs b/ethexe/rpc/src/metrics.rs index c981bfa73c4..e4a2c197a8d 100644 --- a/ethexe/rpc/src/metrics.rs +++ b/ethexe/rpc/src/metrics.rs @@ -18,165 +18,190 @@ //! Metrics for the RPC server. -use futures::future::BoxFuture; -use jsonrpsee::{ - server::{MethodResponse, RpcServiceBuilder, middleware::rpc::RpcServiceT}, - types::Request, -}; -use metrics::{Counter, Gauge, Histogram, counter, gauge, histogram}; -use std::{collections::HashMap, sync::Arc, time::Instant}; -use tower::{ - Layer, - layer::util::{Identity, Stack}, -}; - -/// Methods tracked by the generic RPC middleware. -pub const DEFAULT_TRACKED_METHODS: &[&str] = &[ - "injected_sendTransaction", - "injected_sendTransactionAndWatch", - "program_calculateReplyForHandle", -]; - -/// Metrics for the Injected RPC API lifecycle. -#[derive(Clone, metrics_derive::Metrics)] -#[metrics(scope = "ethexe_rpc_injected_api")] -pub struct InjectedApiMetrics { - /// The number of active injected transaction promises subscriptions. - pub injected_tx_active_subscriptions: Gauge, - /// The total number of injected transaction promises given to subscribers. - pub injected_tx_promises_given: Counter, -} +pub use metrics::*; +pub use middleware::{RpcMetricsLayer, RpcMetricsRegistry}; + +mod middleware { + use super::metrics::{DEFAULT_TRACKED_METHODS, MethodMetrics}; + use futures::future::BoxFuture; + use jsonrpsee::{ + server::{MethodResponse, middleware::rpc::RpcServiceT}, + types::Request, + }; + use std::{collections::HashMap, sync::Arc, time::Instant}; + use tower::Layer; + + /// A methods metrics registry for [RpcMetricsLayer]. + /// Internally it uses the mapping `method_name` => [MethodMetrics], so the + /// access to metrics is fast and do not add extra request latency. + #[derive(Clone)] + pub struct RpcMetricsRegistry { + methods_map: Arc>, + } -#[derive(Clone)] -pub struct RpcMetricsRegistry { - methods: Arc>, -} + impl RpcMetricsRegistry { + pub fn new(methods: &'static [&'static str]) -> Self { + let mut methods_map = HashMap::new(); + methods.iter().copied().for_each(|method_name| { + let method_metrics = MethodMetrics::new_with_labels(&[("method", method_name)]); + methods_map.insert(method_name, method_metrics); + }); -impl RpcMetricsRegistry { - pub fn new(methods: &'static [&'static str]) -> Self { - let methods = methods - .iter() - .copied() - .map(|method| (method, MethodMetrics::new(method))) - .collect(); + Self { + methods_map: Arc::new(methods_map), + } + } - Self { - methods: Arc::new(methods), + pub fn get(&self, method: &str) -> Option<&MethodMetrics> { + self.methods_map.get(method) } } - fn get(&self, method: &str) -> Option<&MethodMetrics> { - self.methods.get(method) + impl Default for RpcMetricsRegistry { + fn default() -> Self { + Self::new(DEFAULT_TRACKED_METHODS) + } } - pub fn middleware(self) -> RpcServiceBuilder> { - RpcServiceBuilder::new().layer(RpcMetricsLayer::new(self)) + /// Metrics layer for [jsonrpsee::server::RpcServiceBuilder]. + /// Uses [RpcMetricsService] to wrap each request to metrics collection logic. + /// + /// Note: [Self::default] creates itself from registry with [DEFAULT_TRACKED_METHODS]. + #[derive(Clone, Default)] + pub struct RpcMetricsLayer { + registry: RpcMetricsRegistry, } -} - -#[derive(Clone)] -pub struct RpcMetricsLayer { - registry: RpcMetricsRegistry, -} -impl RpcMetricsLayer { - fn new(registry: RpcMetricsRegistry) -> Self { - Self { registry } + impl RpcMetricsLayer { + /// Creates new [RpcMetricsLayer] from registry. + pub fn from_registry(registry: RpcMetricsRegistry) -> Self { + Self { registry } + } } -} -impl Layer for RpcMetricsLayer { - type Service = RpcMetricsService; + impl Layer for RpcMetricsLayer { + type Service = RpcMetricsService; - fn layer(&self, service: S) -> Self::Service { - RpcMetricsService { - service, - registry: self.registry.clone(), + fn layer(&self, service: S) -> Self::Service { + RpcMetricsService { + service, + registry: self.registry.clone(), + } } } -} -#[derive(Clone)] -struct MethodMetrics { - calls_started: Counter, - calls_finished_ok: Counter, - calls_finished_err: Counter, - calls_latency_seconds: Histogram, - calls_response_size_bytes: Histogram, - calls_in_flight: Gauge, -} + #[derive(Clone)] + pub struct RpcMetricsService { + service: S, + registry: RpcMetricsRegistry, + } -impl MethodMetrics { - fn new(method: &'static str) -> Self { - Self { - calls_started: counter!("ethexe_rpc_calls_started_total", "method" => method), - calls_finished_ok: counter!( - "ethexe_rpc_calls_finished_total", - "method" => method, - "status" => "ok" - ), - calls_finished_err: counter!( - "ethexe_rpc_calls_finished_total", - "method" => method, - "status" => "error" - ), - calls_latency_seconds: histogram!( - "ethexe_rpc_call_duration_seconds", - "method" => method - ), - calls_response_size_bytes: histogram!( - "ethexe_rpc_response_size_bytes", - "method" => method - ), - calls_in_flight: gauge!("ethexe_rpc_calls_in_flight", "method" => method), + impl<'a, S> RpcServiceT<'a> for RpcMetricsService + where + S: RpcServiceT<'a> + Send + Sync, + S::Future: Send + 'a, + { + type Future = BoxFuture<'a, MethodResponse>; + + fn call(&self, request: Request<'a>) -> Self::Future { + let metrics = self.registry.get(request.method_name()).cloned(); + let future = self.service.call(request); + + Box::pin(async move { + let Some(metrics) = metrics else { + return future.await; + }; + + metrics.on_incoming_request(); + let started_at = Instant::now(); + + let response = future.await; + metrics.on_outgoing_response(started_at, &response); + response + }) } } } -#[derive(Clone)] -pub struct RpcMetricsService { - service: S, - registry: RpcMetricsRegistry, -} - -impl<'a, S> RpcServiceT<'a> for RpcMetricsService -where - S: RpcServiceT<'a> + Send + Sync, - S::Future: Send + 'a, -{ - type Future = BoxFuture<'a, MethodResponse>; - - fn call(&self, request: Request<'a>) -> Self::Future { - let metrics = self.registry.get(request.method_name()).cloned(); - let future = self.service.call(request); - - Box::pin(async move { - let Some(metrics) = metrics else { - return future.await; - }; - - metrics.calls_started.increment(1); - metrics.calls_in_flight.increment(1); - let _in_flight_guard = scopeguard::guard(metrics.calls_in_flight.clone(), |gauge| { - gauge.decrement(1); - }); - let started_at = Instant::now(); +/// Metrics type definitions. +#[allow(clippy::module_inception)] +mod metrics { + use jsonrpsee::server::MethodResponse; + use metrics::{Counter, Gauge, Histogram}; + use std::time::Instant; + + /// Default methods names tracked by [super::RpcMetricsLayer]. + pub const DEFAULT_TRACKED_METHODS: &[&str] = &[ + "injected_sendTransaction", + "injected_sendTransactionAndWatch", + "program_calculateReplyForHandle", + ]; + + /// Unified bundle of metrics for RPC method. + /// [metrics_derive::Metrics] macro will register all metrics under the `ethexe_rpc_*` scope. + /// + /// ## Must use + /// This object must be created using [MethodMetrics::new_with_labels] method. + /// This method will construct all metrics with provided unique label. + #[derive(Clone, metrics_derive::Metrics)] + #[metrics(scope = "ethexe_rpc")] + pub struct MethodMetrics { + #[metric( + rename = "calls_started_total", + describe = "Number of started RPC calls for the method" + )] + calls_started: Counter, + #[metric( + rename = "calls_finished_total", + labels = [("status", "ok")], + describe = "Number of successfully finished RPC calls for the method" + )] + calls_finished_ok: Counter, + #[metric( + rename = "calls_finished_total", + labels = [("status", "error")], + describe = "Number of failed RPC calls for the method" + )] + calls_finished_err: Counter, + #[metric( + rename = "call_duration_seconds", + describe = "Latency of RPC calls for the method in seconds" + )] + calls_latency_seconds: Histogram, + #[metric( + rename = "calls_in_flight", + describe = "Number of in-flight RPC calls for the method" + )] + calls_in_flight: Gauge, + } - let response = future.await; + impl MethodMetrics { + pub fn on_incoming_request(&self) { + self.calls_started.increment(1); + self.calls_in_flight.increment(1); + } - metrics - .calls_latency_seconds + pub fn on_outgoing_response(&self, started_at: Instant, response: &MethodResponse) { + self.calls_latency_seconds .record(started_at.elapsed().as_secs_f64()); - metrics - .calls_response_size_bytes - .record(response.as_result().len() as f64); - - if response.is_success() { - metrics.calls_finished_ok.increment(1); - } else { - metrics.calls_finished_err.increment(1); + + match response.is_success() { + true => self.calls_finished_ok.increment(1), + false => self.calls_finished_err.increment(1), } - response - }) + + self.calls_in_flight.decrement(1); + } + } + + /// The metrics for internal state of [crate::apis::InjectedApi]. + #[derive(Clone, metrics_derive::Metrics)] + #[metrics(scope = "ethexe_rpc_injected_api")] + pub struct InjectedApiMetrics { + #[metric( + rename = "active_promise_subscriptions", + describe = "Number of active subscriptions for injected transaction's promise" + )] + pub injected_tx_active_subscriptions: Gauge, } } From 6c22281ee50ec671f3a28cc52da9e2487461a58e Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 21 Apr 2026 20:37:19 +0300 Subject: [PATCH 49/59] chore: remove layer's method --- ethexe/rpc/src/lib.rs | 14 +++++--------- ethexe/rpc/src/metrics.rs | 9 +-------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index 92e5da28010..b6eb0254df4 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -68,7 +68,7 @@ use jsonrpsee::{ RpcModule as JsonrpcModule, server::{PingConfig, RpcServiceBuilder, Server, ServerHandle}, }; -use metrics::{RpcMetricsLayer, RpcMetricsRegistry}; +use metrics::RpcMetricsLayer; use std::{ net::SocketAddr, pin::Pin, @@ -130,10 +130,8 @@ impl RpcServer { let cors_layer = self.cors_layer()?; let http_middleware = tower::ServiceBuilder::new().layer(cors_layer); - - let rpc_metrics_registry = RpcMetricsRegistry::default(); - let rpc_middleware = - RpcServiceBuilder::new().layer(RpcMetricsLayer::from_registry(rpc_metrics_registry)); + // Setup the default RPC metrics layer. + let rpc_middleware = RpcServiceBuilder::new().layer(RpcMetricsLayer::default()); let processor = Processor::with_config( ProcessorConfig { @@ -163,7 +161,7 @@ impl RpcServer { .enable_ws_ping(PingConfig::default()) .build(self.config.listen_addr) .await? - .start(server_apis.into_methods()); + .start(server_apis.into_module()); Ok((server_handle, RpcService::new(rpc_receiver, injected_api))) } @@ -229,11 +227,9 @@ struct RpcServerApis { } impl RpcServerApis { - pub fn into_methods(self) -> jsonrpsee::server::RpcModule<()> { + pub fn into_module(self) -> jsonrpsee::server::RpcModule<()> { let mut module = JsonrpcModule::new(()); - // let rpc = self.block.into_rpc(); - // let callbacks = rpc.method_names(); module .merge(BlockServer::into_rpc(self.block)) .expect("No conflicts"); diff --git a/ethexe/rpc/src/metrics.rs b/ethexe/rpc/src/metrics.rs index e4a2c197a8d..84527f6963a 100644 --- a/ethexe/rpc/src/metrics.rs +++ b/ethexe/rpc/src/metrics.rs @@ -19,7 +19,7 @@ //! Metrics for the RPC server. pub use metrics::*; -pub use middleware::{RpcMetricsLayer, RpcMetricsRegistry}; +pub use middleware::RpcMetricsLayer; mod middleware { use super::metrics::{DEFAULT_TRACKED_METHODS, MethodMetrics}; @@ -72,13 +72,6 @@ mod middleware { registry: RpcMetricsRegistry, } - impl RpcMetricsLayer { - /// Creates new [RpcMetricsLayer] from registry. - pub fn from_registry(registry: RpcMetricsRegistry) -> Self { - Self { registry } - } - } - impl Layer for RpcMetricsLayer { type Service = RpcMetricsService; From c1837589f47824aafbda7d0855ffe9db11685d81 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Thu, 23 Apr 2026 15:19:05 +0300 Subject: [PATCH 50/59] Reopen pull request #5132 --- Cargo.lock | 81 ++- Cargo.toml | 1 + core/src/rpc.rs | 22 + ethexe/common/Cargo.toml | 1 + ethexe/common/src/db.rs | 12 +- ethexe/common/src/injected.rs | 190 +++++-- ethexe/common/src/mock.rs | 23 +- ethexe/common/src/primitives.rs | 22 + ethexe/compute/Cargo.toml | 1 + ethexe/compute/src/compute.rs | 99 ++-- ethexe/compute/src/service.rs | 17 +- ethexe/compute/src/tests.rs | 18 +- ethexe/consensus/src/connect/mod.rs | 15 +- ethexe/consensus/src/lib.rs | 4 +- ethexe/consensus/src/validator/producer.rs | 13 +- ethexe/db/src/database.rs | 45 +- ethexe/network/src/gossipsub.rs | 6 +- ethexe/network/src/lib.rs | 15 +- ethexe/network/src/validator/topic.rs | 97 ++-- ethexe/rpc/Cargo.toml | 2 + ethexe/rpc/src/apis/injected.rs | 495 ------------------ ethexe/rpc/src/apis/injected/mod.rs | 55 ++ .../rpc/src/apis/injected/promise_manager.rs | 177 +++++++ ethexe/rpc/src/apis/injected/relay.rs | 216 ++++++++ ethexe/rpc/src/apis/injected/server.rs | 280 ++++++++++ ethexe/rpc/src/apis/injected/spawner.rs | 75 +++ ethexe/rpc/src/apis/injected/trait.rs | 64 +++ ethexe/rpc/src/apis/mod.rs | 16 +- ethexe/rpc/src/lib.rs | 16 +- ethexe/rpc/src/tests.rs | 49 +- ethexe/service/src/lib.rs | 37 +- ethexe/service/src/tests/mod.rs | 14 +- ethexe/service/src/tests/utils/env.rs | 9 +- ethexe/service/src/tests/utils/events.rs | 4 +- 34 files changed, 1440 insertions(+), 751 deletions(-) delete mode 100644 ethexe/rpc/src/apis/injected.rs create mode 100644 ethexe/rpc/src/apis/injected/mod.rs create mode 100644 ethexe/rpc/src/apis/injected/promise_manager.rs create mode 100644 ethexe/rpc/src/apis/injected/relay.rs create mode 100644 ethexe/rpc/src/apis/injected/server.rs create mode 100644 ethexe/rpc/src/apis/injected/spawner.rs create mode 100644 ethexe/rpc/src/apis/injected/trait.rs diff --git a/Cargo.lock b/Cargo.lock index 60eab74a2ee..a3941cdf47a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1511,9 +1511,9 @@ checksum = "155a5a185e42c6b77ac7b88a15143d930a9e9727a5b7b77eed417404ab15c247" [[package]] name = "assert_cmd" -version = "2.1.2" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" dependencies = [ "anstyle", "bstr", @@ -1985,7 +1985,7 @@ dependencies = [ "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.13.0", "log", "prettyplease 0.2.37", "proc-macro2", @@ -2002,7 +2002,7 @@ version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ - "bitcoin_hashes 0.13.0", + "bitcoin_hashes 0.14.1", ] [[package]] @@ -2186,6 +2186,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bon" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" +dependencies = [ + "darling 0.23.0", + "ident_case", + "prettyplease 0.2.37", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.114", +] + [[package]] name = "borsh" version = "1.6.0" @@ -2912,7 +2937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -5095,7 +5120,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5183,12 +5208,14 @@ dependencies = [ "sha3", "sp-core", "tap", + "thiserror 2.0.17", ] [[package]] name = "ethexe-compute" version = "1.10.0" dependencies = [ + "bon", "demo-ping", "derive_more 2.1.1", "ethexe-common", @@ -5479,8 +5506,10 @@ dependencies = [ "metrics-derive", "ntest", "parity-scale-codec", + "scopeguard", "serde", "sp-core", + "thiserror 2.0.17", "tokio", "tower 0.4.13", "tower-http 0.5.2", @@ -8700,7 +8729,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -9167,7 +9196,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -9518,9 +9547,9 @@ dependencies = [ [[package]] name = "keccak-asm" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b646a74e746cd25045aa0fd42f4f7f78aa6d119380182c7e63a5593c4ab8df6f" +checksum = "fa468878266ad91431012b3e5ef1bf9b170eab22883503a318d46857afa4579a" dependencies = [ "digest 0.10.7", "sha3-asm", @@ -11694,7 +11723,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9224be3459a0c1d6e9b0f42ab0e76e98b29aef5aba33c0487dfcf47ea08b5150" dependencies = [ - "proc-macro-crate 1.1.3", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 1.0.109", @@ -11706,7 +11735,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -13982,7 +14011,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck 0.4.1", + "heck 0.5.0", "itertools 0.12.1", "log", "multimap 0.10.1", @@ -14217,7 +14246,7 @@ dependencies = [ "quinn-udp 0.5.14", "rustc-hash 2.1.1", "rustls 0.23.36", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tracing", @@ -14316,9 +14345,9 @@ dependencies = [ "cfg_aliases 0.2.1", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -15079,7 +15108,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -15092,7 +15121,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -15195,7 +15224,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -15216,7 +15245,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 1.0.5", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -17026,9 +17055,9 @@ dependencies = [ [[package]] name = "sha3-asm" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b31139435f327c93c6038ed350ae4588e2c70a13d50599509fee6349967ba35a" +checksum = "59cbb88c189d6352cc8ae96a39d19c7ecad8f7330b29461187f2587fdc2988d5" dependencies = [ "cc", "cfg-if", @@ -17139,9 +17168,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "sketches-ddsketch" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" +checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" [[package]] name = "slab" @@ -18812,7 +18841,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -20956,7 +20985,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 30a207b166a..20f5b87e56d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -228,6 +228,7 @@ tap = "1.0.1" ntest = "0.9.3" dashmap = "5.5.3" delegate = "0.13.5" +bon = "3.9.1" # metrics metrics = "0.24.0" diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 74117b44578..0fa12fc4ec6 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -20,11 +20,14 @@ use alloc::vec::Vec; use gear_core_errors::ReplyCode; +use gprimitives::H256; use parity_scale_codec::{Decode, Encode}; use scale_decode::DecodeAsType; use scale_encode::EncodeAsType; use scale_info::TypeInfo; +use crate::utils; + /// Pre-calculated gas consumption estimate for a message. /// /// Intended to be used as a result in `calculateGasFor*` RPC calls. @@ -65,6 +68,25 @@ pub struct ReplyInfo { pub code: ReplyCode, } +impl ReplyInfo { + /// Calculates `blake2b` hash from [`ReplyInfo`]. + pub fn to_hash(&self) -> H256 { + let ReplyInfo { + payload, + value, + code, + } = self; + + let bytes = [ + payload.as_ref(), + value.to_be_bytes().as_ref(), + code.to_bytes().as_ref(), + ] + .concat(); + utils::hash(&bytes).into() + } +} + /// Serializer and deserializer for ReplyCode as 0x-prefixed hex string. #[cfg(feature = "std")] pub(crate) mod serialize_reply_code { diff --git a/ethexe/common/Cargo.toml b/ethexe/common/Cargo.toml index 29a6297f007..51fee03a4da 100644 --- a/ethexe/common/Cargo.toml +++ b/ethexe/common/Cargo.toml @@ -28,6 +28,7 @@ gsigner = { workspace = true, default-features = false, features = [ sha3.workspace = true k256 = { version = "0.13.4", features = ["ecdsa"], default-features = false } nonempty.workspace = true +thiserror.workspace = true # mock deps itertools = { workspace = true, optional = true } diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index a20bbfd4338..dc5b8ec83ea 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -23,7 +23,7 @@ use crate::{ Schedule, SimpleBlockData, ValidatorsVec, events::BlockEvent, gear::StateTransition, - injected::{InjectedTransaction, SignedInjectedTransaction}, + injected::{InjectedTransaction, Promise, SignedCompactPromise, SignedInjectedTransaction}, }; use alloc::{ collections::{BTreeSet, VecDeque}, @@ -117,11 +117,21 @@ pub trait InjectedStorageRO { &self, hash: HashOf, ) -> Option; + + /// Returns the promise by its transaction hash. + fn promise(&self, hash: HashOf) -> Option; + + /// Returns the compact promise by its transaction hash. + fn compact_promise(&self, hash: HashOf) -> Option; } #[auto_impl::auto_impl(&)] pub trait InjectedStorageRW: InjectedStorageRO { fn set_injected_transaction(&self, tx: SignedInjectedTransaction); + + fn set_promise(&self, promise: &Promise); + + fn set_compact_promise(&self, promise: &SignedCompactPromise); } #[derive(Debug, Clone, Default, Encode, Decode, TypeInfo, PartialEq, Eq, Hash)] diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index c6443831344..b584509fe50 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -21,6 +21,7 @@ use alloc::string::{String, ToString}; use core::hash::Hash; use gear_core::{limited::LimitedVec, rpc::ReplyInfo}; use gprimitives::{ActorId, H256, MessageId}; +use gsigner::{PrivateKey, secp256k1::signature::SignResult}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sha3::{Digest, Keccak256}; @@ -144,26 +145,139 @@ pub struct Promise { /// It will be shared among other validators as a proof of promise. pub type SignedPromise = SignedMessage; +impl Promise { + /// Calculates the `blake2b` hash from promise's reply. + pub fn reply_hash(&self) -> HashOf { + // Safety by implementation + unsafe { HashOf::new(self.reply.to_hash()) } + } + + /// Converts promise to its compact version. + pub fn to_compact(&self) -> CompactPromise { + CompactPromise { + tx_hash: self.tx_hash, + reply_hash: self.reply_hash(), + } + } +} + impl ToDigest for Promise { fn update_hasher(&self, hasher: &mut sha3::Keccak256) { - let Self { tx_hash, reply } = self; + self.to_compact().update_hasher(hasher); + } +} + +/// A signed wrapper on top of [`CompactPromise`]. +/// +/// [`SignedCompactPromise`] is a lightweight version of [`SignedPromise`], that is +/// needed to reduce the amount of data transferred in network between validators. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Deref, derive_more::From)] +pub struct SignedCompactPromise(SignedMessage); + +/// The hashes of [`Promise`] parts. +#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)] +pub struct CompactPromise { + pub tx_hash: HashOf, + pub reply_hash: HashOf, +} + +impl ToDigest for CompactPromise { + fn update_hasher(&self, hasher: &mut sha3::Keccak256) { + let Self { + tx_hash, + reply_hash, + } = self; hasher.update(tx_hash.inner()); - let ReplyInfo { - payload, - code, - value, - } = reply; + hasher.update(reply_hash.inner()); + } +} + +impl SignedCompactPromise { + /// Create the [`SignedCompactPromise`] from private key and hashes. + pub fn create(private_key: PrivateKey, promise_hashes: CompactPromise) -> SignResult { + SignedMessage::create(private_key, promise_hashes).map(SignedCompactPromise) + } + + pub fn create_from_promise(private_key: PrivateKey, promise: &Promise) -> SignResult { + Self::create(private_key, promise.to_compact()) + } + + /// Create the [`SignedCompactPromise`] from a valid [`SignedPromise`]. + /// + /// # Panics + /// Panics if the digest of [`Promise`] and [`CompactPromise`] ever diverge. + /// This must hold by construction; tests enforce the invariant. + pub fn from_signed_promise(signed_promise: &SignedPromise) -> Self { + let compact = signed_promise.data().to_compact(); + let (signature, address) = (*signed_promise.signature(), signed_promise.address()); + + let signed_compact = SignedMessage::try_from_parts(compact, signature, address) + .expect("SignedPromise and CompactPromise must have identical digest"); + Self(signed_compact) + } +} - hasher.update(payload); - hasher.update(code.to_bytes()); - hasher.update(value.to_be_bytes()); +/// Restores the [SignedPromise] from parts: [Promise], [SignedCompactPromise]. +pub fn restore_signed_promise( + promise: Promise, + compact: &SignedCompactPromise, +) -> Result { + if promise.tx_hash != compact.data().tx_hash { + return Err(RestorePromiseError::HashesMismatch { + promise_tx_hash: promise.tx_hash, + compact_tx_hash: compact.data().tx_hash, + }); } + + SignedMessage::try_from_parts(promise, *compact.signature(), compact.address()) + .map_err(RestorePromiseError::InvalidSignature) +} + +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum RestorePromiseError { + #[error( + "promise and compact promise has different tx hashes: promise_tx_hash={promise_tx_hash:?}, compact_tx_hash={compact_tx_hash:?}" + )] + HashesMismatch { + promise_tx_hash: HashOf, + compact_tx_hash: HashOf, + }, + #[error("compact promise signature do not match promise: {0}")] + InvalidSignature(&'static str), } -#[cfg(test)] +/// Encoding and decoding of `LimitedVec` as hex string. +#[cfg(feature = "std")] +mod serde_hex { + pub fn serialize( + data: &super::LimitedVec, + serializer: S, + ) -> Result + where + S: serde::Serializer, + { + alloy_primitives::hex::serialize(data.to_vec(), serializer) + } + + pub fn deserialize<'de, D, const N: usize>( + deserializer: D, + ) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let vec: Vec = alloy_primitives::hex::deserialize(deserializer)?; + super::LimitedVec::::try_from(vec) + .map_err(|_| serde::de::Error::custom("LimitedVec deserialization overflow")) + } +} + +#[cfg(all(test, feature = "mock"))] mod tests { + use gsigner::PrivateKey; + use super::*; + use crate::mock::Mock; #[test] fn signed_message_and_injected_transactions() { @@ -202,29 +316,43 @@ mod tests { signed_tx.address() ); } -} -/// Encoding and decoding of `LimitedVec` as hex string. -#[cfg(feature = "std")] -mod serde_hex { - pub fn serialize( - data: &super::LimitedVec, - serializer: S, - ) -> Result - where - S: serde::Serializer, - { - alloy_primitives::serde_hex::serialize(data.to_vec(), serializer) + #[test] + fn promise_hashes_digest_equal_to_promise_digest() { + let promise = Promise::mock(()); + + assert_eq!(promise.to_digest(), promise.to_compact().to_digest()); } - pub fn deserialize<'de, D, const N: usize>( - deserializer: D, - ) -> Result, D::Error> - where - D: serde::Deserializer<'de>, - { - let vec: Vec = alloy_primitives::serde_hex::deserialize(deserializer)?; - super::LimitedVec::::try_from(vec) - .map_err(|_| serde::de::Error::custom("LimitedVec deserialization overflow")) + #[test] + fn signatures_equal_for_promise_and_compact_promise() { + let private_key = PrivateKey::random(); + let promise = Promise::mock(()); + + let signed_promise = SignedPromise::create(private_key.clone(), promise.clone()).unwrap(); + let compact_signed_promise = + SignedCompactPromise::create_from_promise(private_key, &promise).unwrap(); + + assert_eq!(signed_promise.address(), compact_signed_promise.address()); + assert_eq!( + signed_promise.signature().clone(), + compact_signed_promise.signature().clone() + ); + } + + #[test] + fn compact_signed_promise_correctly_builds_from_signed_promise() { + let private_key = PrivateKey::random(); + let promise = Promise::mock(()); + + let signed_promise = SignedPromise::create(private_key.clone(), promise).unwrap(); + + let compact_signed_promise = SignedCompactPromise::from_signed_promise(&signed_promise); + + assert_eq!(signed_promise.address(), compact_signed_promise.address()); + assert_eq!( + signed_promise.signature().clone(), + compact_signed_promise.signature().clone() + ); } } diff --git a/ethexe/common/src/mock.rs b/ethexe/common/src/mock.rs index 2bc84264114..c31a6e3b8e2 100644 --- a/ethexe/common/src/mock.rs +++ b/ethexe/common/src/mock.rs @@ -24,12 +24,14 @@ use crate::{ ecdsa::{PrivateKey, SignedMessage}, events::BlockEvent, gear::{BatchCommitment, ChainCommitment, CodeCommitment, Message, StateTransition}, - injected::{AddressedInjectedTransaction, InjectedTransaction}, + injected::{AddressedInjectedTransaction, InjectedTransaction, Promise}, }; use alloc::{collections::BTreeMap, vec}; use gear_core::{ code::{CodeMetadata, InstrumentedCode}, limited::LimitedVec, + message::{ReplyCode, SuccessReplyReason}, + rpc::ReplyInfo, }; use gprimitives::{ActorId, CodeId, H256, MessageId}; use itertools::Itertools; @@ -460,6 +462,25 @@ impl Arbitrary for AddressedInjectedTransaction { } } +impl Mock<()> for Promise { + fn mock(_args: ()) -> Self { + Promise::mock(HashOf::random()) + } +} + +impl Mock> for Promise { + fn mock(tx_hash: HashOf) -> Self { + Promise { + tx_hash, + reply: ReplyInfo { + payload: H256::random().0.to_vec(), + value: 42, + code: ReplyCode::Success(SuccessReplyReason::Manual), + }, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SyncedBlockData { pub header: BlockHeader, diff --git a/ethexe/common/src/primitives.rs b/ethexe/common/src/primitives.rs index 2cffdc40700..a67e0f9ba7a 100644 --- a/ethexe/common/src/primitives.rs +++ b/ethexe/common/src/primitives.rs @@ -161,6 +161,17 @@ pub enum PromisePolicy { Disabled, } +/// The [PromiseEmissionMode] configures the promise emission mode for the ethexe node +#[derive(Debug, Copy, Clone, PartialEq, Eq, derive_more::IsVariant, Default)] +pub enum PromiseEmissionMode { + /// Node should always emit promises during announces execution. + /// Always set [`PromisePolicy::Enabled`]. + AlwaysEmit, + /// [`PromisePolicy`] is set by consensus service. + #[default] + ConsensusDriven, +} + #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, Default, Encode, Decode, TypeInfo)] #[cfg_attr(feature = "std", derive(serde::Serialize))] pub struct StateHashWithQueueSize { @@ -251,6 +262,17 @@ pub struct ProtocolTimelines { pub slot: NonZeroU64, } +impl Default for ProtocolTimelines { + fn default() -> Self { + Self { + genesis_ts: 0, + era: NonZeroU64::new(10_000).unwrap(), + election: 200, + slot: NonZeroU64::new(2).unwrap(), + } + } +} + impl ProtocolTimelines { /// Returns the era index for the given timestamp. Eras starts from 0. /// diff --git a/ethexe/compute/Cargo.toml b/ethexe/compute/Cargo.toml index a964902cc3d..dd81df62952 100644 --- a/ethexe/compute/Cargo.toml +++ b/ethexe/compute/Cargo.toml @@ -22,6 +22,7 @@ derive_more.workspace = true log.workspace = true gear-workspace-hack.workspace = true future-timing.workspace = true +bon.workspace = true # metrics metrics.workspace = true diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index e2f67248057..99a7684c4db 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -18,7 +18,7 @@ use crate::{ComputeError, ComputeEvent, ProcessorExt, Result, service::SubService}; use ethexe_common::{ - Announce, HashOf, PromisePolicy, SimpleBlockData, + Announce, HashOf, PromiseEmissionMode, PromisePolicy, SimpleBlockData, db::{ AnnounceStorageRO, AnnounceStorageRW, BlockMetaStorageRO, CodesStorageRW, ConfigStorageRO, GlobalsStorageRW, OnChainStorageRO, @@ -37,12 +37,6 @@ use std::{ }; use tokio::sync::mpsc; -#[derive(Debug, Clone, Copy)] -pub struct ComputeConfig { - /// The delay in **blocks** in which events from Ethereum will be apply. - canonical_quarantine: u8, -} - /// Metrics for the [`ComputeSubService`]. #[derive(Clone, metrics_derive::Metrics)] #[metrics(scope = "ethexe_compute_compute")] @@ -51,25 +45,24 @@ struct Metrics { announce_processing_latency: metrics::Histogram, } -impl ComputeConfig { - /// Constructs [`ComputeConfig`] with provided `canonical_quarantine`. - /// In production builds `canonical_quarantine` should be equal [`ethexe_common::gear::CANONICAL_QUARANTINE`]. - pub fn new(canonical_quarantine: u8) -> Self { - Self { - canonical_quarantine, - } - } - - /// Must use only in testing purposes. - pub fn without_quarantine() -> Self { - Self { - canonical_quarantine: 0, - } - } +/// Configuration for [ComputeSubService]. +#[derive(Debug, Clone, Copy, bon::Builder)] +#[cfg_attr(test, derive(Default))] +pub struct ComputeConfig { + /// The delay in **blocks** in which events from Ethereum will be apply. + canonical_quarantine: u8, + /// The promises emission rule. + promises_mode: PromiseEmissionMode, +} +impl ComputeConfig { pub fn canonical_quarantine(&self) -> u8 { self.canonical_quarantine } + + pub fn promises_mode(&self) -> PromiseEmissionMode { + self.promises_mode + } } /// Type alias for computation future with timing. @@ -132,10 +125,23 @@ impl ComputeSubService

{ not_computed_announces.len(), ); + let promise_tx = match config.promises_mode() { + // If AlwaysEmit promises mode - we pass promises tx also for not computed chain. + PromiseEmissionMode::AlwaysEmit => promise_out_tx.clone(), + // Set the promise_out_tx = None, because in this case we want to receive promises only from target announce. + PromiseEmissionMode::ConsensusDriven => None, + }; + for (announce_hash, announce) in not_computed_announces { - // Set the promise_out_tx = None, because we want to receive the promises only from target announce. - Self::compute_one(&db, &mut processor, config, announce_hash, announce, None) - .await?; + Self::compute_one( + &db, + &mut processor, + config, + announce_hash, + announce, + promise_tx.clone(), + ) + .await?; } } @@ -201,18 +207,19 @@ impl SubService for ComputeSubService

{ && self.promises_stream.is_none() && let Some((announce, promise_policy)) = self.input.pop_front() { - let maybe_promise_out_tx = match promise_policy { - PromisePolicy::Enabled => { - let (sender, receiver) = mpsc::unbounded_channel(); - self.promises_stream = Some(utils::AnnouncePromisesStream::new( - receiver, - announce.to_hash(), - )); - - Some(sender) - } - PromisePolicy::Disabled => None, - }; + let maybe_promise_out_tx = + match utils::resolve_promise_policy(promise_policy, self.config.promises_mode()) { + PromisePolicy::Enabled => { + let (sender, receiver) = mpsc::unbounded_channel(); + self.promises_stream = Some(utils::AnnouncePromisesStream::new( + receiver, + announce.to_hash(), + )); + + Some(sender) + } + PromisePolicy::Disabled => None, + }; self.computation = Some(future_timing::timed( Self::compute( @@ -274,6 +281,18 @@ pub(crate) mod utils { use futures::Stream; use std::pin::Pin; + /// Resolves [PromisePolicy] with consensus provided policy and global + /// [PromiseEmissionMode] set for node. + pub(super) fn resolve_promise_policy( + consensus_policy: PromisePolicy, + mode: PromiseEmissionMode, + ) -> PromisePolicy { + match mode { + PromiseEmissionMode::AlwaysEmit => PromisePolicy::Enabled, + PromiseEmissionMode::ConsensusDriven => consensus_policy, + } + } + /// The stream of promises from announce execution. pub(super) struct AnnouncePromisesStream { receiver: mpsc::UnboundedReceiver, @@ -534,7 +553,7 @@ mod tests { let db = Database::memory(); let block_hash = BlockChain::mock(1).setup(&db).blocks[1].hash; - let config = ComputeConfig::without_quarantine(); + let config = ComputeConfig::default(); let mut service = ComputeSubService::new( config, db.clone(), @@ -639,7 +658,7 @@ mod tests { .collect::>(); let mut compute_service = - ComputeService::new(ComputeConfig::without_quarantine(), db.clone(), processor); + ComputeService::new(ComputeConfig::default(), db.clone(), processor); // Send announces for computation. compute_service.compute_announce( @@ -736,7 +755,7 @@ mod tests { }; let mut compute_service = - ComputeService::new(ComputeConfig::without_quarantine(), db.clone(), processor); + ComputeService::new(ComputeConfig::default(), db.clone(), processor); compute_service.compute_announce(announce, PromisePolicy::Enabled); loop { diff --git a/ethexe/compute/src/service.rs b/ethexe/compute/src/service.rs index 5b96f0256a0..7d9ef81f0a6 100644 --- a/ethexe/compute/src/service.rs +++ b/ethexe/compute/src/service.rs @@ -53,9 +53,9 @@ impl ComputeService

{ #[cfg(test)] impl ComputeService { - /// Creates the processor with default [`ComputeConfig::without_quarantine`] and [`Processor`] with default config. + /// Creates the processor with default [`ComputeConfig`] and [`Processor`] with default config. pub fn new_with_defaults(db: Database) -> Self { - let config = ComputeConfig::without_quarantine(); + let config = ComputeConfig::default(); let processor = Processor::new(db.clone()).unwrap(); Self::new(config, db, processor) } @@ -64,11 +64,7 @@ impl ComputeService { #[cfg(test)] impl ComputeService { pub fn new_mock_processor(db: Database) -> Self { - Self::new( - ComputeConfig::without_quarantine(), - db, - MockProcessor::default(), - ) + Self::new(ComputeConfig::default(), db, MockProcessor::default()) } } @@ -211,11 +207,8 @@ mod tests { let db = DB::memory(); let processor = MockProcessor::with_default_valid_code() .tap_mut(|p| p.process_codes_result.as_mut().unwrap().code_id = code_id); - let mut service = ComputeService::new( - ComputeConfig::without_quarantine(), - db.clone(), - processor.clone(), - ); + let mut service = + ComputeService::new(ComputeConfig::default(), db.clone(), processor.clone()); // Create test code diff --git a/ethexe/compute/src/tests.rs b/ethexe/compute/src/tests.rs index 751491fb0d1..26fae95d196 100644 --- a/ethexe/compute/src/tests.rs +++ b/ethexe/compute/src/tests.rs @@ -348,11 +348,7 @@ async fn code_validation_request_for_already_processed_code_does_not_request_loa let db = Database::memory(); let processor = MockProcessor::default(); - let mut compute = ComputeService::new( - ComputeConfig::without_quarantine(), - db.clone(), - processor.clone(), - ); + let mut compute = ComputeService::new(ComputeConfig::default(), db.clone(), processor.clone()); let code = create_new_code(1); let code_id = db.set_original_code(&code); @@ -413,11 +409,7 @@ async fn code_validation_request_for_non_validated_code_requests_loading() -> Re let db = Database::memory(); let processor = MockProcessor::default(); - let mut compute = ComputeService::new( - ComputeConfig::without_quarantine(), - db.clone(), - processor.clone(), - ); + let mut compute = ComputeService::new(ComputeConfig::default(), db.clone(), processor.clone()); let code = create_new_code(1); let code_id = db.set_original_code(&code); @@ -466,11 +458,7 @@ async fn process_code_for_already_processed_valid_code_emits_code_processed() -> let db = Database::memory(); let processor = MockProcessor::default(); - let mut compute = ComputeService::new( - ComputeConfig::without_quarantine(), - db.clone(), - processor.clone(), - ); + let mut compute = ComputeService::new(ComputeConfig::default(), db.clone(), processor.clone()); let code = create_new_code(2); let code_id = db.set_original_code(&code); diff --git a/ethexe/consensus/src/connect/mod.rs b/ethexe/consensus/src/connect/mod.rs index 9e3a525cc52..533e14f8b06 100644 --- a/ethexe/consensus/src/connect/mod.rs +++ b/ethexe/consensus/src/connect/mod.rs @@ -289,17 +289,12 @@ impl ConsensusService for ConnectService { fn receive_promise_for_signing( &mut self, - promise: Promise, - announce_hash: HashOf, + _promise: Promise, + _announce_hash: HashOf, ) -> Result<()> { - tracing::error!( - "Connected consensus node receives the promise for signing, but it not responsible for promises providing: \ - promise={promise:?}, announce_hash={announce_hash}" - ); - debug_assert!( - false, - "Connect node received the promise for signing, this should never happen" - ); + // Nothing to do. + // This case is not error because connect node can be also RPC node that produce promises, + // to send them for external users. Ok(()) } diff --git a/ethexe/consensus/src/lib.rs b/ethexe/consensus/src/lib.rs index e6365e0887c..6ecc5e963dc 100644 --- a/ethexe/consensus/src/lib.rs +++ b/ethexe/consensus/src/lib.rs @@ -203,7 +203,7 @@ use anyhow::Result; use ethexe_common::{ Announce, Digest, HashOf, PromisePolicy, SimpleBlockData, consensus::{BatchCommitmentValidationReply, VerifiedAnnounce, VerifiedValidationRequest}, - injected::{Promise, SignedInjectedTransaction, SignedPromise}, + injected::{Promise, SignedCompactPromise, SignedInjectedTransaction}, network::{AnnouncesRequest, AnnouncesResponse, SignedValidatorMessage}, }; use futures::{Stream, stream::FusedStream}; @@ -287,7 +287,7 @@ pub enum ConsensusEvent { #[from] PublishMessage(SignedValidatorMessage), #[from] - PublishPromise(SignedPromise), + PublishPromise(SignedCompactPromise), /// Outer service have to request announces #[from] RequestAnnounces(AnnouncesRequest), diff --git a/ethexe/consensus/src/validator/producer.rs b/ethexe/consensus/src/validator/producer.rs index 54c640bcb29..5093a92a5cc 100644 --- a/ethexe/consensus/src/validator/producer.rs +++ b/ethexe/consensus/src/validator/producer.rs @@ -27,8 +27,11 @@ use crate::{ use anyhow::{Context as _, Result, anyhow}; use derive_more::{Debug, Display}; use ethexe_common::{ - Announce, HashOf, PromisePolicy, SimpleBlockData, ValidatorsVec, db::BlockMetaStorageRO, - gear::BatchCommitment, injected::Promise, network::ValidatorMessage, + Announce, HashOf, PromisePolicy, SimpleBlockData, ValidatorsVec, + db::BlockMetaStorageRO, + gear::BatchCommitment, + injected::{Promise, SignedCompactPromise}, + network::ValidatorMessage, }; use ethexe_service_utils::Timer; use futures::{FutureExt, future::BoxFuture}; @@ -119,7 +122,11 @@ impl StateHandler for Producer { .core .signer .signed_message(self.ctx.core.pub_key, promise, None)?; - self.ctx.output(signed_promise); + let compact_signed_promise = + SignedCompactPromise::from_signed_promise(&signed_promise); + + self.ctx + .output(ConsensusEvent::PublishPromise(compact_signed_promise)); tracing::trace!("consensus sign promise for transaction-hash={tx_hash}"); Ok(self.into()) diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index 9ae3d1d633a..a7ca02e5af9 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -34,7 +34,7 @@ use ethexe_common::{ }, events::BlockEvent, gear::StateTransition, - injected::{InjectedTransaction, SignedInjectedTransaction}, + injected::{InjectedTransaction, Promise, SignedCompactPromise, SignedInjectedTransaction}, }; use ethexe_runtime_common::state::{ Allocations, DispatchStash, Mailbox, MemoryPages, MemoryPagesRegion, MessageQueue, @@ -81,6 +81,8 @@ enum Key { Announces(HashOf) = 17, BlockAnnounces(H256) = 18, + Promise(HashOf) = 19, + CompactPromise(HashOf) = 20, } impl Key { @@ -112,7 +114,9 @@ impl Key { | Self::AnnounceSchedule(hash) | Self::AnnounceMeta(hash) => bytes.extend(hash.as_ref()), - Self::InjectedTransaction(hash) => bytes.extend(hash.as_ref()), + Self::InjectedTransaction(hash) | Self::Promise(hash) | Self::CompactPromise(hash) => { + bytes.extend(hash.as_ref()) + } Self::ProgramToCodeId(program_id) => bytes.extend(program_id.as_ref()), @@ -717,6 +721,24 @@ impl InjectedStorageRO for RawDatabase { .expect("Failed to decode data into `SignedInjectedTransaction`") }) } + + fn promise(&self, tx_hash: HashOf) -> Option { + self.kv.get(&Key::Promise(tx_hash).to_bytes()).map(|data| { + Promise::decode(&mut data.as_slice()).expect("Failed to decode data into Promise") + }) + } + + fn compact_promise( + &self, + tx_hash: HashOf, + ) -> Option { + self.kv + .get(&Key::CompactPromise(tx_hash).to_bytes()) + .map(|data| { + SignedCompactPromise::decode(&mut data.as_slice()) + .expect("Failed to decode data into SignedCompactPromise") + }) + } } impl InjectedStorageRW for RawDatabase { @@ -727,6 +749,21 @@ impl InjectedStorageRW for RawDatabase { self.kv .put(&Key::InjectedTransaction(tx_hash).to_bytes(), tx.encode()); } + + fn set_promise(&self, promise: &Promise) { + tracing::trace!(?promise, "Set promise for injected transaction"); + + self.kv + .put(&Key::Promise(promise.tx_hash).to_bytes(), promise.encode()) + } + + fn set_compact_promise(&self, promise: &SignedCompactPromise) { + let tx_hash = promise.data().tx_hash; + tracing::trace!(?promise, "Set compact promise for injected transaction"); + + self.kv + .put(&Key::CompactPromise(tx_hash).to_bytes(), promise.encode()) + } } #[derive(derive_more::Debug, Clone)] @@ -946,12 +983,16 @@ impl AnnounceStorageRW for Database { impl InjectedStorageRO for Database { delegate!(to self.raw { fn injected_transaction(&self, hash: HashOf) -> Option; + fn promise(&self, hash: HashOf) -> Option; + fn compact_promise(&self, hash: HashOf) -> Option; }); } impl InjectedStorageRW for Database { delegate!(to self.raw { fn set_injected_transaction(&self, tx: SignedInjectedTransaction); + fn set_promise(&self, promise: &Promise); + fn set_compact_promise(&self, promise: &SignedCompactPromise); }); } diff --git a/ethexe/network/src/gossipsub.rs b/ethexe/network/src/gossipsub.rs index b5592a5d49e..cf1ee3e5bbc 100644 --- a/ethexe/network/src/gossipsub.rs +++ b/ethexe/network/src/gossipsub.rs @@ -23,7 +23,7 @@ use crate::{ peer_score, }; use anyhow::anyhow; -use ethexe_common::{Address, injected::SignedPromise, network::SignedValidatorMessage}; +use ethexe_common::{Address, injected::SignedCompactPromise, network::SignedValidatorMessage}; use libp2p::{ core::{Endpoint, transport::PortUse}, gossipsub, @@ -46,7 +46,7 @@ use std::{ pub enum Message { // TODO: rename to `Validators` Commitments(SignedValidatorMessage), - Promise(SignedPromise), + Promise(SignedCompactPromise), } impl Message { @@ -190,7 +190,7 @@ impl Behaviour { let res = if topic == self.commitments_topic.hash() { SignedValidatorMessage::decode(&mut &data[..]).map(Message::Commitments) } else if topic == self.promises_topic.hash() { - SignedPromise::decode(&mut &data[..]).map(Message::Promise) + SignedCompactPromise::decode(&mut &data[..]).map(Message::Promise) } else { unreachable!("topic we never subscribed to: {topic:?}"); }; diff --git a/ethexe/network/src/lib.rs b/ethexe/network/src/lib.rs index 8a4feb2ae42..322647de4a9 100644 --- a/ethexe/network/src/lib.rs +++ b/ethexe/network/src/lib.rs @@ -59,7 +59,7 @@ use ethexe_common::{ Address, BlockHeader, ValidatorsVec, db::ConfigStorageRO, ecdsa::PublicKey, - injected::{AddressedInjectedTransaction, SignedPromise}, + injected::{AddressedInjectedTransaction, SignedCompactPromise}, network::{SignedValidatorMessage, VerifiedValidatorMessage}, }; use ethexe_db::Database; @@ -110,7 +110,7 @@ pub enum NetworkEvent { /// A validator-signed message from the validator gossipsub topic. ValidatorMessage(VerifiedValidatorMessage), /// A public promise observed on the promise gossipsub topic. - PromiseMessage(SignedPromise), + PromiseMessage(SignedCompactPromise), /// Validator discovery learned or refreshed the network identity of the /// given validator address. ValidatorIdentityUpdated(Address), @@ -562,10 +562,10 @@ impl NetworkService { .verify_validator_message(source, message); (acceptance, message.map(NetworkEvent::ValidatorMessage)) } - gossipsub::Message::Promise(promise) => { + gossipsub::Message::Promise(compact_promise) => { // FIXME: previous era validators are ignored let (acceptance, promise) = - self.validator_topic.verify_promise(source, promise); + self.validator_topic.verify_promise(source, compact_promise); (acceptance, promise.map(NetworkEvent::PromiseMessage)) } }) @@ -668,8 +668,11 @@ impl NetworkService { } /// Publish a signed promise to the public promise gossipsub topic. - pub fn publish_promise(&mut self, promise: SignedPromise) { - self.swarm.behaviour_mut().gossipsub.publish(promise) + pub fn publish_promise(&mut self, compact_promise: SignedCompactPromise) { + self.swarm + .behaviour_mut() + .gossipsub + .publish(compact_promise) } } diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index efd94f3d29f..bd5c8ecd6db 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -25,7 +25,7 @@ use crate::{ }; use ethexe_common::{ Address, HashOf, - injected::{InjectedTransaction, SignedPromise}, + injected::{InjectedTransaction, SignedCompactPromise}, network::VerifiedValidatorMessage, }; use lru::LruCache; @@ -94,7 +94,7 @@ enum VerifyMessageError { Reject(VerifyMessageRejectReason), } -#[derive(Debug, PartialEq, Eq, derive_more::Display)] +#[derive(Debug, derive_more::Display)] enum VerifyPromiseError { #[display("unknown validator: address={address}, tx_hash={tx_hash}")] UnknownValidator { @@ -290,28 +290,29 @@ impl ValidatorTopic { fn inner_verify_promise( &self, _source: PeerId, - promise: SignedPromise, - ) -> Result { - let address = promise.address(); - let tx_hash = promise.data().tx_hash; - + compact_promise: SignedCompactPromise, + ) -> Result { + let address = compact_promise.address(); if !self.snapshot.contains(address) { - return Err(VerifyPromiseError::UnknownValidator { address, tx_hash }); + return Err(VerifyPromiseError::UnknownValidator { + address, + tx_hash: compact_promise.data().tx_hash, + }); } - Ok(promise) + Ok(compact_promise) } // FIXME: messages from previous era validators are ignored pub fn verify_promise( &self, source: PeerId, - promise: SignedPromise, - ) -> (MessageAcceptance, Option) { - match self.inner_verify_promise(source, promise) { - Ok(promise) => (MessageAcceptance::Accept, Some(promise)), + compact_promise: SignedCompactPromise, + ) -> (MessageAcceptance, Option) { + match self.inner_verify_promise(source, compact_promise) { + Ok(compact_promise) => (MessageAcceptance::Accept, Some(compact_promise)), Err(err) => { - log::trace!("failed to verify promise: {err}"); + log::trace!("failed to verify compact promise: {err}"); (MessageAcceptance::Ignore, None) } } @@ -328,13 +329,15 @@ mod tests { use super::*; use assert_matches::assert_matches; use ethexe_common::{ - Announce, - gear_core::{message::ReplyCode, rpc::ReplyInfo}, - injected::Promise, + self, Announce, + injected::{Promise, SignedPromise}, mock::Mock, network::{SignedValidatorMessage, ValidatorMessage}, }; - use gsigner::secp256k1::{Secp256k1SignerExt, Signer}; + use gsigner::{ + PublicKey, + secp256k1::{Secp256k1SignerExt, Signer}, + }; use nonempty::{NonEmpty, nonempty}; const CHAIN_HEAD_ERA: u64 = 10; @@ -375,19 +378,23 @@ mod tests { .into_verified() } - fn signed_promise() -> SignedPromise { + fn signer_with_pubkey() -> (PublicKey, Signer) { let signer = Signer::memory(); - let pub_key = signer.generate().unwrap(); - let promise = Promise { - tx_hash: Default::default(), - reply: ReplyInfo { - payload: vec![], - value: 0, - code: ReplyCode::Unsupported, - }, - }; + (signer.generate().unwrap(), signer) + } + + fn signed_promise(signer: Signer, public_key: PublicKey) -> SignedPromise { + let promise = Promise::mock(()); + signer.signed_message(public_key, promise, None).unwrap() + } - signer.signed_message(pub_key, promise, None).unwrap() + fn compact_signed_promise( + signer: &Signer, + public_key: PublicKey, + promise: Promise, + ) -> SignedCompactPromise { + let signed_promise = signer.signed_message(public_key, promise, None).unwrap(); + SignedCompactPromise::from_signed_promise(&signed_promise) } #[test] @@ -654,37 +661,41 @@ mod tests { #[test] fn verify_promise_unknown_validator() { let topic = new_topic(nonempty![Address::default()]); - let promise = signed_promise(); + + let (pubkey, signer) = signer_with_pubkey(); + let promise = signed_promise(signer.clone(), pubkey); + let compact_promise = compact_signed_promise(&signer, pubkey, promise.clone().into_data()); + let peer_id = PeerId::random(); let err = topic - .inner_verify_promise(peer_id, promise.clone()) + .inner_verify_promise(peer_id, compact_promise.clone()) .unwrap_err(); - assert_eq!( - err, - VerifyPromiseError::UnknownValidator { - address: promise.address(), - tx_hash: promise.data().tx_hash, - } - ); - let (acceptance, promise) = topic.verify_promise(peer_id, promise); + let VerifyPromiseError::UnknownValidator { address, tx_hash } = err; + assert_eq!(address, promise.address()); + assert_eq!(tx_hash, promise.data().tx_hash); + + let (acceptance, promise) = topic.verify_promise(peer_id, compact_promise); assert_matches!(acceptance, MessageAcceptance::Ignore); assert_eq!(promise, None); } #[tokio::test] async fn verify_promise_ok() { - let promise = signed_promise(); + let (pubkey, signer) = signer_with_pubkey(); + let promise = signed_promise(signer.clone(), pubkey); + let compact_promise = compact_signed_promise(&signer, pubkey, promise.clone().into_data()); + let topic = new_topic(nonempty![promise.address()]); let peer_id = PeerId::random(); topic - .inner_verify_promise(peer_id, promise.clone()) + .inner_verify_promise(peer_id, compact_promise.clone()) .unwrap(); - let (acceptance, returned_promise) = topic.verify_promise(peer_id, promise.clone()); + let (acceptance, returned_promise) = topic.verify_promise(peer_id, compact_promise.clone()); assert_matches!(acceptance, MessageAcceptance::Accept); - assert_eq!(returned_promise, Some(promise)); + assert_eq!(returned_promise, Some(compact_promise)); } } diff --git a/ethexe/rpc/Cargo.toml b/ethexe/rpc/Cargo.toml index 7398dc0ef79..5b66f50c660 100644 --- a/ethexe/rpc/Cargo.toml +++ b/ethexe/rpc/Cargo.toml @@ -31,6 +31,8 @@ dashmap.workspace = true metrics.workspace = true metrics-derive.workspace = true gear-workspace-hack.workspace = true +thiserror.workspace = true +scopeguard.workspace = true [dev-dependencies] jsonrpsee = { workspace = true, features = ["client"] } diff --git a/ethexe/rpc/src/apis/injected.rs b/ethexe/rpc/src/apis/injected.rs deleted file mode 100644 index ad9e72fc1a1..00000000000 --- a/ethexe/rpc/src/apis/injected.rs +++ /dev/null @@ -1,495 +0,0 @@ -// This file is part of Gear. -// -// Copyright (C) 2025 Gear Technologies Inc. -// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use crate::{RpcEvent, errors, metrics::InjectedApiMetrics}; -use anyhow::Result; -use dashmap::DashMap; -use ethexe_common::{ - Address, HashOf, - db::InjectedStorageRO, - injected::{ - AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, - SignedInjectedTransaction, SignedPromise, - }, -}; -use ethexe_db::Database; -use jsonrpsee::{ - PendingSubscriptionSink, SubscriptionMessage, SubscriptionSink, - core::{RpcResult, SubscriptionResult, async_trait}, - proc_macros::rpc, - types::error::ErrorObjectOwned, -}; -use std::sync::Arc; -use tokio::sync::{mpsc, oneshot}; - -const MAX_TRANSACTION_IDS: usize = 100; - -#[cfg_attr(not(feature = "client"), rpc(server, namespace = "injected"))] -#[cfg_attr(feature = "client", rpc(server, client, namespace = "injected"))] -pub trait Injected { - /// Just sends an injected transaction. - #[method(name = "sendTransaction")] - async fn send_transaction( - &self, - transaction: AddressedInjectedTransaction, - ) -> RpcResult; - - /// Sends an injected transaction and subscribes to its promise. - #[subscription( - name = "sendTransactionAndWatch", - unsubscribe = "sendTransactionAndWatchUnsubscribe", - item = SignedPromise - )] - async fn send_transaction_and_watch( - &self, - transaction: AddressedInjectedTransaction, - ) -> SubscriptionResult; - - /// Retrieves injected transactions by the provided IDs - #[method(name = "getTransactions")] - async fn get_transactions( - &self, - transaction_ids: Vec>, - ) -> RpcResult>>; -} - -type PromiseWaiters = Arc, oneshot::Sender>>; - -/// Implementation of the injected transactions RPC API. -#[derive(Debug, Clone)] -pub struct InjectedApi { - /// Node database instance. - db: Database, - /// Sender to forward RPC events to the main service. - rpc_sender: mpsc::UnboundedSender, - /// Map of promise waiters. - promise_waiters: PromiseWaiters, - /// The metrics related to [`InjectedApi`] - metrics: InjectedApiMetrics, -} - -#[async_trait] -impl InjectedServer for InjectedApi { - async fn send_transaction( - &self, - transaction: AddressedInjectedTransaction, - ) -> RpcResult { - tracing::trace!( - tx_hash = %transaction.tx.data().to_hash(), - ?transaction, - "Called injected_sendTransaction" - ); - self.forward_transaction(transaction).await - } - - async fn send_transaction_and_watch( - &self, - pending: PendingSubscriptionSink, - transaction: AddressedInjectedTransaction, - ) -> SubscriptionResult { - let tx_hash = transaction.tx.data().to_hash(); - tracing::trace!(%tx_hash, "Called injected_subscribeTransactionPromise"); - self.metrics.send_and_watch_injected_tx_calls.increment(1); - - // Check, that transaction wasn't already send. - if self.promise_waiters.get(&tx_hash).is_some() { - tracing::warn!(tx_hash = ?tx_hash, "transaction was already sent"); - return Err( - format!("transaction with the same hash was already sent: {tx_hash}").into(), - ); - } - - let _acceptance = self.forward_transaction(transaction).await?; - - // Try accept subscription, if some errors occur, just log them and return error to client. - let subscription_sink = pending.accept().await.inspect_err(|err| { - tracing::warn!("failed to accept subscription for injected transaction promise: {err}"); - })?; - - let (promise_sender, promise_receiver) = oneshot::channel(); - self.promise_waiters.insert(tx_hash, promise_sender); - self.spawn_promise_waiter(subscription_sink, promise_receiver, tx_hash); - - Ok(()) - } - - async fn get_transactions( - &self, - transaction_ids: Vec>, - ) -> RpcResult>> { - tracing::trace!(?transaction_ids, "Called injected_getTransactions"); - - if transaction_ids.len() > MAX_TRANSACTION_IDS { - return Err(errors::invalid_params(format!( - "Too many transaction ids requested. Maximum is {MAX_TRANSACTION_IDS}.", - ))); - } - - let transactions = transaction_ids - .into_iter() - .map(|tx_id| self.db.injected_transaction(tx_id)) - .collect::>>(); - - Ok(transactions) - } -} - -impl InjectedApi { - pub(crate) fn new(db: Database, rpc_sender: mpsc::UnboundedSender) -> Self { - Self { - db, - rpc_sender, - promise_waiters: PromiseWaiters::default(), - metrics: InjectedApiMetrics::default(), - } - } - - pub fn send_promise(&self, promise: SignedPromise) { - let Some((_, promise_sender)) = self.promise_waiters.remove(&promise.data().tx_hash) else { - tracing::warn!(promise = ?promise, "receive unregistered promise"); - return; - }; - - self.metrics.injected_tx_active_subscriptions.decrement(1); - - match promise_sender.send(promise.clone()) { - Ok(()) => { - self.metrics.injected_tx_promises_given.increment(1); - tracing::trace!(promise = ?promise, "sent promise to subscriber"); - } - Err(promise) => tracing::trace!(promise = ?promise, "rpc promise receiver dropped"), - } - } - - /// Returns the number of current promise subscribers waiting for promises. - #[cfg(test)] - pub fn promise_subscribers_count(&self) -> usize { - self.promise_waiters.len() - } - - /// This function forwards [`AddressedInjectedTransaction`] to main service and waits for its acceptance. - async fn forward_transaction( - &self, - mut transaction: AddressedInjectedTransaction, - ) -> Result { - let tx_hash = transaction.tx.data().to_hash(); - tracing::trace!(%tx_hash, ?transaction, "Called injected_sendTransaction with vars"); - self.metrics.send_injected_tx_calls.increment(1); - - let (response_sender, response_receiver) = oneshot::channel(); - - if transaction.tx.data().value != 0 { - tracing::warn!( - tx_hash = %tx_hash, - value = transaction.tx.data().value, - "Injected transaction with non-zero value is not supported" - ); - return Err(errors::bad_request( - "Injected transactions with non-zero value are not supported", - )); - } - - if transaction.recipient == Address::default() { - utils::route_transaction(&self.db, &mut transaction)?; - } - - let event = RpcEvent::InjectedTransaction { - transaction, - response_sender, - }; - - if let Err(err) = self.rpc_sender.send(event) { - tracing::error!( - "Failed to send `RpcEvent::InjectedTransaction` event task: {err}. \ - The receiving end in the main service might have been dropped." - ); - return Err(errors::internal()); - } - - tracing::trace!(%tx_hash, "Accept transaction, waiting for promise"); - - response_receiver.await.map_err(|e| { - // No panic case, as a responsibility of the RPC API is fulfilled. - // The dropped sender signalizes that the main service has crashed - // or is malformed, so problems should be handled there. - tracing::error!( - "Response sender for the `RpcEvent::InjectedTransaction` was dropped: {e}" - ); - errors::internal() - }) - } - - // Spawns a task that waits for the promise and sends it to the client. - fn spawn_promise_waiter( - &self, - sink: SubscriptionSink, - receiver: oneshot::Receiver, - tx_hash: HashOf, - ) { - // This clone is cheap, as it only increases the ref count. - let promise_waiters = self.promise_waiters.clone(); - self.metrics.injected_tx_active_subscriptions.increment(1); - let metrics = self.metrics.clone(); - - tokio::spawn(async move { - // Waiting for promise or client disconnection. - let promise = tokio::select! { - result = receiver => match result { - Ok(promise) => { - promise_waiters.remove(&tx_hash); - promise - } - Err(_) => { - unreachable!("promise sender is owned by the api; it cannot be dropped before this point") - } - }, - _ = sink.closed() => { - promise_waiters.remove(&tx_hash); - metrics.injected_tx_active_subscriptions.decrement(1); - return; - }, - }; - - let promise_msg = match SubscriptionMessage::from_json(&promise) { - Ok(msg) => msg, - Err(err) => { - tracing::error!( - error = %err, - "failed to create `SubscriptionMessage` from json object" - ); - return; - } - }; - - if let Err(err) = sink.send(promise_msg).await { - tracing::warn!( - tx_hash = ?tx_hash, - error = %err, - "failed to send subscription message" - ); - } - }); - } -} - -mod utils { - use super::*; - use anyhow::Context as _; - use ethexe_common::{ - Address, - db::{ConfigStorageRO, OnChainStorageRO}, - }; - use std::time::{Duration, SystemTime, SystemTimeError}; - - pub(super) const NEXT_PRODUCER_THRESHOLD_MS: u64 = 50; - - pub fn route_transaction( - db: &Database, - tx: &mut AddressedInjectedTransaction, - ) -> RpcResult<()> { - let now = now_since_unix_epoch().map_err(|err| { - tracing::error!("system clock error: {err}"); - crate::errors::internal() - })?; - - let next_producer = calculate_next_producer(db, now).map_err(|err| { - tracing::error!("calculate next producer error: {err}"); - crate::errors::internal() - })?; - tx.recipient = next_producer; - - Ok(()) - } - - /// Calculates the producer address to route an injected transaction to. - pub(super) fn calculate_next_producer(db: &Database, now: Duration) -> Result

{ - let timelines = db.config().timelines; - - // Calculate target timestamp, taking into account possible delays, so we append NEXT_PRODUCER_THRESHOLD_MS. - // The transaction should be included by the next producer, so we add `slot_duration` to the current time. - let target_timestamp = now - .checked_add(Duration::from_millis(NEXT_PRODUCER_THRESHOLD_MS)) - .context("current time is too close to u64::MAX, cannot calculate next producer")? - .as_secs() - .checked_add(timelines.slot.get()) - .context("current time is too close to u64::MAX, cannot calculate next producer")?; - - let era = timelines - .era_from_ts(target_timestamp) - .context("failed to calculate era from target timestamp")?; - - let validators = db - .validators(era) - .with_context(|| format!("validators not found for era={era}"))?; - - timelines - .block_producer_at(&validators, target_timestamp) - .context("failed to calculate block producer") - } - - /// Returns the current time since [SystemTime::UNIX_EPOCH]. - fn now_since_unix_epoch() -> Result { - SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) - } -} - -#[cfg(test)] -mod tests { - use super::{InjectedApi, InjectedServer, MAX_TRANSACTION_IDS, utils}; - use ethexe_common::{ - Address, ProtocolTimelines, ValidatorsVec, - db::{ConfigStorageRO, InjectedStorageRW, OnChainStorageRW, SetConfig}, - ecdsa::PrivateKey, - injected::{InjectedTransaction, SignedInjectedTransaction}, - mock::Mock, - }; - use ethexe_db::Database; - use gear_core::pages::num_traits::ToPrimitive; - use std::{ops::Sub, time::Duration}; - use tokio::sync::mpsc; - - const SLOT: u64 = 10; - const ERA: u64 = 1000; - - fn setup_db(db: &Database) -> ValidatorsVec { - let validators = ValidatorsVec::from_iter((0..10u64).map(Address::from)); - - let timelines = ProtocolTimelines { - genesis_ts: 0, - era: ERA.try_into().unwrap(), - election: 0, - slot: SLOT.try_into().unwrap(), - }; - db.set_validators(0, validators.clone()); - let mut config = db.config().clone(); - config.timelines = timelines; - db.set_config(config); - validators - } - - #[test] - fn test_calculate_next_producer_return_next() { - let db = Database::memory(); - let validators = setup_db(&db); - - let now = Duration::from_secs(SLOT / 2); - let producer = utils::calculate_next_producer(&db, now).unwrap(); - - assert_eq!(validators[1], producer); - } - - #[test] - fn test_calculate_next_producer_return_next_next() { - let db = Database::memory(); - let validators = setup_db(&db); - - let half_threshold = utils::NEXT_PRODUCER_THRESHOLD_MS.to_u64().unwrap(); - let now = Duration::from_secs(SLOT).sub(Duration::from_millis(half_threshold)); - let producer = utils::calculate_next_producer(&db, now).unwrap(); - - assert_eq!(validators[2], producer); - } - - #[test] - fn test_calculate_next_producer_in_next_era() { - let db = Database::memory(); - let validators = setup_db(&db); - - // Prepare next era validators - let mut next_era_validators = validators.clone(); - next_era_validators[0] = validators[9]; - db.set_validators(1, next_era_validators.clone()); - - let now = Duration::from_secs(ERA).sub(Duration::from_secs(1)); - let producer = utils::calculate_next_producer(&db, now).unwrap(); - - assert_eq!(next_era_validators[0], producer); - } - - fn make_signed_tx() -> SignedInjectedTransaction { - SignedInjectedTransaction::create(PrivateKey::random(), InjectedTransaction::mock(())) - .expect("creating signed injected transaction succeeds") - } - - fn make_injected_api(db: Database) -> InjectedApi { - let (sender, _receiver) = mpsc::unbounded_channel(); - InjectedApi::new(db, sender) - } - - #[tokio::test] - async fn test_get_transactions_found() { - let db = Database::memory(); - let api = make_injected_api(db.clone()); - - let tx = make_signed_tx(); - let tx_hash = tx.data().to_hash(); - db.set_injected_transaction(tx.clone()); - - let result = api.get_transactions(vec![tx_hash]).await.unwrap(); - assert_eq!(result, vec![Some(tx)]); - } - - #[tokio::test] - async fn test_get_transactions_not_found() { - let db = Database::memory(); - let api = make_injected_api(db.clone()); - - let tx_hash = make_signed_tx().data().to_hash(); - // Transaction not stored in DB. - let result = api.get_transactions(vec![tx_hash]).await.unwrap(); - assert_eq!(result, vec![None]); - } - - #[tokio::test] - async fn test_get_transactions_mixed() { - let db = Database::memory(); - let api = make_injected_api(db.clone()); - - let tx1 = make_signed_tx(); - let tx2 = make_signed_tx(); - let hash1 = tx1.data().to_hash(); - let hash2 = tx2.data().to_hash(); - db.set_injected_transaction(tx1.clone()); - // tx2 not stored. - - let result = api.get_transactions(vec![hash1, hash2]).await.unwrap(); - assert_eq!(result, vec![Some(tx1), None]); - } - - #[tokio::test] - async fn test_get_transactions_empty() { - let db = Database::memory(); - let api = make_injected_api(db.clone()); - - let result = api.get_transactions(vec![]).await.unwrap(); - assert!(result.is_empty()); - } - - #[tokio::test] - async fn test_get_transactions_exceeds_limit() { - let db = Database::memory(); - let api = make_injected_api(db.clone()); - - let ids = (0..=MAX_TRANSACTION_IDS) - .map(|_| make_signed_tx().data().to_hash()) - .collect(); - - let result = api.get_transactions(ids).await; - assert!(result.is_err()); - } -} diff --git a/ethexe/rpc/src/apis/injected/mod.rs b/ethexe/rpc/src/apis/injected/mod.rs new file mode 100644 index 00000000000..c1cffbf6785 --- /dev/null +++ b/ethexe/rpc/src/apis/injected/mod.rs @@ -0,0 +1,55 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! # RPC Server Injected API +//! +//! ## Promises Flow +//! [promise_manager::PromiseSubscriptionManager] is the main entity that is responsible for +//! promises handling. +//! Internally it maintains single-promise subscribers. +//! +//! After the manager successfully registers a subscriber for +//! [ethexe_common::injected::SignedPromise], it creates the +//! [promise_manager::PendingSubscriber] and spawns it using +//! [spawner::spawn_pending_subscriber]. +//! +//! **Important:** the pending subscriber will be dropped after +//! waiting for **20 * Ethereum slot** seconds to avoid dead subscribers. +//! +//! [promise_manager::PromiseSubscriptionManager] provides two methods for receiving promises: +//! - [promise_manager::PromiseSubscriptionManager::on_compact_promise] receives the promise +//! signature from the producer. If it matches a promise already stored in the database, it is +//! sent to the subscriber. +//! - [promise_manager::PromiseSubscriptionManager::on_computed_promise] receives the promise +//! body. When RPC receives the corresponding promise signature, it sends the signed promise to +//! the subscriber. + +pub(crate) mod promise_manager; + +pub(crate) mod relay; + +pub(crate) mod server; +pub use server::InjectedApi; + +pub(crate) mod spawner; + +mod r#trait; +pub use r#trait::InjectedServer; + +#[cfg(feature = "client")] +pub use r#trait::InjectedClient; diff --git a/ethexe/rpc/src/apis/injected/promise_manager.rs b/ethexe/rpc/src/apis/injected/promise_manager.rs new file mode 100644 index 00000000000..07e7c0c4aef --- /dev/null +++ b/ethexe/rpc/src/apis/injected/promise_manager.rs @@ -0,0 +1,177 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use anyhow::Result; +use dashmap::{DashMap, mapref::entry::Entry}; +use ethexe_common::{ + HashOf, + db::{InjectedStorageRO, InjectedStorageRW}, + injected::{ + InjectedTransaction, Promise, SignedCompactPromise, SignedPromise, restore_signed_promise, + }, +}; +use ethexe_db::Database; +use std::sync::Arc; +use tokio::sync::oneshot; +use tracing::trace; + +// TODO (kuzmindev): Currently, PromiseSubscriptionManager do not check, that transaction was +// sent by validator, so there must be pre-validation for data received from network (SignedCompactPromise). + +// TODO (kuzmindev): think about using `moka::sync::Cache` instead of DashMap +type PromiseSubscribers = Arc, oneshot::Sender>>; +type PromisesComputationWaiting = Arc, SignedCompactPromise>>; + +/// The manager for promise subscribers. +#[derive(Debug, Clone)] +pub struct PromiseSubscriptionManager { + db: Database, + subscribers: PromiseSubscribers, + + waiting_for_compute: PromisesComputationWaiting, +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum RegisterSubscriberError { + #[error("Subscriber for this transaction already exists, tx_hash={0}")] + AlreadyRegistered(HashOf), +} + +type TimeoutReceiver = tokio::time::Timeout>; + +/// The pending [SignedPromise] subscriber. +/// Subscriber will be spawned in separate tokio runtime task and will wait for promise. +/// +/// Important: to avoid infinite waiting we wrap [oneshot::Receiver] into [tokio::time::timeout]. +pub struct PendingSubscriber { + /// Tx hash waiting promise for. + tx_hash: HashOf, + /// Wrapped promise [oneshot::Receiver]. + receiver: TimeoutReceiver, +} + +impl PendingSubscriber { + pub fn new( + db: &Database, + tx_hash: HashOf, + receiver: oneshot::Receiver, + ) -> Self { + let timeout_duration = utils::promise_waiting_timeout(db); + let receiver = tokio::time::timeout(timeout_duration, receiver); + Self { tx_hash, receiver } + } + + pub fn into_parts(self) -> (HashOf, TimeoutReceiver) { + (self.tx_hash, self.receiver) + } +} + +impl PromiseSubscriptionManager { + pub fn new(db: Database) -> Self { + Self { + db, + subscribers: PromiseSubscribers::default(), + waiting_for_compute: PromisesComputationWaiting::default(), + } + } + + pub fn try_register_subscriber( + &self, + tx_hash: HashOf, + ) -> Result { + match self.subscribers.entry(tx_hash) { + Entry::Occupied(_) => Err(RegisterSubscriberError::AlreadyRegistered(tx_hash)), + Entry::Vacant(entry) => { + let (sender, receiver) = oneshot::channel(); + entry.insert(sender); + Ok(PendingSubscriber::new(&self.db, tx_hash, receiver)) + } + } + } + + pub fn cancel_registration( + &self, + tx_hash: HashOf, + ) -> Option> { + self.subscribers.remove(&tx_hash).map(|(_, v)| v) + } + + pub fn on_compact_promise(&self, compact: SignedCompactPromise) { + trace!(?compact, "received new compact promise"); + let tx_hash = compact.data().tx_hash; + + match self.db.promise(tx_hash) { + Some(promise) => match restore_signed_promise(promise, &compact) { + Ok(signed_promise) => { + self.db.set_compact_promise(&compact); + self.dispatch_promise(signed_promise); + } + + Err(_err) => { + trace!( + ?compact, %tx_hash, "failed to create signed promise from parts, producer send invalid signature: compact_promise={compact:?}" + ); + } + }, + None => { + trace!("not found promise in database, waiting for computation..."); + self.waiting_for_compute.insert(tx_hash, compact); + } + } + } + + pub fn on_computed_promise(&self, promise: Promise) { + trace!(?promise, "received new computed promise"); + self.db.set_promise(&promise); + + if let Some((_, compact_promise)) = self.waiting_for_compute.remove(&promise.tx_hash) { + match restore_signed_promise(promise, &compact_promise) { + Ok(signed_promise) => { + self.db.set_compact_promise(&compact_promise); + self.dispatch_promise(signed_promise); + } + Err(_err) => { + trace!(?compact_promise, tx_hash=?compact_promise.data().tx_hash, "failed to create signed promise from parts"); + } + } + } + } + + fn dispatch_promise(&self, promise: SignedPromise) { + if let Some((_, sender)) = self.subscribers.remove(&promise.data().tx_hash) + && let Err(unsent_promise) = sender.send(promise) + { + trace!("failed to send promise to subscriber, promise={unsent_promise:?}"); + } + } + + #[cfg(test)] + pub fn subscribers_count(&self) -> usize { + self.subscribers.len() + } +} + +mod utils { + use ethexe_common::db::ConfigStorageRO; + + /// Returns the maximum time that spawned [super::PendingSubscriber] will wait for promise. + pub fn promise_waiting_timeout(db: &DB) -> std::time::Duration { + let slot_duration_secs = db.config().timelines.slot.get(); + std::time::Duration::from_secs(slot_duration_secs * 20) + } +} diff --git a/ethexe/rpc/src/apis/injected/relay.rs b/ethexe/rpc/src/apis/injected/relay.rs new file mode 100644 index 00000000000..73c111736f9 --- /dev/null +++ b/ethexe/rpc/src/apis/injected/relay.rs @@ -0,0 +1,216 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::{RpcEvent, errors}; +use ethexe_common::{ + Address, + injected::{AddressedInjectedTransaction, InjectedTransactionAcceptance}, +}; +use ethexe_db::Database; +use jsonrpsee::core::RpcResult; +use tokio::sync::{mpsc, oneshot}; +use tracing::{error, trace, warn}; + +#[derive(Clone)] +pub struct TransactionsRelayer { + rpc_sender: mpsc::UnboundedSender, + db: Database, +} + +impl TransactionsRelayer { + pub fn new(rpc_sender: mpsc::UnboundedSender, db: Database) -> Self { + Self { rpc_sender, db } + } + + pub async fn relay( + &self, + mut transaction: AddressedInjectedTransaction, + ) -> RpcResult { + let tx_hash = transaction.tx.data().to_hash(); + trace!(%tx_hash, ?transaction, "Called injected_sendTransaction with vars"); + + // TODO: maybe should implement the transaction validator. + if transaction.tx.data().value != 0 { + warn!( + tx_hash = %tx_hash, + value = transaction.tx.data().value, + "Injected transaction with non-zero value is not supported" + ); + return Err(errors::bad_request( + "Injected transactions with non-zero value are not supported", + )); + } + + if transaction.recipient == Address::default() { + utils::route_transaction(&self.db, &mut transaction)?; + } + + let (response_sender, response_receiver) = oneshot::channel(); + let event = RpcEvent::InjectedTransaction { + transaction, + response_sender, + }; + + if let Err(err) = self.rpc_sender.send(event) { + error!( + "Failed to send `RpcEvent::InjectedTransaction` event task: {err}. \ + The receiving end in the main service might have been dropped." + ); + return Err(errors::internal()); + } + + trace!(%tx_hash, "Accept transaction, waiting for promise"); + + response_receiver.await.map_err(|err| { + // Expecting no errors here, because the rpc channel is owned by main server. + error!("Response sender for the `RpcEvent::InjectedTransaction` was dropped: {err}"); + errors::internal() + }) + } +} + +mod utils { + use super::*; + use anyhow::{Context as _, Result}; + use ethexe_common::{ + Address, + db::{ConfigStorageRO, OnChainStorageRO}, + }; + use std::time::{Duration, SystemTime, SystemTimeError}; + + pub(super) const NEXT_PRODUCER_THRESHOLD_MS: u64 = 50; + + pub fn route_transaction( + db: &Database, + tx: &mut AddressedInjectedTransaction, + ) -> RpcResult<()> { + let now = now_since_unix_epoch().map_err(|err| { + tracing::error!("system clock error: {err}"); + crate::errors::internal() + })?; + + let next_producer = calculate_next_producer(db, now).map_err(|err| { + tracing::error!("calculate next producer error: {err}"); + crate::errors::internal() + })?; + tx.recipient = next_producer; + + Ok(()) + } + + /// Calculates the producer address to route an injected transaction to. + pub(super) fn calculate_next_producer(db: &Database, now: Duration) -> Result
{ + let timelines = db.config().timelines; + + // Calculate target timestamp, taking into account possible delays, so we append NEXT_PRODUCER_THRESHOLD_MS. + // The transaction should be included by the next producer, so we add `slot_duration` to the current time. + let target_timestamp = now + .checked_add(Duration::from_millis(NEXT_PRODUCER_THRESHOLD_MS)) + .context("current time is too close to u64::MAX, cannot calculate next producer")? + .as_secs() + .checked_add(timelines.slot.get()) + .context("current time is too close to u64::MAX, cannot calculate next producer")?; + + let era = timelines + .era_from_ts(target_timestamp) + .context("failed to calculate era from target timestamp")?; + + let validators = db + .validators(era) + .with_context(|| format!("validators not found for era={era}"))?; + + timelines + .block_producer_at(&validators, target_timestamp) + .context("failed to calculate block producer") + } + + /// Returns the current time since [SystemTime::UNIX_EPOCH]. + fn now_since_unix_epoch() -> Result { + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) + } +} + +#[cfg(test)] +mod tests { + use super::utils; + use ethexe_common::{ + Address, ProtocolTimelines, ValidatorsVec, + db::{ConfigStorageRO, OnChainStorageRW, SetConfig}, + }; + use ethexe_db::Database; + use gear_core::pages::num_traits::ToPrimitive; + use std::{ops::Sub, time::Duration}; + + const SLOT: u64 = 10; + const ERA: u64 = 1000; + + fn setup_db(db: &Database) -> ValidatorsVec { + let validators = ValidatorsVec::from_iter((0..10u64).map(Address::from)); + + let timelines = ProtocolTimelines { + genesis_ts: 0, + era: ERA.try_into().unwrap(), + election: 0, + slot: SLOT.try_into().unwrap(), + }; + db.set_validators(0, validators.clone()); + let mut config = db.config().clone(); + config.timelines = timelines; + db.set_config(config); + validators + } + + #[test] + fn test_calculate_next_producer_return_next() { + let db = Database::memory(); + let validators = setup_db(&db); + + let now = Duration::from_secs(SLOT / 2); + let producer = utils::calculate_next_producer(&db, now).unwrap(); + + assert_eq!(validators[1], producer); + } + + #[test] + fn test_calculate_next_producer_return_next_next() { + let db = Database::memory(); + let validators = setup_db(&db); + + let half_threshold = utils::NEXT_PRODUCER_THRESHOLD_MS.to_u64().unwrap(); + let now = Duration::from_secs(SLOT).sub(Duration::from_millis(half_threshold)); + let producer = utils::calculate_next_producer(&db, now).unwrap(); + + assert_eq!(validators[2], producer); + } + + #[test] + fn test_calculate_next_producer_in_next_era() { + let db = Database::memory(); + let validators = setup_db(&db); + + // Prepare next era validators + let mut next_era_validators = validators.clone(); + next_era_validators[0] = validators[9]; + db.set_validators(1, next_era_validators.clone()); + + let now = Duration::from_secs(ERA).sub(Duration::from_secs(1)); + let producer = utils::calculate_next_producer(&db, now).unwrap(); + + assert_eq!(next_era_validators[0], producer); + } +} diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs new file mode 100644 index 00000000000..71de85f7ced --- /dev/null +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -0,0 +1,280 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::{RpcEvent, errors, metrics::InjectedApiMetrics}; + +use super::{ + InjectedServer, promise_manager::PromiseSubscriptionManager, relay::TransactionsRelayer, + spawner, +}; +use ethexe_common::{ + HashOf, + db::InjectedStorageRO, + injected::{ + AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, + SignedInjectedTransaction, SignedPromise, restore_signed_promise, + }, +}; +use ethexe_db::Database; +use jsonrpsee::{ + core::{RpcResult, SubscriptionResult, async_trait}, + server::PendingSubscriptionSink, +}; +use std::ops::Deref; +use tokio::sync::mpsc; +use tracing::trace; + +const MAX_TRANSACTION_IDS: usize = 100; + +#[derive(Clone)] +pub struct InjectedApi { + db: Database, + manager: PromiseSubscriptionManager, + relayer: TransactionsRelayer, + metrics: InjectedApiMetrics, +} + +// TODO: add metrics middleware for InjectedApi +#[async_trait] +impl InjectedServer for InjectedApi { + async fn send_transaction( + &self, + transaction: AddressedInjectedTransaction, + ) -> RpcResult { + self.send_transaction(transaction).await + } + + async fn send_transaction_and_watch( + &self, + pending: PendingSubscriptionSink, + transaction: AddressedInjectedTransaction, + ) -> SubscriptionResult { + self.send_transaction_and_watch(pending, transaction).await + } + + async fn get_transaction_promise( + &self, + tx_hash: HashOf, + ) -> RpcResult> { + self.get_transaction_promise(tx_hash).await + } + + async fn get_transactions( + &self, + transaction_ids: Vec>, + ) -> RpcResult>> { + self.get_transactions(transaction_ids).await + } +} + +impl Deref for InjectedApi { + type Target = PromiseSubscriptionManager; + + fn deref(&self) -> &Self::Target { + &self.manager + } +} + +impl InjectedApi { + pub fn new(db: Database, rpc_sender: mpsc::UnboundedSender) -> Self { + Self { + db: db.clone(), + manager: PromiseSubscriptionManager::new(db.clone()), + relayer: TransactionsRelayer::new(rpc_sender, db), + metrics: InjectedApiMetrics::default(), + } + } +} + +// RPC API implementation. +impl InjectedApi { + async fn send_transaction( + &self, + transaction: AddressedInjectedTransaction, + ) -> RpcResult { + self.metrics.send_injected_tx_calls.increment(1); + + self.relayer.relay(transaction).await + } + + async fn send_transaction_and_watch( + &self, + pending: PendingSubscriptionSink, + transaction: AddressedInjectedTransaction, + ) -> SubscriptionResult { + self.metrics.send_and_watch_injected_tx_calls.increment(1); + + let tx_hash = transaction.tx.data().to_hash(); + + let pending_subscriber = match self.manager.try_register_subscriber(tx_hash) { + Ok(subscriber) => subscriber, + Err(err) => { + return Err(errors::bad_request(err).into()); + } + }; + + let acceptance = self.relayer.relay(transaction).await.inspect_err(|_err| { + self.manager.cancel_registration(tx_hash); + })?; + let sink = match acceptance { + InjectedTransactionAcceptance::Accept => { + pending.accept().await.inspect_err(|_err| { + self.manager.cancel_registration(tx_hash); + })? + } + InjectedTransactionAcceptance::Reject { reason } => { + self.manager.cancel_registration(tx_hash); + return Err(reason.into()); + } + }; + + let manager = self.manager.clone(); + spawner::spawn_pending_subscriber(sink, pending_subscriber, move |tx_hash| { + manager.cancel_registration(tx_hash); + }); + Ok(()) + } + + async fn get_transaction_promise( + &self, + tx_hash: HashOf, + ) -> RpcResult> { + let Some(promise) = self.db.promise(tx_hash) else { + trace!(?tx_hash, "promise not found for injected transaction"); + return Ok(None); + }; + + let Some(compact) = self.db.compact_promise(tx_hash) else { + trace!( + ?tx_hash, + "compact promise not found for injected transaction" + ); + return Ok(None); + }; + + match restore_signed_promise(promise, &compact) { + Ok(message) => Ok(Some(message)), + Err(err) => { + trace!( + ?tx_hash, + ?err, + "failed to build signed promise from parts for injected transaction" + ); + Ok(None) + } + } + } + + async fn get_transactions( + &self, + transaction_ids: Vec>, + ) -> RpcResult>> { + tracing::trace!(?transaction_ids, "Called injected_getTransactions"); + + if transaction_ids.len() > MAX_TRANSACTION_IDS { + return Err(errors::invalid_params(format!( + "Too many transaction ids requested. Maximum is {MAX_TRANSACTION_IDS}.", + ))); + } + + let transactions = transaction_ids + .into_iter() + .map(|tx_id| self.db.injected_transaction(tx_id)) + .collect::>>(); + + Ok(transactions) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ethexe_common::{PrivateKey, db::InjectedStorageRW, mock::Mock}; + + fn make_signed_tx() -> SignedInjectedTransaction { + SignedInjectedTransaction::create(PrivateKey::random(), InjectedTransaction::mock(())) + .expect("creating signed injected transaction succeeds") + } + + fn make_injected_api(db: Database) -> InjectedApi { + let (sender, _receiver) = mpsc::unbounded_channel(); + InjectedApi::new(db, sender) + } + + #[tokio::test] + async fn test_get_transactions_found() { + let db = Database::memory(); + let api = make_injected_api(db.clone()); + + let tx = make_signed_tx(); + let tx_hash = tx.data().to_hash(); + db.set_injected_transaction(tx.clone()); + + let result = api.get_transactions(vec![tx_hash]).await.unwrap(); + assert_eq!(result, vec![Some(tx)]); + } + + #[tokio::test] + async fn test_get_transactions_not_found() { + let db = Database::memory(); + let api = make_injected_api(db.clone()); + + let tx_hash = make_signed_tx().data().to_hash(); + // Transaction not stored in DB. + let result = api.get_transactions(vec![tx_hash]).await.unwrap(); + assert_eq!(result, vec![None]); + } + + #[tokio::test] + async fn test_get_transactions_mixed() { + let db = Database::memory(); + let api = make_injected_api(db.clone()); + + let tx1 = make_signed_tx(); + let tx2 = make_signed_tx(); + let hash1 = tx1.data().to_hash(); + let hash2 = tx2.data().to_hash(); + db.set_injected_transaction(tx1.clone()); + // tx2 not stored. + + let result = api.get_transactions(vec![hash1, hash2]).await.unwrap(); + assert_eq!(result, vec![Some(tx1), None]); + } + + #[tokio::test] + async fn test_get_transactions_empty() { + let db = Database::memory(); + let api = make_injected_api(db.clone()); + + let result = api.get_transactions(vec![]).await.unwrap(); + assert!(result.is_empty()); + } + + #[tokio::test] + async fn test_get_transactions_exceeds_limit() { + let db = Database::memory(); + let api = make_injected_api(db.clone()); + + let ids = (0..=MAX_TRANSACTION_IDS) + .map(|_| make_signed_tx().data().to_hash()) + .collect(); + + let result = api.get_transactions(ids).await; + assert!(result.is_err()); + } +} diff --git a/ethexe/rpc/src/apis/injected/spawner.rs b/ethexe/rpc/src/apis/injected/spawner.rs new file mode 100644 index 00000000000..73f3702d814 --- /dev/null +++ b/ethexe/rpc/src/apis/injected/spawner.rs @@ -0,0 +1,75 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use super::promise_manager::PendingSubscriber; +use ethexe_common::{HashOf, injected::InjectedTransaction}; +use jsonrpsee::{SubscriptionMessage, SubscriptionSink}; +use tracing::{error, trace, warn}; + +/// Spawns [PendingSubscriber] in tokio runtime. +/// +/// On task finishing applies the `on_finish` function that is need to drop some data. +pub fn spawn_pending_subscriber( + sink: SubscriptionSink, + subscriber: PendingSubscriber, + on_finish: F, +) where + F: FnOnce(HashOf) + std::marker::Send + 'static, +{ + let (tx_hash, receiver) = subscriber.into_parts(); + + // TODO: think about using this handle for aborting runtime tasks in case of long waiting. + let _handle = tokio::spawn(async move { + let _guard = scopeguard::guard(tx_hash, on_finish); + + // Waiting for the first one: promise, timeout_err, client disconnect error. + let promise = tokio::select! { + result = receiver => match result { + Ok(promise_result) => match promise_result { + Ok(promise) => promise, + Err(_err) => { + unreachable!("promise sender is owned by the server; it cannot be dropped before this point"); + } + }, + Err(_) => { + warn!("promise wasn't received in time, finish waiting"); + return; + } + }, + _ = sink.closed() => { + trace!("subscription closed by user, stop background task"); + return; + } + }; + + match SubscriptionMessage::from_json(&promise) { + Ok(message) => { + if let Err(err) = sink.send(message).await { + trace!("failed to send promise, client disconnected: err={err}"); + } + } + Err(err) => { + error!( + ?promise, + ?err, + "serialization error: failed create `SubscriptionMessage` from promise; this must never happen" + ); + } + } + }); +} diff --git a/ethexe/rpc/src/apis/injected/trait.rs b/ethexe/rpc/src/apis/injected/trait.rs new file mode 100644 index 00000000000..8d3ad600a61 --- /dev/null +++ b/ethexe/rpc/src/apis/injected/trait.rs @@ -0,0 +1,64 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethexe_common::{ + HashOf, + injected::{ + AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, + SignedInjectedTransaction, SignedPromise, + }, +}; +use jsonrpsee::{ + core::{RpcResult, SubscriptionResult}, + proc_macros::rpc, +}; + +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "injected"))] +#[cfg_attr(feature = "client", rpc(server, client, namespace = "injected"))] +pub trait Injected { + /// Just sends an injected transaction. + #[method(name = "sendTransaction")] + async fn send_transaction( + &self, + transaction: AddressedInjectedTransaction, + ) -> RpcResult; + + /// Sends an injected transaction and subscribes to its promise. + #[subscription( + name = "sendTransactionAndWatch", + unsubscribe = "sendTransactionAndWatchUnsubscribe", + item = SignedPromise + )] + async fn send_transaction_and_watch( + &self, + transaction: AddressedInjectedTransaction, + ) -> SubscriptionResult; + + #[method(name = "getTransactionPromise")] + async fn get_transaction_promise( + &self, + tx_hash: HashOf, + ) -> RpcResult>; + + /// Retrieves injected transactions by the provided IDs + #[method(name = "getTransactions")] + async fn get_transactions( + &self, + transaction_ids: Vec>, + ) -> RpcResult>>; +} diff --git a/ethexe/rpc/src/apis/mod.rs b/ethexe/rpc/src/apis/mod.rs index 95c1904e9e2..25de0ea3726 100644 --- a/ethexe/rpc/src/apis/mod.rs +++ b/ethexe/rpc/src/apis/mod.rs @@ -22,16 +22,16 @@ mod dev; mod injected; mod program; +#[cfg(feature = "client")] +pub use crate::apis::{ + block::BlockClient, + code::CodeClient, + dev::DevClient, + injected::InjectedClient, + program::{FullProgramState, ProgramClient}, +}; pub use block::{BlockApi, BlockServer}; pub use code::{CodeApi, CodeServer}; pub use dev::{DevApi, DevServer}; pub use injected::{InjectedApi, InjectedServer}; pub use program::{ProgramApi, ProgramServer}; - -#[cfg(feature = "client")] -pub use crate::apis::{ - block::BlockClient, code::CodeClient, dev::DevClient, injected::InjectedClient, - program::ProgramClient, -}; -#[cfg(feature = "client")] -pub use program::FullProgramState; diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index 330d541930d..295061fc9dc 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -58,7 +58,7 @@ use apis::{ ProgramApi, ProgramServer, }; use ethexe_common::injected::{ - AddressedInjectedTransaction, InjectedTransactionAcceptance, SignedPromise, + AddressedInjectedTransaction, InjectedTransactionAcceptance, Promise, SignedCompactPromise, }; use ethexe_db::Database; use ethexe_processor::{Processor, ProcessorConfig}; @@ -192,16 +192,12 @@ impl RpcService { } } - /// Provides a promise inside RPC service to be sent to subscribers. - pub fn provide_promise(&self, promise: SignedPromise) { - self.injected_api.send_promise(promise); + pub fn receive_computed_promise(&self, promise: Promise) { + self.injected_api.on_computed_promise(promise); } - /// Provides a bundle of promises inside RPC service to be sent to subscribers. - pub fn provide_promises(&self, promises: Vec) { - promises.into_iter().for_each(|promise| { - self.provide_promise(promise); - }); + pub fn receive_compact_promise(&self, compact_promise: SignedCompactPromise) { + self.injected_api.on_compact_promise(compact_promise); } } @@ -231,6 +227,8 @@ impl RpcServerApis { pub fn into_methods(self) -> jsonrpsee::server::RpcModule<()> { let mut module = JsonrpcModule::new(()); + // let rpc = self.block.into_rpc(); + // let callbacks = rpc.method_names(); module .merge(BlockServer::into_rpc(self.block)) .expect("No conflicts"); diff --git a/ethexe/rpc/src/tests.rs b/ethexe/rpc/src/tests.rs index 24aa8c29e9b..1bab4b43134 100644 --- a/ethexe/rpc/src/tests.rs +++ b/ethexe/rpc/src/tests.rs @@ -20,19 +20,16 @@ use crate::{ InjectedApi, InjectedClient, InjectedTransactionAcceptance, RpcConfig, RpcEvent, RpcServer, RpcService, }; - use ethexe_common::{ + db::InjectedStorageRW, ecdsa::PrivateKey, gear::MAX_BLOCK_GAS_LIMIT, - injected::{AddressedInjectedTransaction, Promise, SignedPromise}, + injected::{AddressedInjectedTransaction, Promise, SignedCompactPromise}, mock::Mock, }; use ethexe_db::Database; use futures::StreamExt; -use gear_core::{ - message::{ReplyCode, SuccessReplyReason}, - rpc::ReplyInfo, -}; +use gear_core::message::{ReplyCode, SuccessReplyReason}; use jsonrpsee::{server::ServerHandle, ws_client::WsClientBuilder}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use tokio::task::{JoinHandle, JoinSet}; @@ -42,13 +39,15 @@ use tokio::task::{JoinHandle, JoinSet}; struct MockService { rpc: RpcService, handle: ServerHandle, + db: Database, } impl MockService { /// Creates a new mock service which runs an RPC server listening on the given address. pub async fn new(listen_addr: SocketAddr) -> Self { - let (handle, rpc) = start_new_server(listen_addr).await; - Self { rpc, handle } + let db = Database::memory(); + let (handle, rpc) = start_new_server(listen_addr, db.clone()).await; + Self { rpc, handle, db } } pub fn injected_api(&self) -> InjectedApi { @@ -67,8 +66,10 @@ impl MockService { loop { tokio::select! { _ = tx_batch_interval.tick() => { - let promises = tx_batch.drain(..).map(Self::create_promise_for).collect(); - self.rpc.provide_promises(promises); + let promises = self.promises_bundle(tx_batch.drain(..)); + promises.into_iter().for_each(|promise| { + self.rpc.receive_compact_promise(promise); + }); }, _ = self.handle.clone().stopped() => { unreachable!("RPC server should not be stopped during the test") @@ -84,21 +85,23 @@ impl MockService { }) } - fn create_promise_for(tx: AddressedInjectedTransaction) -> SignedPromise { - let promise = Promise { - tx_hash: tx.tx.data().to_hash(), - reply: ReplyInfo { - payload: vec![], - value: 0, - code: ReplyCode::Success(SuccessReplyReason::Manual), - }, - }; - SignedPromise::create(PrivateKey::random(), promise).expect("Signing promise will succeed") + fn promises_bundle( + &self, + txs: impl IntoIterator, + ) -> Vec { + let pk = PrivateKey::random(); + txs.into_iter() + .map(|tx| { + let promise = Promise::mock(tx.tx.data().to_hash()); + self.db.set_promise(&promise); + SignedCompactPromise::create_from_promise(pk.clone(), &promise).unwrap() + }) + .collect() } } /// Starts a new RPC server listening on the given address. -async fn start_new_server(listen_addr: SocketAddr) -> (ServerHandle, RpcService) { +async fn start_new_server(listen_addr: SocketAddr, db: Database) -> (ServerHandle, RpcService) { let rpc_config = RpcConfig { listen_addr, cors: None, @@ -106,7 +109,7 @@ async fn start_new_server(listen_addr: SocketAddr) -> (ServerHandle, RpcService) chunk_size: 2, with_dev_api: false, }; - RpcServer::new(rpc_config, Database::memory()) + RpcServer::new(rpc_config, db) .run_server() .await .expect("RPC Server will start successfully") @@ -114,7 +117,7 @@ async fn start_new_server(listen_addr: SocketAddr) -> (ServerHandle, RpcService) /// This helper function waits until all promise subscriptions being closed and cleaned up. async fn wait_for_closed_subscriptions(injected_api: InjectedApi) { - while injected_api.promise_subscribers_count() > 0 { + while injected_api.subscribers_count() > 0 { tokio::time::sleep(std::time::Duration::from_millis(10)).await; } } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 662dde49aac..7937e7615c3 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -56,7 +56,8 @@ use anyhow::{Context, Result, bail}; use async_trait::async_trait; use ethexe_blob_loader::{BlobLoader, BlobLoaderEvent, BlobLoaderService, ConsensusLayerConfig}; use ethexe_common::{ - COMMITMENT_DELAY_LIMIT, CodeAndIdUnchecked, gear::CodeState, network::VerifiedValidatorMessage, + COMMITMENT_DELAY_LIMIT, CodeAndIdUnchecked, PromiseEmissionMode, gear::CodeState, + network::VerifiedValidatorMessage, }; use ethexe_compute::{ComputeConfig, ComputeEvent, ComputeService}; use ethexe_consensus::{ @@ -313,6 +314,11 @@ impl Service { None }; + let rpc = config + .rpc + .clone() + .map(|config| RpcServer::new(config, db.clone())); + let observer = ObserverService::new( db.clone(), ObserverConfig { @@ -446,12 +452,15 @@ impl Service { None }; - let rpc = config - .rpc - .as_ref() - .map(|config| RpcServer::new(config.clone(), db.clone())); - - let compute_config = ComputeConfig::new(config.node.canonical_quarantine); + // RPC-node always requires promises + let promises_mode = match rpc.is_some() { + true => PromiseEmissionMode::AlwaysEmit, + false => PromiseEmissionMode::ConsensusDriven, + }; + let compute_config = ComputeConfig::builder() + .canonical_quarantine(config.node.canonical_quarantine) + .promises_mode(promises_mode) + .build(); let processor_config = ProcessorConfig { chunk_size: config.node.chunk_processing_threads, }; @@ -630,6 +639,10 @@ impl Service { // Nothing } ComputeEvent::Promise(promise, announce_hash) => { + if let Some(ref rpc) = rpc { + rpc.receive_computed_promise(promise.clone()); + } + consensus.receive_promise_for_signing(promise, announce_hash)?; } }, @@ -677,9 +690,9 @@ impl Service { let _res = response_sender.send(acceptance); } }, - NetworkEvent::PromiseMessage(promise) => { + NetworkEvent::PromiseMessage(compact_promise) => { if let Some(rpc) = &rpc { - rpc.provide_promise(promise); + rpc.receive_compact_promise(compact_promise); } } NetworkEvent::ValidatorIdentityUpdated(_) @@ -727,17 +740,17 @@ impl Service { ConsensusEvent::ComputeAnnounce(announce, promise_policy) => { compute.compute_announce(announce, promise_policy) } - ConsensusEvent::PublishPromise(signed_promise) => { + ConsensusEvent::PublishPromise(compact_promise) => { if rpc.is_none() && network.is_none() { panic!("Promise without network or rpc"); } if let Some(rpc) = &rpc { - rpc.provide_promise(signed_promise.clone()); + rpc.receive_compact_promise(compact_promise.clone()); } if let Some(network) = &mut network { - network.publish_promise(signed_promise); + network.publish_promise(compact_promise); } } ConsensusEvent::PublishMessage(message) => { diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index bdf90bc9b44..80c73020197 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -30,7 +30,7 @@ use alloy::{ providers::{Provider as _, WalletProvider, ext::AnvilApi}, }; use ethexe_common::{ - Announce, HashOf, ScheduledTask, ToDigest, + Announce, HashOf, PromiseEmissionMode, ScheduledTask, ToDigest, db::*, ecdsa::ContractSignature, events::{ @@ -2199,8 +2199,12 @@ async fn validators_election() { async fn execution_with_canonical_events_quarantine() { init_logger(); + let compute_config = ComputeConfig::builder() + .canonical_quarantine(CANONICAL_QUARANTINE) + .promises_mode(Default::default()) + .build(); let config = TestEnvConfig { - compute_config: ComputeConfig::new(CANONICAL_QUARANTINE), + compute_config, ..Default::default() }; let mut env = TestEnv::new(config).await.unwrap(); @@ -2645,7 +2649,6 @@ async fn injected_tx_fungible_token() { let env_config = TestEnvConfig { network: EnvNetworkConfig::Enabled, - compute_config: ComputeConfig::without_quarantine(), ..Default::default() }; @@ -2876,7 +2879,10 @@ async fn injected_tx_fungible_token_over_network() { let env_config = TestEnvConfig { network: EnvNetworkConfig::Enabled, - compute_config: ComputeConfig::without_quarantine(), + compute_config: ComputeConfig::builder() + .canonical_quarantine(Default::default()) + .promises_mode(PromiseEmissionMode::AlwaysEmit) + .build(), ..Default::default() }; diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index 67bd134322b..a1fd024bff5 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -818,7 +818,10 @@ impl Default for TestEnvConfig { network: EnvNetworkConfig::Disabled, deploy_params: Default::default(), commitment_delay_limit: COMMITMENT_DELAY_LIMIT, - compute_config: ComputeConfig::without_quarantine(), + compute_config: ComputeConfig::builder() + .canonical_quarantine(Default::default()) + .promises_mode(Default::default()) + .build(), } } } @@ -1055,8 +1058,8 @@ impl Node { let rpc = self .service_rpc_config - .as_ref() - .map(|service_rpc_config| RpcServer::new(service_rpc_config.clone(), self.db.clone())); + .clone() + .map(|config| RpcServer::new(config, self.db.clone())); self.receiver = Some(receiver); diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index 29f61220f03..8a0d6d0cf51 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -27,7 +27,7 @@ use ethexe_common::{ events::BlockEvent, injected::{ AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, - SignedInjectedTransaction, SignedPromise, + SignedCompactPromise, SignedInjectedTransaction, }, network::VerifiedValidatorMessage, }; @@ -85,7 +85,7 @@ impl TestingNetworkInjectedEvent { #[derive(Debug, Clone, Eq, PartialEq)] pub enum TestingNetworkEvent { ValidatorMessage(VerifiedValidatorMessage), - PromiseMessage(SignedPromise), + PromiseMessage(SignedCompactPromise), ValidatorIdentityUpdated(Address), InjectedTransaction(TestingNetworkInjectedEvent), PeerBlocked(PeerId), From 540c499af54843262897d5575e2f534c72efd3aa Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Thu, 23 Apr 2026 16:36:08 +0300 Subject: [PATCH 51/59] chore: small refactoring | self-review --- Cargo.lock | 1 - core/src/rpc.rs | 4 +- ethexe/common/Cargo.toml | 1 - ethexe/common/src/injected.rs | 37 +++-------------- ethexe/compute/src/compute.rs | 40 +++++++------------ ethexe/network/src/validator/topic.rs | 2 +- .../rpc/src/apis/injected/promise_manager.rs | 19 +++++---- ethexe/rpc/src/apis/injected/server.rs | 4 +- ethexe/rpc/src/lib.rs | 2 - 9 files changed, 33 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a3941cdf47a..4722a123be8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5208,7 +5208,6 @@ dependencies = [ "sha3", "sp-core", "tap", - "thiserror 2.0.17", ] [[package]] diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 0fa12fc4ec6..2c8af9afa41 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -26,8 +26,6 @@ use scale_decode::DecodeAsType; use scale_encode::EncodeAsType; use scale_info::TypeInfo; -use crate::utils; - /// Pre-calculated gas consumption estimate for a message. /// /// Intended to be used as a result in `calculateGasFor*` RPC calls. @@ -83,7 +81,7 @@ impl ReplyInfo { code.to_bytes().as_ref(), ] .concat(); - utils::hash(&bytes).into() + super::utils::hash(&bytes).into() } } diff --git a/ethexe/common/Cargo.toml b/ethexe/common/Cargo.toml index 51fee03a4da..29a6297f007 100644 --- a/ethexe/common/Cargo.toml +++ b/ethexe/common/Cargo.toml @@ -28,7 +28,6 @@ gsigner = { workspace = true, default-features = false, features = [ sha3.workspace = true k256 = { version = "0.13.4", features = ["ecdsa"], default-features = false } nonempty.workspace = true -thiserror.workspace = true # mock deps itertools = { workspace = true, optional = true } diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index b584509fe50..187ee16fd88 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -212,41 +212,16 @@ impl SignedCompactPromise { let compact = signed_promise.data().to_compact(); let (signature, address) = (*signed_promise.signature(), signed_promise.address()); - let signed_compact = SignedMessage::try_from_parts(compact, signature, address) - .expect("SignedPromise and CompactPromise must have identical digest"); - Self(signed_compact) + SignedMessage::try_from_parts(compact, signature, address) + .expect("SignedPromise and CompactPromise must have identical digest") + .into() } -} -/// Restores the [SignedPromise] from parts: [Promise], [SignedCompactPromise]. -pub fn restore_signed_promise( - promise: Promise, - compact: &SignedCompactPromise, -) -> Result { - if promise.tx_hash != compact.data().tx_hash { - return Err(RestorePromiseError::HashesMismatch { - promise_tx_hash: promise.tx_hash, - compact_tx_hash: compact.data().tx_hash, - }); + /// Tries to restore the [SignedPromise] with provided [Promise] body. + pub fn restore(&self, promise: Promise) -> Result { + SignedMessage::try_from_parts(promise, *self.0.signature(), self.0.address()) } - - SignedMessage::try_from_parts(promise, *compact.signature(), compact.address()) - .map_err(RestorePromiseError::InvalidSignature) } - -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] -pub enum RestorePromiseError { - #[error( - "promise and compact promise has different tx hashes: promise_tx_hash={promise_tx_hash:?}, compact_tx_hash={compact_tx_hash:?}" - )] - HashesMismatch { - promise_tx_hash: HashOf, - compact_tx_hash: HashOf, - }, - #[error("compact promise signature do not match promise: {0}")] - InvalidSignature(&'static str), -} - /// Encoding and decoding of `LimitedVec` as hex string. #[cfg(feature = "std")] mod serde_hex { diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 99a7684c4db..c8f42146267 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -205,21 +205,21 @@ impl SubService for ComputeSubService

{ fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll> { if self.computation.is_none() && self.promises_stream.is_none() - && let Some((announce, promise_policy)) = self.input.pop_front() + && let Some((announce, consensus_policy)) = self.input.pop_front() { - let maybe_promise_out_tx = - match utils::resolve_promise_policy(promise_policy, self.config.promises_mode()) { - PromisePolicy::Enabled => { - let (sender, receiver) = mpsc::unbounded_channel(); - self.promises_stream = Some(utils::AnnouncePromisesStream::new( - receiver, - announce.to_hash(), - )); - - Some(sender) - } - PromisePolicy::Disabled => None, - }; + let promise_policy = match self.config.promises_mode() { + PromiseEmissionMode::AlwaysEmit => PromisePolicy::Enabled, + PromiseEmissionMode::ConsensusDriven => consensus_policy, + }; + + let maybe_promise_out_tx = promise_policy.is_enabled().then(|| { + let (sender, receiver) = mpsc::unbounded_channel(); + self.promises_stream = Some(utils::AnnouncePromisesStream::new( + receiver, + announce.to_hash(), + )); + sender + }); self.computation = Some(future_timing::timed( Self::compute( @@ -281,18 +281,6 @@ pub(crate) mod utils { use futures::Stream; use std::pin::Pin; - /// Resolves [PromisePolicy] with consensus provided policy and global - /// [PromiseEmissionMode] set for node. - pub(super) fn resolve_promise_policy( - consensus_policy: PromisePolicy, - mode: PromiseEmissionMode, - ) -> PromisePolicy { - match mode { - PromiseEmissionMode::AlwaysEmit => PromisePolicy::Enabled, - PromiseEmissionMode::ConsensusDriven => consensus_policy, - } - } - /// The stream of promises from announce execution. pub(super) struct AnnouncePromisesStream { receiver: mpsc::UnboundedReceiver, diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index bd5c8ecd6db..aafee8b6efd 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -94,7 +94,7 @@ enum VerifyMessageError { Reject(VerifyMessageRejectReason), } -#[derive(Debug, derive_more::Display)] +#[derive(Debug, PartialEq, Eq, derive_more::Display)] enum VerifyPromiseError { #[display("unknown validator: address={address}, tx_hash={tx_hash}")] UnknownValidator { diff --git a/ethexe/rpc/src/apis/injected/promise_manager.rs b/ethexe/rpc/src/apis/injected/promise_manager.rs index 07e7c0c4aef..aef76d4e68b 100644 --- a/ethexe/rpc/src/apis/injected/promise_manager.rs +++ b/ethexe/rpc/src/apis/injected/promise_manager.rs @@ -21,19 +21,17 @@ use dashmap::{DashMap, mapref::entry::Entry}; use ethexe_common::{ HashOf, db::{InjectedStorageRO, InjectedStorageRW}, - injected::{ - InjectedTransaction, Promise, SignedCompactPromise, SignedPromise, restore_signed_promise, - }, + injected::{InjectedTransaction, Promise, SignedCompactPromise, SignedPromise}, }; use ethexe_db::Database; use std::sync::Arc; use tokio::sync::oneshot; -use tracing::trace; +use tracing::{trace, warn}; // TODO (kuzmindev): Currently, PromiseSubscriptionManager do not check, that transaction was // sent by validator, so there must be pre-validation for data received from network (SignedCompactPromise). -// TODO (kuzmindev): think about using `moka::sync::Cache` instead of DashMap +// TODO (kuzmindev): think about using `moka::sync::Cache` instead of DashMap. type PromiseSubscribers = Arc, oneshot::Sender>>; type PromisesComputationWaiting = Arc, SignedCompactPromise>>; @@ -116,16 +114,17 @@ impl PromiseSubscriptionManager { let tx_hash = compact.data().tx_hash; match self.db.promise(tx_hash) { - Some(promise) => match restore_signed_promise(promise, &compact) { + Some(promise) => match compact.restore(promise) { Ok(signed_promise) => { self.db.set_compact_promise(&compact); self.dispatch_promise(signed_promise); } - Err(_err) => { - trace!( - ?compact, %tx_hash, "failed to create signed promise from parts, producer send invalid signature: compact_promise={compact:?}" + Err(err) => { + warn!( + ?compact, %tx_hash, error=?err, "failed to create signed promise from parts, producer send invalid signature: compact_promise={compact:?}" ); + self.waiting_for_compute.insert(tx_hash, compact); } }, None => { @@ -140,7 +139,7 @@ impl PromiseSubscriptionManager { self.db.set_promise(&promise); if let Some((_, compact_promise)) = self.waiting_for_compute.remove(&promise.tx_hash) { - match restore_signed_promise(promise, &compact_promise) { + match compact_promise.restore(promise) { Ok(signed_promise) => { self.db.set_compact_promise(&compact_promise); self.dispatch_promise(signed_promise); diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index 71de85f7ced..d3a2ff06608 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -27,7 +27,7 @@ use ethexe_common::{ db::InjectedStorageRO, injected::{ AddressedInjectedTransaction, InjectedTransaction, InjectedTransactionAcceptance, - SignedInjectedTransaction, SignedPromise, restore_signed_promise, + SignedInjectedTransaction, SignedPromise, }, }; use ethexe_db::Database; @@ -167,7 +167,7 @@ impl InjectedApi { return Ok(None); }; - match restore_signed_promise(promise, &compact) { + match compact.restore(promise) { Ok(message) => Ok(Some(message)), Err(err) => { trace!( diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index 295061fc9dc..38c50678a70 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -227,8 +227,6 @@ impl RpcServerApis { pub fn into_methods(self) -> jsonrpsee::server::RpcModule<()> { let mut module = JsonrpcModule::new(()); - // let rpc = self.block.into_rpc(); - // let callbacks = rpc.method_names(); module .merge(BlockServer::into_rpc(self.block)) .expect("No conflicts"); From 85cfa8a85c867ca0575c2c8b73323710feb6be01 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 24 Apr 2026 15:03:02 +0300 Subject: [PATCH 52/59] fix: Promise emission for case when node is validator + RPC --- ethexe/compute/src/compute.rs | 83 +++++++++---------- ethexe/compute/src/lib.rs | 11 +-- ethexe/compute/src/tests.rs | 6 +- .../src/handling/run/chunk_execution_spawn.rs | 4 +- ethexe/processor/src/handling/run/mod.rs | 16 ++-- ethexe/processor/src/host/api/promise.rs | 2 +- ethexe/processor/src/host/mod.rs | 13 +-- ethexe/processor/src/host/threads.rs | 19 ++--- ethexe/processor/src/lib.rs | 14 ++-- ethexe/processor/src/promise.rs | 48 +++++++++++ ethexe/processor/src/tests.rs | 44 ++++++---- 11 files changed, 159 insertions(+), 101 deletions(-) create mode 100644 ethexe/processor/src/promise.rs diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index c8f42146267..b45f62d9902 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -27,7 +27,7 @@ use ethexe_common::{ injected::Promise, }; use ethexe_db::Database; -use ethexe_processor::ExecutableData; +use ethexe_processor::{BoundPromiseSink, ExecutableData}; use ethexe_runtime_common::FinalizedBlockTransitions; use futures::{FutureExt, StreamExt, future::BoxFuture}; use gprimitives::H256; @@ -50,6 +50,7 @@ struct Metrics { #[cfg_attr(test, derive(Default))] pub struct ComputeConfig { /// The delay in **blocks** in which events from Ethereum will be apply. + #[cfg_attr(test, builder(default))] canonical_quarantine: u8, /// The promises emission rule. promises_mode: PromiseEmissionMode, @@ -109,7 +110,7 @@ impl ComputeSubService

{ config: ComputeConfig, mut processor: P, announce: Announce, - promise_out_tx: Option>, + promise_sender: Option, Promise)>>, ) -> Result> { let announce_hash = announce.to_hash(); let block_hash = announce.block_hash; @@ -125,26 +126,31 @@ impl ComputeSubService

{ not_computed_announces.len(), ); - let promise_tx = match config.promises_mode() { + let promise_sender = match config.promises_mode() { // If AlwaysEmit promises mode - we pass promises tx also for not computed chain. - PromiseEmissionMode::AlwaysEmit => promise_out_tx.clone(), - // Set the promise_out_tx = None, because in this case we want to receive promises only from target announce. + PromiseEmissionMode::AlwaysEmit => promise_sender.clone(), + // Set the promise_sink = None, because in this case we want to receive promises only from target announce. PromiseEmissionMode::ConsensusDriven => None, }; for (announce_hash, announce) in not_computed_announces { + let promise_sink = promise_sender + .clone() + .map(|sender| BoundPromiseSink::new(sender, announce_hash)); Self::compute_one( &db, &mut processor, config, announce_hash, announce, - promise_tx.clone(), + promise_sink, ) .await?; } } + // TODO: maybe implement `switch_to_announce` for BoundPromiseSink + let promise_sink = promise_sender.map(|s| BoundPromiseSink::new(s, announce_hash)); // Compute the target announce Self::compute_one( &db, @@ -152,7 +158,7 @@ impl ComputeSubService

{ config, announce_hash, announce, - promise_out_tx, + promise_sink, ) .await } @@ -163,13 +169,11 @@ impl ComputeSubService

{ config: ComputeConfig, announce_hash: HashOf, announce: Announce, - promise_out_tx: Option>, + promise_sink: Option, ) -> Result> { let executable = utils::prepare_executable_for_announce(db, announce, config.canonical_quarantine())?; - let processing_result = processor - .process_programs(executable, promise_out_tx) - .await?; + let processing_result = processor.process_programs(executable, promise_sink).await?; let FinalizedBlockTransitions { transitions, @@ -212,12 +216,9 @@ impl SubService for ComputeSubService

{ PromiseEmissionMode::ConsensusDriven => consensus_policy, }; - let maybe_promise_out_tx = promise_policy.is_enabled().then(|| { + let maybe_promise_sender = promise_policy.is_enabled().then(|| { let (sender, receiver) = mpsc::unbounded_channel(); - self.promises_stream = Some(utils::AnnouncePromisesStream::new( - receiver, - announce.to_hash(), - )); + self.promises_stream = Some(utils::AnnouncePromisesStream::new(receiver)); sender }); @@ -227,7 +228,7 @@ impl SubService for ComputeSubService

{ self.config, self.processor.clone(), announce, - maybe_promise_out_tx, + maybe_promise_sender, ) .boxed(), )); @@ -283,19 +284,12 @@ pub(crate) mod utils { /// The stream of promises from announce execution. pub(super) struct AnnouncePromisesStream { - receiver: mpsc::UnboundedReceiver, - announce_hash: HashOf, + receiver: mpsc::UnboundedReceiver<(HashOf, Promise)>, } impl AnnouncePromisesStream { - pub fn new( - receiver: mpsc::UnboundedReceiver, - announce_hash: HashOf, - ) -> Self { - Self { - receiver, - announce_hash, - } + pub fn new(receiver: mpsc::UnboundedReceiver<(HashOf, Promise)>) -> Self { + Self { receiver } } } @@ -305,7 +299,7 @@ pub(crate) mod utils { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Poll::Ready( futures::ready!(self.receiver.poll_recv(cx)) - .map(|promise| ComputeEvent::Promise(promise, self.announce_hash)), + .map(|event| ComputeEvent::Promise(event.1, event.0)), ) } } @@ -618,12 +612,14 @@ mod tests { announce.gas_allowance = Some(DEFAULT_BLOCK_GAS_LIMIT); announce.parent = parent_announce; - let block = announce.block_hash; - let txs = if i != 1 { - vec![test_utils::injected_tx(ping_id, b"PING".into(), block)] - } else { - Default::default() - }; + let mut txs = Vec::new(); + if i != 1 { + txs.push(test_utils::injected_tx( + ping_id, + b"PING".into(), + announce.block_hash, + )); + } announce.injected_transactions = txs; announce @@ -645,8 +641,10 @@ mod tests { }) .collect::>(); - let mut compute_service = - ComputeService::new(ComputeConfig::default(), db.clone(), processor); + let config = ComputeConfig::builder() + .promises_mode(PromiseEmissionMode::ConsensusDriven) + .build(); + let mut compute_service = ComputeService::new(config, db.clone(), processor); // Send announces for computation. compute_service.compute_announce( @@ -668,23 +666,24 @@ mod tests { announces_chain.get(8).unwrap().to_hash(), ]; - let mut expected_promises = expected_announces + let mut expected_events = expected_announces .iter() .map(|hash| { let announce = db.announce(*hash).unwrap(); let tx = announce.injected_transactions[0].clone().into_data(); - Promise { + let promise = Promise { tx_hash: tx.to_hash(), reply: ReplyInfo { payload: b"PONG".into(), value: 0, code: ReplyCode::Success(SuccessReplyReason::Manual), }, - } + }; + (*hash, promise) }) .collect::>(); - while !expected_announces.is_empty() || !expected_promises.is_empty() { + while !expected_announces.is_empty() || !expected_events.is_empty() { match compute_service.next().await.unwrap().unwrap() { ComputeEvent::AnnounceComputed(hash) => { if *expected_announces.first().unwrap() == hash { @@ -693,9 +692,9 @@ mod tests { } ComputeEvent::Promise(promise, announce) => { if *expected_announces.first().unwrap() == announce - && expected_promises.first().unwrap().clone() == promise + && expected_events.first().unwrap().clone() == (announce, promise) { - expected_promises.remove(0); + expected_events.remove(0); } } _ => unreachable!("unexpected event for current test"), diff --git a/ethexe/compute/src/lib.rs b/ethexe/compute/src/lib.rs index b8f11a00ad9..acee44d5c71 100644 --- a/ethexe/compute/src/lib.rs +++ b/ethexe/compute/src/lib.rs @@ -151,12 +151,13 @@ pub use compute::{ utils::{find_canonical_events_post_quarantine, prepare_executable_for_announce}, }; use ethexe_common::{Announce, CodeAndIdUnchecked, HashOf, injected::Promise}; -use ethexe_processor::{ExecutableData, ProcessedCodeInfo, Processor, ProcessorError}; +use ethexe_processor::{ + BoundPromiseSink, ExecutableData, ProcessedCodeInfo, Processor, ProcessorError, +}; use ethexe_runtime_common::FinalizedBlockTransitions; use gprimitives::{CodeId, H256}; pub use service::ComputeService; use std::collections::HashSet; -use tokio::sync::mpsc; mod codes; mod compute; @@ -227,7 +228,7 @@ pub trait ProcessorExt: Sized + Unpin + Send + Clone + 'static { fn process_programs( &mut self, executable: ExecutableData, - promise_out_tx: Option>, + promise_sink: Option, ) -> impl Future> + Send; fn process_code( &mut self, @@ -239,9 +240,9 @@ impl ProcessorExt for Processor { async fn process_programs( &mut self, executable: ExecutableData, - promise_out_tx: Option>, + promise_sink: Option, ) -> Result { - self.process_programs(executable, promise_out_tx) + self.process_programs(executable, promise_sink) .await .map_err(Into::into) } diff --git a/ethexe/compute/src/tests.rs b/ethexe/compute/src/tests.rs index 26fae95d196..61783aa978a 100644 --- a/ethexe/compute/src/tests.rs +++ b/ethexe/compute/src/tests.rs @@ -27,14 +27,14 @@ use ethexe_common::{ mock::*, }; use ethexe_db::Database; -use ethexe_processor::ValidCodeInfo; +use ethexe_processor::{BoundPromiseSink, ValidCodeInfo}; use futures::StreamExt; use gear_core::{ code::{CodeMetadata, InstantiatedSectionSizes, InstrumentedCode}, ids::prelude::CodeIdExt, }; use std::time::Duration; -use tokio::{sync::mpsc, time::timeout}; +use tokio::time::timeout; // MockProcessor that implements ProcessorExt and always returns Ok with empty results #[derive(Clone, Default)] @@ -81,7 +81,7 @@ impl ProcessorExt for MockProcessor { async fn process_programs( &mut self, _executable: ExecutableData, - _promise_out_tx: Option>, + _promise_sink: Option, ) -> Result { Ok(self.process_programs_result.take().unwrap_or_default()) } diff --git a/ethexe/processor/src/handling/run/chunk_execution_spawn.rs b/ethexe/processor/src/handling/run/chunk_execution_spawn.rs index eaa3d671141..3360de22cb4 100644 --- a/ethexe/processor/src/handling/run/chunk_execution_spawn.rs +++ b/ethexe/processor/src/handling/run/chunk_execution_spawn.rs @@ -59,7 +59,7 @@ pub async fn spawn_chunk_execution( let (instrumented_code, code_metadata) = ctx.program_code(program_id)?; let mut executor = ctx.inner().instance_creator.instantiate()?; let db = ctx.inner().db.cas().clone_boxed(); - let promise_out_tx = ctx.inner().promise_out_tx.clone(); + let promise_sink = ctx.inner().promise_sink.clone(); Ok(thread_pool::spawn(move || { let (jn, new_state_hash, gas_spent) = executor.run( db, @@ -73,7 +73,7 @@ pub async fn spawn_chunk_execution( block_info, promise_policy, }, - promise_out_tx, + promise_sink, )?; Ok((program_id, new_state_hash, jn, gas_spent)) })) diff --git a/ethexe/processor/src/handling/run/mod.rs b/ethexe/processor/src/handling/run/mod.rs index d80c8300c75..50564fe9079 100644 --- a/ethexe/processor/src/handling/run/mod.rs +++ b/ethexe/processor/src/handling/run/mod.rs @@ -110,7 +110,7 @@ pub(super) mod chunks_splitting; pub(crate) use chunks_splitting::ActorStateHashWithQueueSize; -use crate::{ProcessorError, Result, host::InstanceCreator}; +use crate::{BoundPromiseSink, ProcessorError, Result, host::InstanceCreator}; use chunk_execution_processing::ChunkJournalsProcessingOutput; use chunks_splitting::ExecutionChunks; use core_processor::common::JournalNote; @@ -120,7 +120,6 @@ use ethexe_common::{ StateHashWithQueueSize, db::CodesStorageRO, gear::{CHUNK_PROCESSING_GAS_LIMIT, MessageType}, - injected::Promise, }; use ethexe_db::{CASDatabase, Database}; use ethexe_runtime_common::{ @@ -133,7 +132,6 @@ use gear_core::{ }; use gprimitives::{ActorId, CodeId, H256}; use itertools::Itertools; -use tokio::sync::mpsc; // Process chosen queue type in chunks pub(super) async fn run_for_queue_type( @@ -269,9 +267,9 @@ pub(super) trait RunContext { } /// [`PromisePolicy`] tells processor should it emit promises or not. - /// By default if [`RunContext::promise_out_tx`] returns [`Some`] this function will return [`PromisePolicy::Enabled`]. + /// By default if [`RunContext::promise_sink`] returns [`Some`] this function will return [`PromisePolicy::Enabled`]. fn promise_policy(&self) -> PromisePolicy { - match self.inner().promise_out_tx.is_some() { + match self.inner().promise_sink.is_some() { true => PromisePolicy::Enabled, false => PromisePolicy::Disabled, } @@ -349,7 +347,7 @@ pub(crate) struct CommonRunContext { out_of_gas: bool, chunk_size: usize, block_header: BlockHeader, - promise_out_tx: Option>, + promise_sink: Option, } impl CommonRunContext { @@ -360,7 +358,7 @@ impl CommonRunContext { gas_allowance: u64, chunk_size: usize, block_header: BlockHeader, - promise_out_tx: Option>, + promise_sink: Option, ) -> Self { CommonRunContext { db, @@ -373,12 +371,12 @@ impl CommonRunContext { out_of_gas: false, chunk_size, block_header, - promise_out_tx, + promise_sink, } } fn disable_promises(&mut self) { - if self.promise_out_tx.take().is_some() { + if self.promise_sink.take().is_some() { log::trace!("dropping the promise sender"); } } diff --git a/ethexe/processor/src/host/api/promise.rs b/ethexe/processor/src/host/api/promise.rs index 58578772a43..e5783d99de4 100644 --- a/ethexe/processor/src/host/api/promise.rs +++ b/ethexe/processor/src/host/api/promise.rs @@ -29,7 +29,7 @@ pub fn link(linker: &mut Linker) -> Result<(), wasmtime::Error> { fn publish_promise(caller: Caller<'_, StoreData>, promise_ptr_len: i64) { threads::with_params(|params| { - if let Some(ref sender) = params.promise_out_tx { + if let Some(ref sender) = params.promise_sink { let memory = MemoryWrap(caller.data().memory()); let promise = memory.decode_by_val(&caller, promise_ptr_len); diff --git a/ethexe/processor/src/host/mod.rs b/ethexe/processor/src/host/mod.rs index 153d712cdd2..20a162a0907 100644 --- a/ethexe/processor/src/host/mod.rs +++ b/ethexe/processor/src/host/mod.rs @@ -17,7 +17,7 @@ // along with this program. If not, see . use core_processor::common::JournalNote; -use ethexe_common::{gear::MessageType, injected::Promise}; +use ethexe_common::gear::MessageType; use ethexe_db::CASDatabase; use ethexe_runtime_common::{ProcessQueueContext, ProgramJournals, unpack_i64_to_u32}; use gear_core::code::{CodeMetadata, InstrumentedCode}; @@ -26,7 +26,8 @@ use parity_scale_codec::{Decode, Encode}; use sp_allocator::{AllocationStats, FreeingBumpHeapAllocator}; use sp_wasm_interface::{HostState, IntoValue, MemoryWrapper, StoreData}; use std::sync::Arc; -use tokio::sync::mpsc; + +use crate::BoundPromiseSink; pub mod api; pub mod runtime; @@ -171,13 +172,13 @@ impl InstanceWrapper { &mut self, db: Box, ctx: ProcessQueueContext, - promise_out_tx: Option>, + promise_sink: Option, ) -> Result<(ProgramJournals, H256, u64)> { - threads::set(db, ctx.state_root, promise_out_tx.clone()); + threads::set(db, ctx.state_root, promise_sink.clone()); - // Cleanup the `promise_out_tx` from thread-local to signal receiver that channel is closed. + // Cleanup the `promise_sink` from thread-local to signal receiver that channel is closed. let _cleanup = scopeguard::guard((), |()| { - threads::clear_promise_out_tx(); + threads::clear_promise_sink(); }); // Pieces of resulting journal. Hack to avoid single allocation limit. diff --git a/ethexe/processor/src/host/threads.rs b/ethexe/processor/src/host/threads.rs index fdf70e9f68e..c644d0aa3cd 100644 --- a/ethexe/processor/src/host/threads.rs +++ b/ethexe/processor/src/host/threads.rs @@ -19,7 +19,7 @@ // TODO: for each panic here place log::error, otherwise it won't be printed. use core::fmt; -use ethexe_common::{HashOf, injected::Promise}; +use ethexe_common::HashOf; use ethexe_db::CASDatabase; use ethexe_runtime_common::state::{ ActiveProgram, MemoryPages, MemoryPagesRegionInner, Program, ProgramState, QueryableStorage, @@ -30,7 +30,8 @@ use gear_lazy_pages::LazyPagesStorage; use gprimitives::H256; use parity_scale_codec::{Decode, DecodeAll}; use std::{cell::RefCell, collections::BTreeMap}; -use tokio::sync::mpsc; + +use crate::BoundPromiseSink; const UNSET_PANIC: &str = "params should be set before query"; const UNKNOWN_STATE: &str = "state should always be valid (must exist)"; @@ -42,7 +43,7 @@ thread_local! { pub struct ThreadParams { pub db: Box, pub state_hash: H256, - pub promise_out_tx: Option>, + pub promise_sink: Option, pages_registry_cache: Option, pages_regions_cache: Option>, } @@ -104,15 +105,11 @@ impl PageKey { } } -pub fn set( - db: Box, - state_hash: H256, - promise_out_tx: Option>, -) { +pub fn set(db: Box, state_hash: H256, promise_sink: Option) { PARAMS.set(Some(ThreadParams { db, state_hash, - promise_out_tx, + promise_sink, pages_registry_cache: None, pages_regions_cache: None, })) @@ -144,10 +141,10 @@ pub fn with_params(f: impl FnOnce(&mut ThreadParams) -> T) -> T { }) } -pub fn clear_promise_out_tx() { +pub fn clear_promise_sink() { PARAMS.with_borrow_mut(|maybe_params| { let params = maybe_params.as_mut().expect(UNSET_PANIC); - let _ = params.promise_out_tx.take(); + let _ = params.promise_sink.take(); }) } diff --git a/ethexe/processor/src/lib.rs b/ethexe/processor/src/lib.rs index b0d5b2a2206..59fa273f7bb 100644 --- a/ethexe/processor/src/lib.rs +++ b/ethexe/processor/src/lib.rs @@ -159,7 +159,7 @@ use ethexe_common::{ CodeAndIdUnchecked, ProgramStates, Schedule, SimpleBlockData, ecdsa::VerifiedData, events::{BlockRequestEvent, MirrorRequestEvent, mirror::MessageQueueingRequestedEvent}, - injected::{InjectedTransaction, Promise}, + injected::InjectedTransaction, }; use ethexe_db::Database; use ethexe_runtime_common::{ @@ -174,10 +174,12 @@ use gear_core::{ use gprimitives::{ActorId, CodeId, H256, MessageId}; use handling::{ProcessingHandler, overlaid::OverlaidRunContext, run::CommonRunContext}; use host::InstanceCreator; -use tokio::sync::mpsc; mod handling; mod host; +mod promise; +pub use promise::BoundPromiseSink; + #[cfg(test)] mod tests; mod thread_pool; @@ -312,7 +314,7 @@ impl Processor { pub async fn process_programs( &mut self, executable: ExecutableData, - promise_out_tx: Option>, + promise_sink: Option, ) -> Result { log::debug!("{executable}"); @@ -338,7 +340,7 @@ impl Processor { // Third step: process queues until limits are exhausted or all queues are empty. if let Some(gas_allowance) = gas_allowance { transitions = self - .process_queues(transitions, block, gas_allowance, promise_out_tx) + .process_queues(transitions, block, gas_allowance, promise_sink) .await?; } @@ -378,7 +380,7 @@ impl Processor { transitions: InBlockTransitions, block: SimpleBlockData, gas_allowance: u64, - promise_out_tx: Option>, + promise_sink: Option, ) -> Result { CommonRunContext::new( self.db.clone(), @@ -387,7 +389,7 @@ impl Processor { gas_allowance, self.config.chunk_size, block.header, - promise_out_tx, + promise_sink, ) .run() .await diff --git a/ethexe/processor/src/promise.rs b/ethexe/processor/src/promise.rs new file mode 100644 index 00000000000..edaac551543 --- /dev/null +++ b/ethexe/processor/src/promise.rs @@ -0,0 +1,48 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use ethexe_common::{Announce, HashOf, injected::Promise}; +use tokio::sync::mpsc::{UnboundedSender, error::SendError}; + +type SinkEvent = (HashOf, Promise); + +/// Wrapper on top of [tokio::sync::mpsc::UnboundedSender]. +/// [BoundPromiseSink] is responsible for sending the promises with +/// announce hash it belongs to. +#[derive(Clone)] +pub struct BoundPromiseSink { + sender: UnboundedSender, + announce_hash: HashOf, +} + +impl BoundPromiseSink { + /// Creates new instance of [BoundPromiseSink]. + pub fn new(sender: UnboundedSender, announce_hash: HashOf) -> Self { + Self { + sender, + announce_hash, + } + } + + /// Sends [Promise] to outer service. + /// Internally wraps result into `(HashOf, Promise)`. + pub fn send(&self, promise: Promise) -> Result<(), SendError> { + let event = (self.announce_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 8f664618ab5..4dece7884d8 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -19,8 +19,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, SimpleBlockData, + DEFAULT_BLOCK_GAS_LIMIT, HashOf, OUTGOING_MESSAGES_SOFT_LIMIT, + PROGRAM_MODIFICATIONS_SOFT_LIMIT, PrivateKey, ScheduledTask, SignedMessage, SimpleBlockData, db::*, events::{ BlockRequestEvent, MirrorRequestEvent, RouterRequestEvent, @@ -923,7 +923,9 @@ async fn overlay_execution() { async fn injected_ping_pong() { init_logger(); - let (promise_out_tx, mut promise_receiver) = mpsc::unbounded_channel(); + // TODO: !!! Make easier to work with `promise_receiver`. + let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); + let promise_sink = BoundPromiseSink::new(promise_sender, HashOf::random()); let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY]).await; let block1 = chain.blocks[1].to_simple(); @@ -993,7 +995,7 @@ async fn injected_ping_pong() { handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, - Some(promise_out_tx.clone()), + Some(promise_sink.clone()), ) .await .unwrap(); @@ -1001,7 +1003,8 @@ async fn injected_ping_pong() { let promise = promise_receiver .recv() .await - .expect("promise must be sent after processing"); + .expect("promise must be sent after processing") + .1; assert_eq!(promise.tx_hash, injected_tx.to_hash()); assert_eq!(promise.reply.payload, b"PONG"); @@ -1034,7 +1037,9 @@ async fn injected_prioritized_over_canonical() { init_logger(); - let (promise_out_tx, mut promise_receiver) = mpsc::unbounded_channel(); + let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); + let promise_sink = BoundPromiseSink::new(promise_sender, HashOf::random()); + let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_ping::WASM_BINARY]).await; let block1 = chain.blocks[1].to_simple(); @@ -1112,7 +1117,7 @@ async fn injected_prioritized_over_canonical() { handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, - Some(promise_out_tx.clone()), + Some(promise_sink.clone()), ) .await .unwrap(); @@ -1121,7 +1126,8 @@ async fn injected_prioritized_over_canonical() { let promise = promise_receiver .recv() .await - .expect("promise for injected transaction"); + .expect("promise for injected transaction") + .1; assert_eq!(promise.tx_hash, tx_hash); assert_eq!(promise.reply.value, 0); @@ -1234,7 +1240,9 @@ async fn executable_balance_injected_panic_not_charged() { init_logger(); - let (promise_out_tx, mut promise_receiver) = mpsc::unbounded_channel(); + let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); + let promise_sink = BoundPromiseSink::new(promise_sender, HashOf::random()); + let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([demo_panic_payload::WASM_BINARY]).await; let block1 = chain.blocks[1].to_simple(); @@ -1283,7 +1291,7 @@ async fn executable_balance_injected_panic_not_charged() { handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, - Some(promise_out_tx.clone()), + Some(promise_sink.clone()), ) .await .unwrap(); @@ -1300,7 +1308,7 @@ async fn executable_balance_injected_panic_not_charged() { handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, - Some(promise_out_tx.clone()), + Some(promise_sink.clone()), ) .await .unwrap(); @@ -1309,7 +1317,9 @@ async fn executable_balance_injected_panic_not_charged() { let panic_promise = promise_receiver .recv() .await - .expect("promise for injected transaction"); + .expect("promise for injected transaction") + .1; + assert_eq!(panic_promise.tx_hash, panic_tx.to_hash()); assert_eq!(panic_promise.reply.value, 0); assert_eq!( @@ -1347,7 +1357,7 @@ async fn executable_balance_injected_panic_not_charged() { handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, - Some(promise_out_tx.clone()), + Some(promise_sink.clone()), ) .await .unwrap(); @@ -1654,7 +1664,8 @@ async fn injected_and_events_then_tasks_then_queues() { }), }]; - let (promise_out_tx, mut promise_receiver) = mpsc::unbounded_channel(); + let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); + let promise_sink = BoundPromiseSink::new(promise_sender, HashOf::random()); let executable = ExecutableData { block: block3, @@ -1665,7 +1676,7 @@ async fn injected_and_events_then_tasks_then_queues() { gas_allowance: Some(DEFAULT_BLOCK_GAS_LIMIT), }; let FinalizedBlockTransitions { transitions, .. } = processor - .process_programs(executable, Some(promise_out_tx)) + .process_programs(executable, Some(promise_sink)) .await .unwrap(); @@ -1701,7 +1712,8 @@ async fn injected_and_events_then_tasks_then_queues() { let promise = promise_receiver .recv() .await - .expect("promise must be sent for injected transaction"); + .expect("promise must be sent for injected transaction") + .1; assert_eq!(promise.reply.payload, b"DONE"); assert_eq!( promise.reply.code, From d9ee03559bbfaabaed851d8e43ce6ac49bf78e2b Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 28 Apr 2026 16:45:23 +0300 Subject: [PATCH 53/59] chore: remove useless TODO | add issues to track --- ethexe/compute/src/compute.rs | 1 - ethexe/processor/src/tests.rs | 1 - ethexe/rpc/src/apis/injected/promise_manager.rs | 10 +++++----- ethexe/rpc/src/apis/injected/relay.rs | 1 - ethexe/rpc/src/apis/injected/server.rs | 3 ++- ethexe/rpc/src/apis/injected/spawner.rs | 1 - 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index b45f62d9902..a2abe89e398 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -149,7 +149,6 @@ impl ComputeSubService

{ } } - // TODO: maybe implement `switch_to_announce` for BoundPromiseSink let promise_sink = promise_sender.map(|s| BoundPromiseSink::new(s, announce_hash)); // Compute the target announce Self::compute_one( diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index 4dece7884d8..d8ecf631cda 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -923,7 +923,6 @@ async fn overlay_execution() { async fn injected_ping_pong() { init_logger(); - // TODO: !!! Make easier to work with `promise_receiver`. let (promise_sender, mut promise_receiver) = mpsc::unbounded_channel(); let promise_sink = BoundPromiseSink::new(promise_sender, HashOf::random()); let (mut processor, chain, [code_id]) = diff --git a/ethexe/rpc/src/apis/injected/promise_manager.rs b/ethexe/rpc/src/apis/injected/promise_manager.rs index aef76d4e68b..fd9cd283487 100644 --- a/ethexe/rpc/src/apis/injected/promise_manager.rs +++ b/ethexe/rpc/src/apis/injected/promise_manager.rs @@ -28,10 +28,7 @@ use std::sync::Arc; use tokio::sync::oneshot; use tracing::{trace, warn}; -// TODO (kuzmindev): Currently, PromiseSubscriptionManager do not check, that transaction was -// sent by validator, so there must be pre-validation for data received from network (SignedCompactPromise). - -// TODO (kuzmindev): think about using `moka::sync::Cache` instead of DashMap. +// TODO: Issues #5384 and #5385. type PromiseSubscribers = Arc, oneshot::Sender>>; type PromisesComputationWaiting = Arc, SignedCompactPromise>>; @@ -168,9 +165,12 @@ impl PromiseSubscriptionManager { mod utils { use ethexe_common::db::ConfigStorageRO; + /// The maximum number of slots RPC will wait for transaction promise. + const MAX_PROMISE_WAITING_SLOTS: u64 = 20; + /// Returns the maximum time that spawned [super::PendingSubscriber] will wait for promise. pub fn promise_waiting_timeout(db: &DB) -> std::time::Duration { let slot_duration_secs = db.config().timelines.slot.get(); - std::time::Duration::from_secs(slot_duration_secs * 20) + std::time::Duration::from_secs(slot_duration_secs * MAX_PROMISE_WAITING_SLOTS) } } diff --git a/ethexe/rpc/src/apis/injected/relay.rs b/ethexe/rpc/src/apis/injected/relay.rs index 73c111736f9..4469d334ab3 100644 --- a/ethexe/rpc/src/apis/injected/relay.rs +++ b/ethexe/rpc/src/apis/injected/relay.rs @@ -44,7 +44,6 @@ impl TransactionsRelayer { let tx_hash = transaction.tx.data().to_hash(); trace!(%tx_hash, ?transaction, "Called injected_sendTransaction with vars"); - // TODO: maybe should implement the transaction validator. if transaction.tx.data().value != 0 { warn!( tx_hash = %tx_hash, diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index d3a2ff06608..ee27cb0121f 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -49,7 +49,7 @@ pub struct InjectedApi { metrics: InjectedApiMetrics, } -// TODO: add metrics middleware for InjectedApi +// TODO: Issue #5387 #[async_trait] impl InjectedServer for InjectedApi { async fn send_transaction( @@ -112,6 +112,7 @@ impl InjectedApi { self.relayer.relay(transaction).await } + // TODO: Issue #5386. async fn send_transaction_and_watch( &self, pending: PendingSubscriptionSink, diff --git a/ethexe/rpc/src/apis/injected/spawner.rs b/ethexe/rpc/src/apis/injected/spawner.rs index 73f3702d814..c0d2026647c 100644 --- a/ethexe/rpc/src/apis/injected/spawner.rs +++ b/ethexe/rpc/src/apis/injected/spawner.rs @@ -33,7 +33,6 @@ pub fn spawn_pending_subscriber( { let (tx_hash, receiver) = subscriber.into_parts(); - // TODO: think about using this handle for aborting runtime tasks in case of long waiting. let _handle = tokio::spawn(async move { let _guard = scopeguard::guard(tx_hash, on_finish); From 5c602ff6e791220e292204ba929ba157ce611699 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 29 Apr 2026 12:00:37 +0300 Subject: [PATCH 54/59] chore: small refactoring for TRACKED_METHODS --- ethexe/rpc/src/metrics.rs | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/ethexe/rpc/src/metrics.rs b/ethexe/rpc/src/metrics.rs index 84527f6963a..bb5f7e3b506 100644 --- a/ethexe/rpc/src/metrics.rs +++ b/ethexe/rpc/src/metrics.rs @@ -22,7 +22,7 @@ pub use metrics::*; pub use middleware::RpcMetricsLayer; mod middleware { - use super::metrics::{DEFAULT_TRACKED_METHODS, MethodMetrics}; + use super::metrics::MethodMetrics; use futures::future::BoxFuture; use jsonrpsee::{ server::{MethodResponse, middleware::rpc::RpcServiceT}, @@ -31,6 +31,13 @@ mod middleware { use std::{collections::HashMap, sync::Arc, time::Instant}; use tower::Layer; + /// Default methods names tracked by [super::RpcMetricsLayer]. + pub const TRACKED_METHODS: &[&str] = &[ + "injected_sendTransaction", + "injected_sendTransactionAndWatch", + "program_calculateReplyForHandle", + ]; + /// A methods metrics registry for [RpcMetricsLayer]. /// Internally it uses the mapping `method_name` => [MethodMetrics], so the /// access to metrics is fast and do not add extra request latency. @@ -41,7 +48,8 @@ mod middleware { impl RpcMetricsRegistry { pub fn new(methods: &'static [&'static str]) -> Self { - let mut methods_map = HashMap::new(); + let mut methods_map = HashMap::with_capacity(methods.len()); + methods.iter().copied().for_each(|method_name| { let method_metrics = MethodMetrics::new_with_labels(&[("method", method_name)]); methods_map.insert(method_name, method_metrics); @@ -59,14 +67,14 @@ mod middleware { impl Default for RpcMetricsRegistry { fn default() -> Self { - Self::new(DEFAULT_TRACKED_METHODS) + Self::new(TRACKED_METHODS) } } /// Metrics layer for [jsonrpsee::server::RpcServiceBuilder]. /// Uses [RpcMetricsService] to wrap each request to metrics collection logic. /// - /// Note: [Self::default] creates itself from registry with [DEFAULT_TRACKED_METHODS]. + /// Note: [Self::default] creates itself from registry with [TRACKED_METHODS]. #[derive(Clone, Default)] pub struct RpcMetricsLayer { registry: RpcMetricsRegistry, @@ -97,14 +105,12 @@ mod middleware { type Future = BoxFuture<'a, MethodResponse>; fn call(&self, request: Request<'a>) -> Self::Future { - let metrics = self.registry.get(request.method_name()).cloned(); - let future = self.service.call(request); + let Some(metrics) = self.registry.get(request.method_name()).cloned() else { + return Box::pin(self.service.call(request)); + }; + let future = self.service.call(request); Box::pin(async move { - let Some(metrics) = metrics else { - return future.await; - }; - metrics.on_incoming_request(); let started_at = Instant::now(); @@ -123,13 +129,6 @@ mod metrics { use metrics::{Counter, Gauge, Histogram}; use std::time::Instant; - /// Default methods names tracked by [super::RpcMetricsLayer]. - pub const DEFAULT_TRACKED_METHODS: &[&str] = &[ - "injected_sendTransaction", - "injected_sendTransactionAndWatch", - "program_calculateReplyForHandle", - ]; - /// Unified bundle of metrics for RPC method. /// [metrics_derive::Metrics] macro will register all metrics under the `ethexe_rpc_*` scope. /// From e4bf652c753f22a31668ef5c607a18511270e071 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 4 May 2026 13:58:13 +0300 Subject: [PATCH 55/59] fix: part of gsobol review --- ethexe/common/src/primitives.rs | 11 -- ethexe/compute/src/compute.rs | 199 ++++++++++++++++++++-------- ethexe/compute/src/lib.rs | 6 +- ethexe/consensus/src/connect/mod.rs | 6 +- 4 files changed, 154 insertions(+), 68 deletions(-) diff --git a/ethexe/common/src/primitives.rs b/ethexe/common/src/primitives.rs index a67e0f9ba7a..53174c09f8f 100644 --- a/ethexe/common/src/primitives.rs +++ b/ethexe/common/src/primitives.rs @@ -262,17 +262,6 @@ pub struct ProtocolTimelines { pub slot: NonZeroU64, } -impl Default for ProtocolTimelines { - fn default() -> Self { - Self { - genesis_ts: 0, - era: NonZeroU64::new(10_000).unwrap(), - election: 200, - slot: NonZeroU64::new(2).unwrap(), - } - } -} - impl ProtocolTimelines { /// Returns the era index for the given timestamp. Eras starts from 0. /// diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index a2abe89e398..16e959cd227 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -574,7 +574,7 @@ mod tests { #[tokio::test] #[ntest::timeout(60000)] - async fn test_compute_with_promises() { + async fn compute_promises_consensus_driven() { gear_utils::init_default_logger(); const BLOCKCHAIN_LEN: usize = 10; @@ -604,7 +604,7 @@ mod tests { // Setup announces and events. let mut parent_announce = start_announce_hash; - let announces_chain = (1..BLOCKCHAIN_LEN) + let chain = (1..BLOCKCHAIN_LEN) .map(|i| { let announce = { let mut announce = blockchain.block_top_announce(i).announce.clone(); @@ -640,65 +640,160 @@ mod tests { }) .collect::>(); - let config = ComputeConfig::builder() - .promises_mode(PromiseEmissionMode::ConsensusDriven) - .build(); - let mut compute_service = ComputeService::new(config, db.clone(), processor); + let mut compute_service = + ComputeService::new(ComputeConfig::default(), db.clone(), processor); + + let expected_announces = [ + chain.get(2).unwrap().clone(), + chain.get(5).unwrap().clone(), + chain.get(8).unwrap().clone(), + ]; // Send announces for computation. - compute_service.compute_announce( - announces_chain.get(2).unwrap().clone(), - PromisePolicy::Enabled, - ); - compute_service.compute_announce( - announces_chain.get(5).unwrap().clone(), - PromisePolicy::Enabled, - ); - compute_service.compute_announce( - announces_chain.get(8).unwrap().clone(), - PromisePolicy::Enabled, + expected_announces.iter().for_each(|announce| { + compute_service.compute_announce(announce.clone(), PromisePolicy::Enabled); + }); + + let expected_events = expected_announces.iter().map(|announce| { + let tx = announce.injected_transactions[0].clone().into_data(); + let promise = Promise { + tx_hash: tx.to_hash(), + reply: ReplyInfo { + payload: b"PONG".into(), + value: 0, + code: ReplyCode::Success(SuccessReplyReason::Manual), + }, + }; + ComputeEvent::Promise(promise, announce.to_hash()) + }); + + let events = compute_service + .take_while(|event| { + let last_announce = chain.last().unwrap().to_hash(); + let stop = matches!( + event, + Ok(ComputeEvent::AnnounceComputed(announce)) if *announce == last_announce + ); + std::future::ready(event.is_ok() && !stop) + }) + .filter_map(|e| { + let event = e.expect("infallible"); + std::future::ready(event.is_promise().then_some(event)) + }); + + assert_eq!( + expected_events.collect::>(), + events.collect::>().await ); + } - let mut expected_announces = vec![ - announces_chain.get(2).unwrap().to_hash(), - announces_chain.get(5).unwrap().to_hash(), - announces_chain.get(8).unwrap().to_hash(), - ]; + #[tokio::test] + async fn compute_promises_always_emit() { + gear_utils::init_default_logger(); + const BLOCKCHAIN_LEN: usize = 5; - let mut expected_events = expected_announces - .iter() - .map(|hash| { - let announce = db.announce(*hash).unwrap(); - let tx = announce.injected_transactions[0].clone().into_data(); - let promise = Promise { - tx_hash: tx.to_hash(), - reply: ReplyInfo { - payload: b"PONG".into(), - value: 0, - code: ReplyCode::Success(SuccessReplyReason::Manual), - }, + let db = Database::memory(); + let mut processor = Processor::new(db.clone()).unwrap(); + let ping_code_id = + test_utils::upload_code(&mut processor, demo_ping::WASM_BINARY, &db).await; + let ping_id = ActorId::from(0x10000); + + let blockchain = BlockChain::mock(BLOCKCHAIN_LEN as u32).setup(&db); + + // Setup first announce. + let start_announce_hash = { + let mut announce = blockchain.block_top_announce(0).announce.clone(); + announce.gas_allowance = Some(DEFAULT_BLOCK_GAS_LIMIT); + + let announce_hash = db.set_announce(announce); + db.mutate_announce_meta(announce_hash, |meta| meta.computed = true); + db.globals_mutate(|globals| { + globals.start_announce_hash = announce_hash; + }); + db.set_announce_program_states(announce_hash, Default::default()); + db.set_announce_schedule(announce_hash, Default::default()); + + announce_hash + }; + + // Setup announces and events. + let mut parent_announce = start_announce_hash; + let chain = (1..BLOCKCHAIN_LEN) + .map(|i| { + let announce = { + let mut announce = blockchain.block_top_announce(i).announce.clone(); + announce.gas_allowance = Some(DEFAULT_BLOCK_GAS_LIMIT); + announce.parent = parent_announce; + + let mut txs = Vec::new(); + if i != 1 { + txs.push(test_utils::injected_tx( + ping_id, + b"PING".into(), + announce.block_hash, + )); + } + + announce.injected_transactions = txs; + announce + }; + + let announce_hash = db.set_announce(announce.clone()); + db.mutate_announce_meta(announce_hash, |meta| meta.computed = false); + + let mut block_events = if i == 1 { + test_utils::create_program_events(ping_id, ping_code_id) + } else { + Default::default() }; - (*hash, promise) + block_events.extend(test_utils::block_events(5, ping_id, b"PING".into())); + db.set_block_events(announce.block_hash, &block_events); + + parent_announce = announce_hash; + announce }) .collect::>(); - while !expected_announces.is_empty() || !expected_events.is_empty() { - match compute_service.next().await.unwrap().unwrap() { - ComputeEvent::AnnounceComputed(hash) => { - if *expected_announces.first().unwrap() == hash { - expected_announces.remove(0); - } - } - ComputeEvent::Promise(promise, announce) => { - if *expected_announces.first().unwrap() == announce - && expected_events.first().unwrap().clone() == (announce, promise) - { - expected_events.remove(0); - } - } - _ => unreachable!("unexpected event for current test"), - } - } + let config = ComputeConfig::builder() + .promises_mode(PromiseEmissionMode::AlwaysEmit) + .build(); + let mut compute_service = ComputeService::new(config, db.clone(), processor); + + // Send announces for computation. + compute_service.compute_announce(chain.first().unwrap().clone(), PromisePolicy::Disabled); + compute_service.compute_announce(chain.get(3).unwrap().clone(), PromisePolicy::Enabled); + + let expected_events = chain[1..].iter().map(|announce| { + let tx = announce.injected_transactions.first().unwrap(); + let promise = Promise { + tx_hash: tx.data().to_hash(), + reply: ReplyInfo { + payload: b"PONG".into(), + value: 0, + code: ReplyCode::Success(SuccessReplyReason::Manual), + }, + }; + ComputeEvent::Promise(promise, announce.to_hash()) + }); + + let events = compute_service + .take_while(|event| { + let last_announce = chain.last().unwrap().to_hash(); + let stop = matches!( + event, + Ok(ComputeEvent::AnnounceComputed(announce)) if *announce == last_announce + ); + std::future::ready(event.is_ok() && !stop) + }) + .filter_map(|e| { + let event = e.expect("infallible"); + std::future::ready(event.is_promise().then_some(event)) + }); + + assert_eq!( + expected_events.collect::>(), + events.collect::>().await + ); } #[tokio::test] diff --git a/ethexe/compute/src/lib.rs b/ethexe/compute/src/lib.rs index acee44d5c71..3ad066680e5 100644 --- a/ethexe/compute/src/lib.rs +++ b/ethexe/compute/src/lib.rs @@ -103,9 +103,8 @@ //! Computation is sequential: at most one announce is executed at a time. //! If the announce's parent (or any further ancestor) has not been //! computed yet, missing ancestors are computed first, in order. -//! Ancestors are always computed without promise collection regardless of -//! the requested policy — promises describe the user-visible result of -//! the target announce only. +//! Promises for ancestors are computed according to [PromiseEmissionMode](ethexe_common::PromiseEmissionMode) +//! in [ComputeConfig]. //! //! The target block must already be prepared; otherwise the computation //! fails with [`ComputeError::BlockNotPrepared`]. @@ -173,6 +172,7 @@ pub struct BlockProcessed { } #[derive(Debug, Clone, Eq, PartialEq, derive_more::Unwrap, derive_more::From)] +#[cfg_attr(test, derive(derive_more::IsVariant))] pub enum ComputeEvent { RequestLoadCodes(HashSet), CodeProcessed(CodeId), diff --git a/ethexe/consensus/src/connect/mod.rs b/ethexe/consensus/src/connect/mod.rs index 533e14f8b06..80b6d7aaf63 100644 --- a/ethexe/consensus/src/connect/mod.rs +++ b/ethexe/consensus/src/connect/mod.rs @@ -43,6 +43,7 @@ use std::{ pin::Pin, task::{Context, Poll}, }; +use tracing::trace; /// Maximum number of pending announces to store const MAX_PENDING_ANNOUNCES: NonZeroUsize = NonZeroUsize::new(10).unwrap(); @@ -289,12 +290,13 @@ impl ConsensusService for ConnectService { fn receive_promise_for_signing( &mut self, - _promise: Promise, - _announce_hash: HashOf, + promise: Promise, + announce_hash: HashOf, ) -> Result<()> { // Nothing to do. // This case is not error because connect node can be also RPC node that produce promises, // to send them for external users. + trace!(?promise, %announce_hash, "connect node received the promise for signing, skipping..."); Ok(()) } From 285b7294c3a18befb6ec2aec814777864b77ec95 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 4 May 2026 15:14:39 +0300 Subject: [PATCH 56/59] chore: add todos for issues --- ethexe/rpc/src/apis/injected/promise_manager.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ethexe/rpc/src/apis/injected/promise_manager.rs b/ethexe/rpc/src/apis/injected/promise_manager.rs index fd9cd283487..31355e0bc41 100644 --- a/ethexe/rpc/src/apis/injected/promise_manager.rs +++ b/ethexe/rpc/src/apis/injected/promise_manager.rs @@ -85,6 +85,7 @@ impl PromiseSubscriptionManager { } } + // TODO: Issue #5402 pub fn try_register_subscriber( &self, tx_hash: HashOf, @@ -106,6 +107,7 @@ impl PromiseSubscriptionManager { self.subscribers.remove(&tx_hash).map(|(_, v)| v) } + // TODO: Issue #5403 pub fn on_compact_promise(&self, compact: SignedCompactPromise) { trace!(?compact, "received new compact promise"); let tx_hash = compact.data().tx_hash; From cb03241b918238587826bdaf7833948c57768673 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 4 May 2026 18:37:16 +0300 Subject: [PATCH 57/59] chore: move registry to lazy lock static --- ethexe/rpc/src/lib.rs | 2 +- ethexe/rpc/src/metrics.rs | 60 +++++++++++---------------------------- 2 files changed, 17 insertions(+), 45 deletions(-) diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index b6eb0254df4..60375278857 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -131,7 +131,7 @@ impl RpcServer { let cors_layer = self.cors_layer()?; let http_middleware = tower::ServiceBuilder::new().layer(cors_layer); // Setup the default RPC metrics layer. - let rpc_middleware = RpcServiceBuilder::new().layer(RpcMetricsLayer::default()); + let rpc_middleware = RpcServiceBuilder::new().layer(RpcMetricsLayer); let processor = Processor::with_config( ProcessorConfig { diff --git a/ethexe/rpc/src/metrics.rs b/ethexe/rpc/src/metrics.rs index bb5f7e3b506..7d1d313cc3a 100644 --- a/ethexe/rpc/src/metrics.rs +++ b/ethexe/rpc/src/metrics.rs @@ -28,7 +28,7 @@ mod middleware { server::{MethodResponse, middleware::rpc::RpcServiceT}, types::Request, }; - use std::{collections::HashMap, sync::Arc, time::Instant}; + use std::{collections::HashMap, sync::LazyLock, time::Instant}; use tower::Layer; /// Default methods names tracked by [super::RpcMetricsLayer]. @@ -38,63 +38,35 @@ mod middleware { "program_calculateReplyForHandle", ]; - /// A methods metrics registry for [RpcMetricsLayer]. - /// Internally it uses the mapping `method_name` => [MethodMetrics], so the - /// access to metrics is fast and do not add extra request latency. - #[derive(Clone)] - pub struct RpcMetricsRegistry { - methods_map: Arc>, - } - - impl RpcMetricsRegistry { - pub fn new(methods: &'static [&'static str]) -> Self { - let mut methods_map = HashMap::with_capacity(methods.len()); - - methods.iter().copied().for_each(|method_name| { - let method_metrics = MethodMetrics::new_with_labels(&[("method", method_name)]); - methods_map.insert(method_name, method_metrics); - }); - - Self { - methods_map: Arc::new(methods_map), - } - } - - pub fn get(&self, method: &str) -> Option<&MethodMetrics> { - self.methods_map.get(method) - } - } - - impl Default for RpcMetricsRegistry { - fn default() -> Self { - Self::new(TRACKED_METHODS) - } - } + static METHODS_MAP: LazyLock> = LazyLock::new(|| { + TRACKED_METHODS + .iter() + .copied() + .map(|method_name| { + ( + method_name, + MethodMetrics::new_with_labels(&[("method", method_name)]), + ) + }) + .collect() + }); /// Metrics layer for [jsonrpsee::server::RpcServiceBuilder]. /// Uses [RpcMetricsService] to wrap each request to metrics collection logic. - /// - /// Note: [Self::default] creates itself from registry with [TRACKED_METHODS]. #[derive(Clone, Default)] - pub struct RpcMetricsLayer { - registry: RpcMetricsRegistry, - } + pub struct RpcMetricsLayer; impl Layer for RpcMetricsLayer { type Service = RpcMetricsService; fn layer(&self, service: S) -> Self::Service { - RpcMetricsService { - service, - registry: self.registry.clone(), - } + RpcMetricsService { service } } } #[derive(Clone)] pub struct RpcMetricsService { service: S, - registry: RpcMetricsRegistry, } impl<'a, S> RpcServiceT<'a> for RpcMetricsService @@ -105,7 +77,7 @@ mod middleware { type Future = BoxFuture<'a, MethodResponse>; fn call(&self, request: Request<'a>) -> Self::Future { - let Some(metrics) = self.registry.get(request.method_name()).cloned() else { + let Some(metrics) = METHODS_MAP.get(request.method_name()) else { return Box::pin(self.service.call(request)); }; From acc7d488bd211ace3867fd06365e722272c1ffcf Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 4 May 2026 19:24:12 +0300 Subject: [PATCH 58/59] fix: claude review --- ethexe/rpc/src/metrics.rs | 47 ++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/ethexe/rpc/src/metrics.rs b/ethexe/rpc/src/metrics.rs index 7d1d313cc3a..9803e62dd02 100644 --- a/ethexe/rpc/src/metrics.rs +++ b/ethexe/rpc/src/metrics.rs @@ -83,11 +83,23 @@ mod middleware { let future = self.service.call(request); Box::pin(async move { - metrics.on_incoming_request(); + metrics.calls_started.increment(1); + metrics.calls_in_flight.increment(1); + let _metrics_guard = + scopeguard::guard((), |_| metrics.calls_in_flight.decrement(1)); + let started_at = Instant::now(); let response = future.await; - metrics.on_outgoing_response(started_at, &response); + + metrics + .calls_latency_seconds + .record(started_at.elapsed().as_secs_f64()); + match response.is_success() { + true => metrics.calls_finished_ok.increment(1), + false => metrics.calls_finished_err.increment(1), + } + response }) } @@ -97,9 +109,7 @@ mod middleware { /// Metrics type definitions. #[allow(clippy::module_inception)] mod metrics { - use jsonrpsee::server::MethodResponse; use metrics::{Counter, Gauge, Histogram}; - use std::time::Instant; /// Unified bundle of metrics for RPC method. /// [metrics_derive::Metrics] macro will register all metrics under the `ethexe_rpc_*` scope. @@ -114,48 +124,29 @@ mod metrics { rename = "calls_started_total", describe = "Number of started RPC calls for the method" )] - calls_started: Counter, + pub calls_started: Counter, #[metric( rename = "calls_finished_total", labels = [("status", "ok")], describe = "Number of successfully finished RPC calls for the method" )] - calls_finished_ok: Counter, + pub calls_finished_ok: Counter, #[metric( rename = "calls_finished_total", labels = [("status", "error")], describe = "Number of failed RPC calls for the method" )] - calls_finished_err: Counter, + pub calls_finished_err: Counter, #[metric( rename = "call_duration_seconds", describe = "Latency of RPC calls for the method in seconds" )] - calls_latency_seconds: Histogram, + pub calls_latency_seconds: Histogram, #[metric( rename = "calls_in_flight", describe = "Number of in-flight RPC calls for the method" )] - calls_in_flight: Gauge, - } - - impl MethodMetrics { - pub fn on_incoming_request(&self) { - self.calls_started.increment(1); - self.calls_in_flight.increment(1); - } - - pub fn on_outgoing_response(&self, started_at: Instant, response: &MethodResponse) { - self.calls_latency_seconds - .record(started_at.elapsed().as_secs_f64()); - - match response.is_success() { - true => self.calls_finished_ok.increment(1), - false => self.calls_finished_err.increment(1), - } - - self.calls_in_flight.decrement(1); - } + pub calls_in_flight: Gauge, } /// The metrics for internal state of [crate::apis::InjectedApi]. From 72d776af862d523aa58aefa4f5f577386c4de146 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 4 May 2026 19:32:28 +0300 Subject: [PATCH 59/59] chore: fix cargo shear --- Cargo.lock | 1 - ethexe/common/Cargo.toml | 1 - ethexe/network/src/validator/topic.rs | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 583f9866fe8..afd7b900b34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5208,7 +5208,6 @@ dependencies = [ "sha3", "sp-core", "tap", - "thiserror 2.0.17", ] [[package]] diff --git a/ethexe/common/Cargo.toml b/ethexe/common/Cargo.toml index 51fee03a4da..29a6297f007 100644 --- a/ethexe/common/Cargo.toml +++ b/ethexe/common/Cargo.toml @@ -28,7 +28,6 @@ gsigner = { workspace = true, default-features = false, features = [ sha3.workspace = true k256 = { version = "0.13.4", features = ["ecdsa"], default-features = false } nonempty.workspace = true -thiserror.workspace = true # mock deps itertools = { workspace = true, optional = true } diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index ce4c5aad4a2..52e3b8a302a 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -94,7 +94,7 @@ enum VerifyMessageError { Reject(VerifyMessageRejectReason), } -#[derive(Debug, derive_more::Display)] +#[derive(Debug, PartialEq, Eq, derive_more::Display)] enum VerifyPromiseError { #[display("unknown validator: address={address}, tx_hash={tx_hash}")] UnknownValidator {