From 9d49c1c236b4c90939776195f06090becef0f790 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 15 Jun 2026 14:08:25 +0300 Subject: [PATCH 01/41] initial shielded txs handling --- ethexe/cli/src/commands/tx.rs | 4 +- ethexe/common/src/db.rs | 6 +- ethexe/common/src/injected.rs | 49 +++++++++++ ethexe/common/src/malachite.rs | 22 +++-- ethexe/compute/src/compute.rs | 3 + ethexe/malachite/service/src/externalities.rs | 68 ++++++++++------ ethexe/malachite/service/src/lib.rs | 4 +- ethexe/malachite/service/src/mempool.rs | 81 +++++++++++++------ ethexe/malachite/service/src/service.rs | 4 +- ethexe/malachite/service/src/tx_validity.rs | 42 ++++++---- .../service/tests/restart_resilience.rs | 8 +- ethexe/network/src/injected.rs | 20 ++--- ethexe/network/src/lib.rs | 4 +- ethexe/rpc/src/apis/injected/relay.rs | 28 ++++--- ethexe/rpc/src/apis/injected/server.rs | 12 +-- ethexe/rpc/src/apis/injected/trait.rs | 6 +- ethexe/rpc/src/lib.rs | 4 +- ethexe/rpc/src/tests.rs | 16 ++-- ethexe/sdk/src/mirror.rs | 4 +- ethexe/service/src/lib.rs | 2 +- ethexe/service/src/tests/mod.rs | 12 +-- ethexe/service/src/tests/utils/events.rs | 11 +-- 22 files changed, 269 insertions(+), 141 deletions(-) diff --git a/ethexe/cli/src/commands/tx.rs b/ethexe/cli/src/commands/tx.rs index 8350e454170..b864ad4a849 100644 --- a/ethexe/cli/src/commands/tx.rs +++ b/ethexe/cli/src/commands/tx.rs @@ -1081,7 +1081,7 @@ impl TxCommand { if !watch { ws_client - .send_transaction(transaction.clone()) + .send_transaction(transaction.clone().into()) .await .with_context(|| "failed to send injected transaction")?; } @@ -1102,7 +1102,7 @@ impl TxCommand { eprintln!("Waiting for reply (promise for injected transaction)..."); let mut subscription = ws_client - .send_transaction_and_watch(transaction) + .send_transaction_and_watch(transaction.into()) .await .with_context( || "failed to send injected transaction to Vara.eth RPC", diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index 390af381b92..85e28d82071 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -259,7 +259,7 @@ pub use mock_interfaces::{SetConfig, SetGlobals}; #[cfg(test)] mod tests { use super::*; - use crate::malachite::Operations; + // use crate::malachite::Operations; use indoc::formatdoc; use scale_info::{PortableRegistry, Registry, meta_type}; use sha3::{Digest, Sha3_256}; @@ -267,7 +267,7 @@ mod tests { #[test] fn ensure_types_unchanged() { const EXPECTED_TYPE_INFO_HASH: &str = - "600c7b8ccc11ab8c87a94170473bad7cf7c1c87973f5f56f3734ff4ad7473a2a"; + "c543e8c3d27f17bd77d510ce3f1d2b3a286b6444559444eb78807b3c2fd9ffbf"; let types = [ meta_type::(), @@ -288,7 +288,7 @@ mod tests { // NOTE: `Operation` hand-rolls its `Encode`/`Decode` (fixed-width // u32 tag), so this TypeInfo hash does NOT cover its wire format — // the exact bytes are pinned by `malachite::tests::operation_encoding_is_frozen`. - meta_type::(), + // meta_type::(), meta_type::(), meta_type::(), ]; diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index 1ef91c893c9..7a3188bab5e 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -433,6 +433,55 @@ pub struct ShieldedTransaction { pub salt: LimitedVec, } +#[cfg(feature = "shielded")] +pub type SignedShieldedTransaction = SignedMessage; + +#[cfg(feature = "shielded")] +#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] +#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq, derive_more::From)] +pub enum Transaction { + Injected(SignedInjectedTransaction), + Shielded(SignedShieldedTransaction), +} + +#[cfg(feature = "shielded")] +impl Transaction { + pub fn hash(&self) -> HashOf { + match self { + Self::Injected(tx) => tx.data().to_hash(), + Self::Shielded(_) => todo!("Shielded transaction hash"), + } + } + + pub fn reference_block(&self) -> H256 { + match self { + Self::Injected(tx) => tx.data().reference_block, + Self::Shielded(tx) => tx.data().reference_block, + } + } + + pub fn as_injected(&self) -> Option<&SignedInjectedTransaction> { + match self { + Self::Injected(tx) => Some(tx), + Self::Shielded(_) => None, + } + } + + pub fn into_injected(self) -> Option { + match self { + Self::Injected(tx) => Some(tx), + Self::Shielded(_) => None, + } + } +} + +#[cfg(feature = "shielded")] +impl ToDigest for ShieldedTransaction { + fn update_hasher(&self, _hasher: &mut sha3::Keccak256) { + todo!("Shielded transaction digest") + } +} + #[cfg(feature = "shielded")] impl ShieldedTransaction { /// Decrypts [Ciphertext] with provided [SharedSecret]. diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index d62cc3866a7..5e507689c56 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -28,17 +28,19 @@ //! depending on the consensus layer. use crate::injected::SignedInjectedTransaction; +#[cfg(feature = "shielded")] +use crate::injected::SignedShieldedTransaction; use alloc::vec::Vec; use derive_more::{Deref, DerefMut, IntoIterator}; use gprimitives::H256; use parity_scale_codec::{Decode, Encode}; -use scale_info::TypeInfo; +// use scale_info::TypeInfo; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; /// A single operation in the malachite block. -#[derive(Clone, Debug, PartialEq, Eq, TypeInfo, derive_more::IsVariant)] +#[derive(Clone, Debug, PartialEq, Eq, derive_more::IsVariant)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[repr(u32)] pub enum Operation { @@ -53,6 +55,10 @@ pub enum Operation { /// User-submitted transaction from the mempool. Injected(SignedInjectedTransaction) = 3, + + /// User-submitted shielded transaction from mempool. + #[cfg(feature = "shielded")] + Shielded(SignedShieldedTransaction) = 4, } impl Operation { @@ -71,6 +77,8 @@ impl Operation { Self::ProgressTasks => 1, Self::ProcessQueues { .. } => 2, Self::Injected(_) => 3, + #[cfg(feature = "shielded")] + Self::Shielded(_) => 4, } } } @@ -95,6 +103,10 @@ impl Decode for Operation { 3 => Ok(Operation::Injected(SignedInjectedTransaction::decode( input, )?)), + #[cfg(feature = "shielded")] + 4 => Ok(Operation::Shielded(SignedShieldedTransaction::decode( + input, + )?)), _ => Err(parity_scale_codec::Error::from("invalid operation tag")), } } @@ -108,14 +120,14 @@ impl Encode for Operation { Operation::ProgressTasks => {} Operation::ProcessQueues { gas_allowance } => gas_allowance.encode_to(dest), Operation::Injected(signed_tx) => signed_tx.encode_to(dest), + #[cfg(feature = "shielded")] + Operation::Shielded(shielded_tx) => shielded_tx.encode_to(dest), } } } /// Ordered list of [`Operation`]s; CAS key = Blake2b-256 of the SCALE-encoded list. -#[derive( - Clone, Debug, Default, PartialEq, Eq, Encode, Decode, TypeInfo, Deref, DerefMut, IntoIterator, -)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Encode, Decode, Deref, DerefMut, IntoIterator)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct Operations(pub Vec); diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 970f7d30da9..1591b11c74b 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -287,6 +287,9 @@ fn build_executable_data( let verified = signed.into_verified(); injected_transactions.push(verified); } + Operation::Shielded(shielded) => { + let _verified = shielded.into_verified(); + }, Operation::ProgressTasks => {} Operation::ProcessQueues { gas_allowance: op_gas_allowance, diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 5464d65a967..840a304c609 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -47,7 +47,7 @@ use async_trait::async_trait; use ethexe_common::{ MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, db::{CompactMb, GlobalsStorageRO, GlobalsStorageRW, MbStorageRO, MbStorageRW}, - injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, SignedInjectedTransaction}, + injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, Transaction}, malachite::{Operation, Operations}, }; use ethexe_db::Database; @@ -61,6 +61,20 @@ use std::{ use tokio::sync::{Notify, mpsc}; use tracing::{error, info, warn}; +fn operation_to_transaction(operation: &Operation) -> Option { + match operation { + Operation::Injected(tx) => Some(Transaction::Injected(tx.clone())), + _ => None, + } +} + +fn transaction_to_operation(transaction: Transaction) -> Operation { + match transaction { + Transaction::Injected(tx) => Operation::Injected(tx), + Transaction::Shielded(_) => todo!("Shielded transaction block inclusion"), + } +} + /// Inputs the externalities need to satisfy the [`ethexe_malachite_core::Externalities`] /// contract. Constructed by [`crate::MalachiteService::new`] and /// handed to the inner ethexe-malachite-core service inside an [`Arc`]. @@ -193,12 +207,9 @@ impl Externalities for EthexeExternalities { // Flush the committed injected txs from the mempool and add // their hashes to the seen-set so a re-gossip can't slip them // back in before they age out. - let injected: Vec = payload + let injected: Vec = payload .iter() - .filter_map(|tx| match tx { - Operation::Injected(t) => Some(t.clone()), - _ => None, - }) + .filter_map(operation_to_transaction) .collect(); if !injected.is_empty() { self.mempool.forget(&injected).await; @@ -254,7 +265,7 @@ impl Externalities for EthexeExternalities { // run through TxValidityChecker so we don't waste an MB // round-trip on a tx the participant would reject. let chain_head_snapshot = *self.chain_head.read().expect("chain_head poisoned"); - let valid: Vec = match chain_head_snapshot { + let valid: Vec = match chain_head_snapshot { Some(head) => { let checker = TxValidityChecker::new_for_mb(self.db.clone(), head, parent_mb_hash)?; let mut accepted = Vec::with_capacity(injected.len()); @@ -263,7 +274,7 @@ impl Externalities for EthexeExternalities { TxValidity::Valid => accepted.push(tx), reason => { warn!( - tx_hash = %tx.data().to_hash(), + tx_hash = %tx.hash(), ?reason, "build_block_above: dropping injected tx — fails TxValidity", ); @@ -317,7 +328,7 @@ impl Externalities for EthexeExternalities { } let mut size_counter: usize = 0; - let mut capped: Vec = Vec::with_capacity(valid.len()); + let mut capped: Vec = Vec::with_capacity(valid.len()); for tx in valid { // Skip the whole loop body once initial touched > limit — // any injected tx would only push it further over. @@ -332,7 +343,10 @@ impl Externalities for EthexeExternalities { continue; } - let destination = tx.data().destination; + let destination = match &tx { + Transaction::Injected(tx) => tx.data().destination, + Transaction::Shielded(_) => todo!("Shielded transaction touched-program cap"), + }; if !touched.contains(&destination) && touched.len() >= MAX_TOUCHED_PROGRAMS_PER_MB as usize { @@ -356,7 +370,7 @@ impl Externalities for EthexeExternalities { operations.push(Operation::AdvanceTillEthereumBlock { block_hash }); } for tx in capped { - operations.push(Operation::Injected(tx)); + operations.push(transaction_to_operation(tx)); } operations.push(Operation::ProgressTasks); operations.push(Operation::ProcessQueues { @@ -404,7 +418,7 @@ impl Externalities for EthexeExternalities { None }; - while let Some(Operation::Injected(_)) = next { + while matches!(next, Some(Operation::Injected(_))) { next = iter.next(); } @@ -523,7 +537,7 @@ impl Externalities for EthexeExternalities { // since the checker has no anchor to walk from. let has_injected = payload .iter() - .any(|tx| matches!(tx, Operation::Injected(_))); + .any(|tx| operation_to_transaction(tx).is_some()); if has_injected { warn!("validate: MB carries injected txs but no local chain head — abstaining"); return Ok(false); @@ -540,7 +554,7 @@ impl Externalities for EthexeExternalities { // local DB corruption, not a peer-side issue. let checker = TxValidityChecker::new_for_mb(self.db.clone(), chain_head, parent_hash)?; for tx in payload.iter() { - let Operation::Injected(signed) = tx else { + let Some(transaction) = operation_to_transaction(tx) else { continue; }; // `?` inside `check_tx_validity` only fires on local DB @@ -548,11 +562,11 @@ impl Externalities for EthexeExternalities { // is absent from CAS). Every malicious-tx-data path returns // `Ok(TxValidity::)` instead of `Err`, so this `?` // can't be triggered by what the proposer placed in the MB. - match checker.check_tx_validity(signed)? { + match checker.check_tx_validity(&transaction)? { TxValidity::Valid => {} reason => { warn!( - tx_hash = %signed.data().to_hash(), + tx_hash = %transaction.hash(), ?reason, "validate: injected tx fails TxValidity — rejecting MB", ); @@ -595,8 +609,12 @@ impl Externalities for EthexeExternalities { }; let limit = touched.len().max(MAX_TOUCHED_PROGRAMS_PER_MB as usize); for tx in payload.iter() { - if let Operation::Injected(signed) = tx { - touched.insert(signed.data().destination); + match tx { + Operation::Injected(signed) => { + touched.insert(signed.data().destination); + } + Operation::Shielded(_shielded) => todo!("implement me"), + _ => {} } } if touched.len() > limit { @@ -671,7 +689,7 @@ impl EthexeExternalities { async fn wait_for_proposable_content( &self, prev_advanced_eb_hash: H256, - ) -> (Option, Vec) { + ) -> (Option, Vec) { loop { let chain_head_notified = self.chain_head_notify.notified(); tokio::pin!(chain_head_notified); @@ -742,7 +760,7 @@ mod tests { use ethexe_common::{ BlockHeader, db::{BlockMetaStorageRW, OnChainStorageRW}, - injected::PurgedTransaction, + injected::{PurgedTransaction, SignedInjectedTransaction}, }; fn to_payload(bytes: Vec) -> BlockPayload { @@ -1122,12 +1140,12 @@ mod tests { /// can assert which txs reached the mempool eviction path. #[derive(Default)] struct ForgetTracker { - seen: tokio::sync::Mutex>, + seen: tokio::sync::Mutex>, } #[async_trait::async_trait] impl Mempool for ForgetTracker { - fn insert(&self, _tx: SignedInjectedTransaction) -> crate::mempool::TxInsertionStatus { + fn insert(&self, _tx: Transaction) -> crate::mempool::TxInsertionStatus { crate::mempool::TxInsertionStatus::Inserted } @@ -1135,10 +1153,10 @@ mod tests { Vec::new() } - async fn fetch(&self, _head: SimpleBlockData) -> Vec { + async fn fetch(&self, _head: SimpleBlockData) -> Vec { Vec::new() } - async fn forget(&self, committed: &[SignedInjectedTransaction]) { + async fn forget(&self, committed: &[Transaction]) { self.seen.lock().await.extend_from_slice(committed); } async fn wait_for_new_tx(&self) { @@ -1228,7 +1246,7 @@ mod tests { .unwrap(); let seen = tracker.seen.lock().await.clone(); - let seen_hashes: Vec<_> = seen.iter().map(|t| t.data().to_hash()).collect(); + let seen_hashes: Vec<_> = seen.iter().map(Transaction::hash).collect(); assert_eq!( seen.len(), 2, diff --git a/ethexe/malachite/service/src/lib.rs b/ethexe/malachite/service/src/lib.rs index 7a71f916731..adac9bab075 100644 --- a/ethexe/malachite/service/src/lib.rs +++ b/ethexe/malachite/service/src/lib.rs @@ -63,7 +63,7 @@ pub use crate::{ }; use ethexe_common::injected::PurgedTransaction; pub use ethexe_common::{ - injected::SignedInjectedTransaction, + injected::Transaction, malachite::{Operation, Operations}, }; pub use ethexe_malachite_core::{ @@ -138,6 +138,6 @@ fn _api_shape( _ops: Operations, _cert: CommitCertificate, _cfg: MalachiteConfig, - _tx: ethexe_common::injected::SignedInjectedTransaction, + _tx: ethexe_common::injected::Transaction, ) { } diff --git a/ethexe/malachite/service/src/mempool.rs b/ethexe/malachite/service/src/mempool.rs index ddefa48bb24..43714f394bd 100644 --- a/ethexe/malachite/service/src/mempool.rs +++ b/ethexe/malachite/service/src/mempool.rs @@ -40,8 +40,8 @@ use ethexe_common::{ HashOf, SimpleBlockData, db::{GlobalsStorageRO, InjectedStorageRW, OnChainStorageRO}, injected::{ - InjectedTransaction, InjectedTransactionAcceptance, PurgedTransaction, - SignedInjectedTransaction, TransactionPurgedReason, VALIDITY_WINDOW, + InjectedTransaction, InjectedTransactionAcceptance, PurgedTransaction, Transaction, + TransactionPurgedReason, VALIDITY_WINDOW, }, }; use ethexe_db::Database; @@ -119,7 +119,7 @@ pub trait Mempool: Send + Sync + 'static { /// [`TxInsertionStatus`] value. The method is infallible: invariant /// violations inside the implementation panic (e.g. a poisoned mutex) /// rather than surface as an error variant. - fn insert(&self, tx: SignedInjectedTransaction) -> TxInsertionStatus; + fn insert(&self, tx: Transaction) -> TxInsertionStatus; /// Drives validity-window GC. /// Returns the purged injected transactions. @@ -127,10 +127,10 @@ pub trait Mempool: Send + Sync + 'static { fn set_chain_head(&self, head: SimpleBlockData) -> Vec; /// Txs whose `reference_block` is an ancestor of `head`. - async fn fetch(&self, head: SimpleBlockData) -> Vec; + async fn fetch(&self, head: SimpleBlockData) -> Vec; /// Drop committed txs and remember their hashes for dedup. - async fn forget(&self, committed: &[SignedInjectedTransaction]); + async fn forget(&self, committed: &[Transaction]); /// Best-effort wake-up on new tx; spurious wake-ups allowed. async fn wait_for_new_tx(&self); @@ -146,7 +146,7 @@ pub(crate) struct EmptyMempool; #[cfg(test)] #[async_trait] impl Mempool for EmptyMempool { - fn insert(&self, _tx: SignedInjectedTransaction) -> TxInsertionStatus { + fn insert(&self, _tx: Transaction) -> TxInsertionStatus { TxInsertionStatus::Inserted } @@ -154,11 +154,11 @@ impl Mempool for EmptyMempool { Vec::new() } - async fn fetch(&self, _head: SimpleBlockData) -> Vec { + async fn fetch(&self, _head: SimpleBlockData) -> Vec { Vec::new() } - async fn forget(&self, _committed: &[SignedInjectedTransaction]) {} + async fn forget(&self, _committed: &[Transaction]) {} async fn wait_for_new_tx(&self) { std::future::pending().await @@ -176,7 +176,7 @@ pub const DEFAULT_POOL_CAPACITY: usize = 10_000; /// Pool state behind a single mutex — operations are short, contention low. #[derive(Debug, Default)] struct Inner { - pool: HashMap, SignedInjectedTransaction>, + pool: HashMap, Transaction>, /// Recently committed txs (tx_hash → ref_block) for dedup. Aged out with the validity window. seen: HashMap, H256>, /// Latest chain head height — drives age-out of pool/seen entries. @@ -214,6 +214,22 @@ impl InjectedTxMempool { self.inner.lock().expect("poisoned mempool").pool.is_empty() } + pub fn insert(&self, tx: impl Into) -> TxInsertionStatus { + ::insert(self, tx.into()) + } + + pub async fn forget(&self, committed: &[T]) + where + T: Clone + Into, + { + let committed = committed + .iter() + .cloned() + .map(Into::into) + .collect::>(); + ::forget(self, &committed).await + } + /// Resolve `reference_block` to its canonical height via the DB. /// Returns `None` if the block isn't in the DB yet. fn ref_block_height(&self, reference_block: H256) -> Option { @@ -270,7 +286,7 @@ impl InjectedTxMempool { }); let mut purged_txs = Vec::new(); inner.pool.retain(|tx_hash, tx| { - let ref_block = tx.data().reference_block; + let ref_block = tx.reference_block(); match db.block_header(ref_block).map(|h| h.height) { Some(h) if !Self::is_expired(head_height, h) => true, Some(h) => { @@ -303,19 +319,20 @@ impl InjectedTxMempool { #[async_trait] impl Mempool for InjectedTxMempool { - fn insert(&self, tx: SignedInjectedTransaction) -> TxInsertionStatus { - let tx_data = tx.data(); - let tx_hash = tx_data.to_hash(); - let ref_block = tx_data.reference_block; + fn insert(&self, tx: Transaction) -> TxInsertionStatus { + let tx_hash = tx.hash(); + let ref_block = tx.reference_block(); // Reject non-zero-value txs unconditionally (#5083 — value-bearing // injected txs are not supported yet). Done first so a malicious // sender can't burn pool capacity with txs that will never be // selectable. - if tx_data.value != 0 { + if let Transaction::Injected(tx_data) = &tx + && tx_data.data().value != 0 + { info!( %tx_hash, - value = tx_data.value, + value = tx_data.data().value, "mempool: rejecting tx — non-zero value (#5083 not supported)", ); return TxInsertionStatus::NonZeroValue; @@ -368,7 +385,10 @@ impl Mempool for InjectedTxMempool { // immediately picks the tx is guaranteed to find it in the DB. // The DB row is content-addressed by tx_hash, so two racing // writes converge on the same byte content. - self.db.set_injected_transaction(tx.clone()); + match &tx { + Transaction::Injected(tx) => self.db.set_injected_transaction(tx.clone()), + Transaction::Shielded(_) => todo!("Shielded transaction storage"), + } let mut inner = self.inner.lock().expect("poisoned mempool"); @@ -412,7 +432,7 @@ impl Mempool for InjectedTxMempool { Self::purge_expired(&mut inner, h, &self.db) } - async fn fetch(&self, head: SimpleBlockData) -> Vec { + async fn fetch(&self, head: SimpleBlockData) -> Vec { let ancestors = self.recent_ancestors(&head); let inner = self.inner.lock().expect("poisoned mempool"); @@ -420,7 +440,7 @@ impl Mempool for InjectedTxMempool { let result: Vec<_> = inner .pool .values() - .filter(|tx| ancestors.contains(&tx.data().reference_block)) + .filter(|tx| ancestors.contains(&tx.reference_block())) .cloned() .collect(); info!( @@ -434,12 +454,12 @@ impl Mempool for InjectedTxMempool { result } - async fn forget(&self, committed: &[SignedInjectedTransaction]) { + async fn forget(&self, committed: &[Transaction]) { let mut inner = self.inner.lock().expect("poisoned mempool"); for tx in committed { - let tx_hash = tx.data().to_hash(); + let tx_hash = tx.hash(); inner.pool.remove(&tx_hash); - inner.seen.insert(tx_hash, tx.data().reference_block); + inner.seen.insert(tx_hash, tx.reference_block()); } } @@ -462,7 +482,7 @@ mod tests { use ethexe_common::{ BlockHeader, PrivateKey, SignedMessage, SimpleBlockData, db::{BlockMetaStorageRW, GlobalsStorageRW, OnChainStorageRW}, - injected::{InjectedTransaction, InjectedTransactionAcceptance}, + injected::{InjectedTransaction, InjectedTransactionAcceptance, SignedInjectedTransaction}, }; use gprimitives::ActorId; use std::time::Duration; @@ -666,7 +686,14 @@ mod tests { let head = chain[2]; let fetched = futures::executor::block_on(pool.fetch(head)); assert_eq!(fetched.len(), 1); - assert_eq!(fetched[0].data().to_hash(), tx_hash); + assert_eq!( + fetched[0] + .as_injected() + .expect("injected transaction") + .data() + .to_hash(), + tx_hash + ); } #[test] @@ -1016,7 +1043,11 @@ mod tests { let fetched = futures::executor::block_on(pool.fetch(head)); for tx in &fetched { prop_assert_ne!( - tx.data().reference_block, alt_hash, + tx.as_injected() + .expect("injected transaction") + .data() + .reference_block, + alt_hash, "alt-branch tx surfaced on canonical fetch" ); } diff --git a/ethexe/malachite/service/src/service.rs b/ethexe/malachite/service/src/service.rs index dc2495325ad..bde19cde3ea 100644 --- a/ethexe/malachite/service/src/service.rs +++ b/ethexe/malachite/service/src/service.rs @@ -27,7 +27,7 @@ use anyhow::{Context as _, Result, anyhow}; use ethexe_common::{ Address, SimpleBlockData, db::{ConfigStorageRO, OnChainStorageRO}, - injected::SignedInjectedTransaction, + injected::Transaction, }; use ethexe_db::Database; use futures::{Stream, stream::FusedStream}; @@ -198,7 +198,7 @@ impl MalachiteService { /// queried via [`crate::mempool::TxInsertionStatus::is_accepted`]. pub fn receive_injected_transaction( &self, - tx: SignedInjectedTransaction, + tx: Transaction, ) -> crate::mempool::TxInsertionStatus { self.mempool.insert(tx) } diff --git a/ethexe/malachite/service/src/tx_validity.rs b/ethexe/malachite/service/src/tx_validity.rs index 78fe303879c..a2dba48546a 100644 --- a/ethexe/malachite/service/src/tx_validity.rs +++ b/ethexe/malachite/service/src/tx_validity.rs @@ -30,7 +30,7 @@ use ethexe_common::{ db::{GlobalsStorageRO, MbStorageRO, OnChainStorageRO}, events::{BlockRequestEvent, RouterRequestEvent, router::ProgramCreatedEvent}, gear::INJECTED_MESSAGE_PANIC_GAS_CHARGE_THRESHOLD, - injected::{InjectedTransaction, SignedInjectedTransaction, VALIDITY_WINDOW}, + injected::{InjectedTransaction, Transaction, VALIDITY_WINDOW}, malachite::Operation, }; use ethexe_db::Database; @@ -130,7 +130,11 @@ impl TxValidityChecker { } /// Determine [`TxValidity`] for one injected transaction. - pub fn check_tx_validity(&self, tx: &SignedInjectedTransaction) -> Result { + pub fn check_tx_validity(&self, tx: &Transaction) -> Result { + // let tx = tx.into(); + let Transaction::Injected(tx) = tx else { + todo!("Shielded transaction validity"); + }; let reference_block = tx.data().reference_block; if tx.data().value != 0 { @@ -363,7 +367,7 @@ mod tests { MaybeHashOf, PrivateKey, SignedMessage, StateHashWithQueueSize, db::{CompactMb, MbStorageRW, OnChainStorageRW}, gear_core::program::MemoryInfix, - injected::InjectedTransaction, + injected::{InjectedTransaction, SignedInjectedTransaction}, malachite::Operations, mock::{BlockChain, Mock, Tap}, }; @@ -393,12 +397,16 @@ mod tests { } } - fn signed_tx(tx: InjectedTransaction) -> SignedInjectedTransaction { + fn sign_injected_tx(tx: InjectedTransaction) -> SignedInjectedTransaction { SignedMessage::create(PrivateKey::random(), tx).unwrap() } - fn mock_tx(reference_block: H256) -> SignedInjectedTransaction { - signed_tx(test_injected_transaction(reference_block, ActorId::zero())) + fn mock_injected_tx() -> SignedInjectedTransaction { + sign_injected_tx(InjectedTransaction::mock(())) + } + + fn mock_tx(reference_block: H256) -> Transaction { + sign_injected_tx(test_injected_transaction(reference_block, ActorId::zero())).into() } fn program_state(initialized: bool, executable_balance: u128) -> ProgramState { @@ -524,8 +532,10 @@ mod tests { let chain = test_block_chain(100).setup(&db); let chain_head = chain.blocks[9].to_simple(); - let tx = mock_tx(chain.blocks[5].hash); - let parent_mb = setup_mb(&db, vec![tx.clone()], true, chain.mb_hash_at(8)); + let injected_tx = + sign_injected_tx(test_injected_transaction(chain_head.hash, ActorId::zero())); + let tx = injected_tx.clone().into(); + let parent_mb = setup_mb(&db, vec![injected_tx], true, chain.mb_hash_at(8)); let tx_checker = TxValidityChecker::new_for_mb(db.clone(), chain_head, parent_mb).unwrap(); assert_eq!( @@ -622,13 +632,14 @@ mod tests { let chain_head = chain.blocks[9].to_simple(); let tx = test_injected_transaction(chain.blocks[5].hash, ActorId::zero()) .tap_mut(|tx| tx.value = 100); + let tx = sign_injected_tx(tx).into(); let parent_mb = setup_mb(&db, vec![], true, chain.mb_hash_at(8)); let tx_checker = TxValidityChecker::new_for_mb(db.clone(), chain_head, parent_mb).unwrap(); assert_eq!( TxValidity::NonZeroValue, - tx_checker.check_tx_validity(&signed_tx(tx)).unwrap() + tx_checker.check_tx_validity(&tx).unwrap() ); } @@ -639,14 +650,14 @@ mod tests { let chain = test_block_chain(10).setup(&db); let chain_head = chain.blocks[9].to_simple(); - let tx = test_injected_transaction(H256::zero(), ActorId::zero()); + let tx = mock_injected_tx().into(); let parent_mb = setup_mb(&db, vec![], true, chain.mb_hash_at(8)); let tx_checker = TxValidityChecker::new_for_mb(db.clone(), chain_head, parent_mb).unwrap(); assert_eq!( TxValidity::Outdated, - tx_checker.check_tx_validity(&signed_tx(tx)).unwrap() + tx_checker.check_tx_validity(&tx).unwrap() ); } @@ -668,14 +679,14 @@ mod tests { .setup(&db); let chain_head = chain.blocks[3].to_simple(); - let tx = test_injected_transaction(chain.blocks[0].hash, ActorId::zero()); + let tx = mock_tx(chain.blocks[0].hash); let parent_mb = setup_mb(&db, vec![], true, chain.mb_hash_at(3)); let tx_checker = TxValidityChecker::new_for_mb(db.clone(), chain_head, parent_mb).unwrap(); assert_eq!( TxValidity::NotOnCurrentBranch, - tx_checker.check_tx_validity(&signed_tx(tx)).unwrap() + tx_checker.check_tx_validity(&tx).unwrap() ); } @@ -761,10 +772,9 @@ mod tests { .unwrap(); // value != 0 AND ref_block not in DB. NonZeroValue wins. - let tx = - test_injected_transaction(H256::random(), ActorId::zero()).tap_mut(|tx| tx.value = 1); + let tx = sign_injected_tx(InjectedTransaction::mock(()).tap_mut(|tx| tx.value = 1)).into(); assert_eq!( - checker.check_tx_validity(&signed_tx(tx)).unwrap(), + checker.check_tx_validity(&tx).unwrap(), TxValidity::NonZeroValue, ); } diff --git a/ethexe/malachite/service/tests/restart_resilience.rs b/ethexe/malachite/service/tests/restart_resilience.rs index ad7a3582ee9..b2f05cfb06a 100644 --- a/ethexe/malachite/service/tests/restart_resilience.rs +++ b/ethexe/malachite/service/tests/restart_resilience.rs @@ -22,7 +22,7 @@ use async_trait::async_trait; use ethexe_common::{ BlockHeader, SimpleBlockData, db::{BlockMetaStorageRW, CompactMb, GlobalsStorageRO, MbStorageRO, OnChainStorageRW}, - injected::{PurgedTransaction, SignedInjectedTransaction}, + injected::{PurgedTransaction, Transaction}, }; use ethexe_db::Database; use ethexe_malachite::{ @@ -40,7 +40,7 @@ struct EmptyMempool; #[async_trait] impl Mempool for EmptyMempool { - fn insert(&self, _tx: SignedInjectedTransaction) -> TxInsertionStatus { + fn insert(&self, _tx: Transaction) -> TxInsertionStatus { TxInsertionStatus::Inserted } @@ -48,11 +48,11 @@ impl Mempool for EmptyMempool { Vec::new() } - async fn fetch(&self, _head: SimpleBlockData) -> Vec { + async fn fetch(&self, _head: SimpleBlockData) -> Vec { Vec::new() } - async fn forget(&self, _committed: &[SignedInjectedTransaction]) {} + async fn forget(&self, _committed: &[Transaction]) {} async fn wait_for_new_tx(&self) { std::future::pending().await diff --git a/ethexe/network/src/injected.rs b/ethexe/network/src/injected.rs index 8b3f1c05ee4..2495c7804aa 100644 --- a/ethexe/network/src/injected.rs +++ b/ethexe/network/src/injected.rs @@ -8,7 +8,7 @@ use crate::{ }; use ethexe_common::{ Address, HashOf, - injected::{InjectedTransaction, InjectedTransactionAcceptance, SignedInjectedTransaction}, + injected::{InjectedTransaction, InjectedTransactionAcceptance, Transaction}, }; use futures::{FutureExt, StreamExt, future::BoxFuture, stream::FuturesUnordered}; use libp2p::{ @@ -66,7 +66,7 @@ impl Metrics { /// Network-only type to be encoded-decoded and sent over the network #[derive(Debug, Encode, Decode)] -pub(crate) struct InnerRequest(SignedInjectedTransaction); +pub(crate) struct InnerRequest(Transaction); /// Network-only type to be encoded-decoded and sent over the network #[derive(Debug, Encode, Decode)] @@ -77,7 +77,7 @@ pub enum Event { /// Peer sent a new transaction to us InboundTransaction { peer: PeerId, - transaction: Box, + transaction: Box, channel: oneshot::Sender, }, /// We got a response from a validator we sent transaction to @@ -93,7 +93,7 @@ impl Event { self, ) -> ( PeerId, - SignedInjectedTransaction, + Transaction, oneshot::Sender, ) { match self { @@ -155,14 +155,14 @@ impl Behaviour { } } - /// Broadcasts [SignedInjectedTransaction] to all known validators. + /// Broadcasts [Transaction] to all known validators. /// Returns the number of sent requests. pub fn broadcast_transaction( &mut self, identities: &ValidatorIdentities, - transaction: SignedInjectedTransaction, + transaction: Transaction, ) -> Result { - let tx_hash = transaction.data().to_hash(); + let tx_hash = transaction.hash(); if identities.is_empty() { return Err(SendTransactionError::NoValidatorsFound); @@ -400,7 +400,7 @@ mod tests { utils::tests::{arb_value, init_logger}, validator::discovery::{SignedValidatorIdentity, ValidatorAddresses, ValidatorIdentity}, }; - use ethexe_common::injected::InjectedTransaction; + use ethexe_common::injected::{InjectedTransaction, Transaction}; use gsigner::secp256k1::{Secp256k1SignerExt, Signer}; use libp2p::{ Swarm, Transport, @@ -410,12 +410,12 @@ mod tests { use libp2p_swarm_test::SwarmExt; use std::time::Duration; - fn signed_injected_tx() -> SignedInjectedTransaction { + fn signed_injected_tx() -> Transaction { let signer = Signer::memory(); let pub_key = signer.generate().unwrap(); let tx = arb_value::(()); - signer.signed_message(pub_key, tx, None).unwrap() + Transaction::Injected(signer.signed_message(pub_key, tx, None).unwrap()) } async fn new_swarm() -> (Swarm, SignedValidatorIdentity) { diff --git a/ethexe/network/src/lib.rs b/ethexe/network/src/lib.rs index 10b1f5ec942..612e3e7d0b1 100644 --- a/ethexe/network/src/lib.rs +++ b/ethexe/network/src/lib.rs @@ -44,7 +44,7 @@ use ethexe_common::{ Address, BlockHeader, ValidatorsVec, db::ConfigStorageRO, ecdsa::PublicKey, - injected::{SignedCompactTxReceipt, SignedInjectedTransaction}, + injected::{SignedCompactTxReceipt, Transaction}, network::{SignedValidatorMessage, VerifiedValidatorMessage}, }; use ethexe_db::Database; @@ -634,7 +634,7 @@ impl NetworkService { /// Send an injected transaction privately to all known validators. pub fn broadcast_injected_transaction( &mut self, - transaction: SignedInjectedTransaction, + transaction: Transaction, ) -> Result { let behaviour = self.swarm.behaviour_mut(); behaviour diff --git a/ethexe/rpc/src/apis/injected/relay.rs b/ethexe/rpc/src/apis/injected/relay.rs index f5d53760010..3610cd81ceb 100644 --- a/ethexe/rpc/src/apis/injected/relay.rs +++ b/ethexe/rpc/src/apis/injected/relay.rs @@ -7,7 +7,7 @@ //! validator in the current era and returns the first acceptance. use crate::{RpcEvent, errors}; -use ethexe_common::injected::{InjectedTransactionAcceptance, SignedInjectedTransaction}; +use ethexe_common::injected::{InjectedTransactionAcceptance, Transaction}; use jsonrpsee::core::RpcResult; use tokio::sync::{mpsc, oneshot}; @@ -25,20 +25,24 @@ impl TransactionsRelayer { /// returning the first `Accept` observed by the service. pub async fn relay( &self, - transaction: SignedInjectedTransaction, + transaction: Transaction, ) -> RpcResult { - let tx_hash = transaction.data().to_hash(); + let tx_hash = transaction.hash(); tracing::trace!(%tx_hash, ?transaction, "Called injected_sendTransaction with vars"); - if transaction.data().value != 0 { - tracing::warn!( - tx_hash = %tx_hash, - value = transaction.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", - )); + match &transaction { + Transaction::Injected(transaction) if transaction.data().value != 0 => { + tracing::warn!( + tx_hash = %tx_hash, + value = transaction.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", + )); + } + Transaction::Injected(_) => {} + Transaction::Shielded(_) => todo!("Shielded transaction relay validation"), } let (response_sender, response_receiver) = oneshot::channel(); diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index c34040e1fdc..a05ba08d821 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -12,7 +12,7 @@ use ethexe_common::{ db::InjectedStorageRO, injected::{ InjectedTransaction, InjectedTransactionAcceptance, SignedInjectedTransaction, - SignedTxReceipt, + SignedTxReceipt, Transaction, }, }; use ethexe_db::Database; @@ -39,7 +39,7 @@ pub struct InjectedApi { impl InjectedServer for InjectedApi { async fn send_transaction( &self, - transaction: SignedInjectedTransaction, + transaction: Transaction, ) -> RpcResult { self.send_transaction(transaction).await } @@ -47,7 +47,7 @@ impl InjectedServer for InjectedApi { async fn send_transaction_and_watch( &self, pending: PendingSubscriptionSink, - transaction: SignedInjectedTransaction, + transaction: Transaction, ) -> SubscriptionResult { self.send_transaction_and_watch(pending, transaction).await } @@ -90,7 +90,7 @@ impl InjectedApi { impl InjectedApi { async fn send_transaction( &self, - transaction: SignedInjectedTransaction, + transaction: Transaction, ) -> RpcResult { self.relayer.relay(transaction).await } @@ -99,9 +99,9 @@ impl InjectedApi { async fn send_transaction_and_watch( &self, pending: PendingSubscriptionSink, - transaction: SignedInjectedTransaction, + transaction: Transaction, ) -> SubscriptionResult { - let tx_hash = transaction.data().to_hash(); + let tx_hash = transaction.hash(); let pending_subscriber = match self.manager.try_register_subscriber(tx_hash) { Ok(subscriber) => subscriber, diff --git a/ethexe/rpc/src/apis/injected/trait.rs b/ethexe/rpc/src/apis/injected/trait.rs index 69773040603..0186340b7eb 100644 --- a/ethexe/rpc/src/apis/injected/trait.rs +++ b/ethexe/rpc/src/apis/injected/trait.rs @@ -5,7 +5,7 @@ use ethexe_common::{ HashOf, injected::{ InjectedTransaction, InjectedTransactionAcceptance, SignedInjectedTransaction, - SignedTxReceipt, + SignedTxReceipt, Transaction, }, }; use jsonrpsee::proc_macros::rpc; @@ -27,7 +27,7 @@ pub trait Injected { #[method(name = "sendTransaction")] async fn send_transaction( &self, - transaction: SignedInjectedTransaction, + transaction: Transaction, ) -> jsonrpsee::core::RpcResult; /// Sends an injected transaction and subscribes to its promise. @@ -38,7 +38,7 @@ pub trait Injected { )] async fn send_transaction_and_watch( &self, - transaction: SignedInjectedTransaction, + transaction: Transaction, ) -> jsonrpsee::core::SubscriptionResult; #[method(name = "getTransactionReceipt")] diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index e912b149a77..5be6fac8372 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -49,7 +49,7 @@ use apis::{ }; #[cfg(feature = "server")] use ethexe_common::injected::{ - InjectedTransactionAcceptance, Promise, SignedCompactTxReceipt, SignedInjectedTransaction, + InjectedTransactionAcceptance, Promise, SignedCompactTxReceipt, Transaction, }; #[cfg(feature = "server")] use ethexe_db::Database; @@ -96,7 +96,7 @@ pub const DEFAULT_BLOCK_GAS_LIMIT_MULTIPLIER: u64 = 10; #[derive(Debug)] pub enum RpcEvent { InjectedTransaction { - transaction: SignedInjectedTransaction, + transaction: Transaction, response_sender: oneshot::Sender, }, } diff --git a/ethexe/rpc/src/tests.rs b/ethexe/rpc/src/tests.rs index 6c0602e42ed..54d41785c7b 100644 --- a/ethexe/rpc/src/tests.rs +++ b/ethexe/rpc/src/tests.rs @@ -12,6 +12,7 @@ use ethexe_common::{ gear::MAX_BLOCK_GAS_LIMIT, injected::{ InjectedTransaction, Promise, Receipt, SignedCompactTxReceipt, SignedInjectedTransaction, + Transaction, }, mock::Mock, }; @@ -61,7 +62,7 @@ impl MockService { let mut tx_batch_interval = tokio::time::interval(std::time::Duration::from_millis(350)); - let mut tx_batch = Vec::new(); + let mut tx_batch = Vec::::new(); loop { tokio::select! { @@ -79,7 +80,10 @@ impl MockService { let RpcEvent::InjectedTransaction {transaction, response_sender} = event.expect("RPC event will be valid"); response_sender.send(InjectedTransactionAcceptance::Accept).expect("Response sender will be valid"); - tx_batch.push(transaction); + match transaction { + Transaction::Injected(transaction) => tx_batch.push(transaction), + Transaction::Shielded(_) => todo!("Shielded transaction execution"), + } }, } } @@ -202,7 +206,7 @@ async fn test_cleanup_promise_subscribers() { let mut subscribers = JoinSet::new(); for _ in 0..20 { let mut sub = ws_client - .send_transaction_and_watch(mock_signed_transaction()) + .send_transaction_and_watch(mock_signed_transaction().into()) .await .expect("Subscription will be created"); @@ -231,7 +235,7 @@ async fn test_cleanup_promise_subscribers() { let mut subscribers = JoinSet::new(); for _ in 0..20 { let mut subscription = ws_client - .send_transaction_and_watch(mock_signed_transaction()) + .send_transaction_and_watch(mock_signed_transaction().into()) .await .expect("Subscription will be created"); @@ -259,7 +263,7 @@ async fn test_cleanup_promise_subscribers() { let mut subscriptions = vec![]; for _ in 0..20 { let subscription = ws_client - .send_transaction_and_watch(mock_signed_transaction()) + .send_transaction_and_watch(mock_signed_transaction().into()) .await .expect("Subscription will be created"); subscriptions.push(subscription); @@ -296,7 +300,7 @@ async fn test_concurrent_multiple_clients() { let mut subscriptions = vec![]; for _ in 0..50 { let mut subscription = client - .send_transaction_and_watch(mock_signed_transaction()) + .send_transaction_and_watch(mock_signed_transaction().into()) .await .expect("Subscription will be created"); diff --git a/ethexe/sdk/src/mirror.rs b/ethexe/sdk/src/mirror.rs index 02bbaa294bc..f7aa0929f18 100644 --- a/ethexe/sdk/src/mirror.rs +++ b/ethexe/sdk/src/mirror.rs @@ -213,7 +213,7 @@ impl<'a> Mirror<'a> { let result: InjectedTransactionAcceptance = self .api .vara_eth_client - .send_transaction(transaction) + .send_transaction(transaction.into()) .await .with_context(|| "failed to send injected transaction")?; @@ -238,7 +238,7 @@ impl<'a> Mirror<'a> { let mut subscription = self .api .vara_eth_client - .send_transaction_and_watch(transaction) + .send_transaction_and_watch(transaction.into()) .await .with_context(|| "failed to send injected transaction and subscribe to it's promise")?; diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index d0d3d752982..3751ac42818 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -908,7 +908,7 @@ impl Service { } _ => { // no local malachite or malachite reject transaction, wait for other acceptances - let tx_hash = transaction.data().to_hash(); + let tx_hash = transaction.hash(); if let Some(pending) = network_injected_txs.get_mut(&tx_hash) { diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 6169d4ad832..a33fe347afe 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -1881,7 +1881,7 @@ async fn send_injected_tx() { let acceptance = node1 .rpc_http_client() .unwrap() - .send_transaction(signed_tx.clone()) + .send_transaction(signed_tx.clone().into()) .await .expect("rpc server is set"); assert_eq!(acceptance, InjectedTransactionAcceptance::Accept); @@ -1891,7 +1891,7 @@ async fn send_injected_tx() { .events() .find(|event| { if let TestingEvent::Rpc(TestingRpcEvent::InjectedTransaction { transaction }) = event - && *transaction == signed_tx + && transaction.as_injected() == Some(&signed_tx) { true } else { @@ -1947,7 +1947,7 @@ async fn injected_tx_purged_receipt() { let rpc_tx = env.signer.signed_message(pubkey, tx, None).unwrap(); let mut subscription = rpc_client - .send_transaction_and_watch(rpc_tx) + .send_transaction_and_watch(rpc_tx.into()) .await .expect("successfully subscribe for transaction receipt"); @@ -2632,7 +2632,7 @@ async fn injected_tx_fungible_token() { .unwrap(); let mut subscription = rpc_client - .send_transaction_and_watch(rpc_tx) + .send_transaction_and_watch(rpc_tx.into()) .await .expect("successfully send transaction to RPC"); @@ -2738,7 +2738,7 @@ async fn injected_tx_fungible_token() { .expect("RPC WS client provide by node"); let mut subscription = ws_client - .send_transaction_and_watch(rpc_tx) + .send_transaction_and_watch(rpc_tx.into()) .await .expect("successfully subscribe for transaction promise"); @@ -2882,7 +2882,7 @@ async fn injected_tx_fungible_token_over_network() { .await; let mut subscription = alice_rpc_client - .send_transaction_and_watch(rpc_tx) + .send_transaction_and_watch(rpc_tx.into()) .await .expect("successfully subscribe for transaction promise"); diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index 4c8530ecd5e..3b036896b8a 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -12,8 +12,7 @@ use ethexe_common::{ db::*, events::BlockEvent, injected::{ - InjectedTransaction, InjectedTransactionAcceptance, SignedCompactTxReceipt, - SignedInjectedTransaction, + InjectedTransaction, InjectedTransactionAcceptance, SignedCompactTxReceipt, Transaction, }, network::VerifiedValidatorMessage, }; @@ -45,7 +44,7 @@ pub type ObserverEventReceiver = KickingStream>; #[derive(Debug, Clone, Eq, PartialEq)] pub enum TestingNetworkInjectedEvent { InboundTransaction { - transaction: SignedInjectedTransaction, + transaction: Transaction, }, OutboundAcceptance { transaction_hash: HashOf, @@ -61,7 +60,7 @@ impl TestingNetworkInjectedEvent { transaction, channel: _, } => Self::InboundTransaction { - transaction: SignedInjectedTransaction::clone(transaction), + transaction: transaction.as_ref().clone(), }, NetworkInjectedEvent::OutboundAcceptance { transaction_hash, @@ -103,9 +102,7 @@ impl TestingNetworkEvent { #[derive(Debug, Clone, Eq, PartialEq)] pub enum TestingRpcEvent { - InjectedTransaction { - transaction: SignedInjectedTransaction, - }, + InjectedTransaction { transaction: Transaction }, } impl TestingRpcEvent { From 517ea1fbed88d92e6cafd4fe724fb7fa540a247f Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 15 Jun 2026 16:46:45 +0300 Subject: [PATCH 02/41] simple Voting extension for malachite --- Cargo.lock | 12 +- Cargo.toml | 3 +- ethexe/common/src/malachite.rs | 28 +++- ethexe/malachite/core/src/app.rs | 126 +++++++++++++++--- ethexe/malachite/core/src/codec.rs | 91 ++++++++++--- ethexe/malachite/core/src/externalities.rs | 26 +++- .../malachite/core/tests/multi_validators.rs | 12 +- ethexe/malachite/service/Cargo.toml | 1 + ethexe/malachite/service/src/externalities.rs | 69 +++++++++- 9 files changed, 306 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5a19ceb735..497d7235f2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5799,6 +5799,7 @@ dependencies = [ "alloy", "anyhow", "async-trait", + "bytes", "derive_more 2.1.1", "ethexe-common", "ethexe-db", @@ -6273,14 +6274,12 @@ dependencies = [ [[package]] name = "ferveo-gear-common" version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "049504d8d4e9c23ae2ff50e37f81cac1c191eb71baf79f20bf9361ee29546461" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#8674e4f1009600f96016320a28882c4cc57d1ab1" dependencies = [ "ark-ec 0.6.0", "ark-serialize 0.6.0", "ark-std 0.6.0", "bincode", - "const-hex", "generic-array 0.14.7", "rand 0.8.5", "serde", @@ -6290,8 +6289,7 @@ dependencies = [ [[package]] name = "ferveo-gear-tdec" version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "991a44fbe8ea0d5b1e3c49884c1cda791d0bd3612a2deacf652d027d0b759090" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#8674e4f1009600f96016320a28882c4cc57d1ab1" dependencies = [ "ark-bls12-381 0.6.0", "ark-ec 0.6.0", @@ -6301,7 +6299,6 @@ dependencies = [ "ark-std 0.6.0", "bincode", "chacha20poly1305", - "const-hex", "ferveo-gear-common", "itertools 0.10.5", "parity-scale-codec", @@ -18919,8 +18916,7 @@ dependencies = [ [[package]] name = "subproductdomain-gear" version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e0585b62ebf2ec655a49e5f60033b0c240695e18ef3f28f4b81710ec15cbbd" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#8674e4f1009600f96016320a28882c4cc57d1ab1" dependencies = [ "anyhow", "ark-ec 0.6.0", diff --git a/Cargo.toml b/Cargo.toml index 96861fc1a76..d26b6d78be5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -268,7 +268,8 @@ metrics = "0.24.0" metrics-derive = "0.1" metrics-exporter-prometheus = { version = "0.16.0", default-features = false } -gear-tdec = { package = "ferveo-gear-tdec", version = "0.5.0"} +# gear-tdec = { package = "ferveo-gear-tdec", version = "0.5.0"} +gear-tdec = { package = "ferveo-gear-tdec", git = "https://github.com/gear-tech/ferveo-nucypher.git", branch = "more-codec"} # Published deps # diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index 5e507689c56..8b4b0a11660 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -28,13 +28,12 @@ //! depending on the consensus layer. use crate::injected::SignedInjectedTransaction; -#[cfg(feature = "shielded")] -use crate::injected::SignedShieldedTransaction; use alloc::vec::Vec; use derive_more::{Deref, DerefMut, IntoIterator}; use gprimitives::H256; use parity_scale_codec::{Decode, Encode}; -// use scale_info::TypeInfo; +#[cfg(feature = "shielded")] +use {crate::injected::SignedShieldedTransaction, gear_tdec::bls12_381::DecryptionShareSimple}; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; @@ -142,6 +141,29 @@ impl Operations { } } +/// Opaque application data attached to a Malachite precommit vote. +/// +/// The consensus layer transports this as bytes. Ethexe decodes the bytes into +/// this type at the application boundary, so the generic Malachite service does +/// not need to know about shielded transactions or threshold-decryption types. +#[cfg(feature = "shielded")] +#[derive(Clone, Debug, Default, PartialEq, Eq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct VotingExtension { + pub decryption_shares: Vec, +} + +/// One validator's decryption-share payload for one shielded transaction. +/// Holds [DecryptionShareSimple] over [ShieldedTransaction]. +/// +/// [ShieldedTransaction]: crate::injected::ShieldedTransaction +#[cfg(feature = "shielded")] +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct VotingDecryptionShare { + pub tx_hash: H256, + pub share: DecryptionShareSimple, +} #[cfg(test)] mod tests { use super::*; diff --git a/ethexe/malachite/core/src/app.rs b/ethexe/malachite/core/src/app.rs index 9f2452741ae..02f29406386 100644 --- a/ethexe/malachite/core/src/app.rs +++ b/ethexe/malachite/core/src/app.rs @@ -52,7 +52,7 @@ use malachitebft_app_channel::{ }, }, }; -use malachitebft_core_types::Height as _; +use malachitebft_core_types::{Height as _, VoteExtensions}; use parity_scale_codec::{Decode, Encode}; use std::{ops::RangeInclusive, sync::Arc}; use tracing::{error, info}; @@ -170,14 +170,35 @@ where } } - // Vote extensions (unused — return defaults). - AppMsg::ExtendVote { reply, .. } => { - if reply.send(self.process_extend_vote()).is_err() { + // Vote extensions. + AppMsg::ExtendVote { + value_id, reply, .. + } => { + let extension = self + .process_extend_vote(value_id) + .await + .unwrap_or_else(|e| { + error!(?e, %value_id, "ExtendVote: process failed"); + None + }); + if reply.send(extension).is_err() { error!("ExtendVote: failed to send reply"); } } - AppMsg::VerifyVoteExtension { reply, .. } => { - if reply.send(self.process_verify_vote_extension()).is_err() { + AppMsg::VerifyVoteExtension { + value_id, + reply, + extension, + .. + } => { + let result = self + .process_verify_vote_extension(value_id, extension) + .await + .unwrap_or_else(|e| { + error!(?e, %value_id, "VerifyVoteExtension: process failed"); + Err(VoteExtensionError::InvalidVoteExtension) + }); + if reply.send(result).is_err() { error!("VerifyVoteExtension: failed to send reply"); } } @@ -209,7 +230,7 @@ where // Finalized (commit + cascade). AppMsg::Finalized { certificate, - extensions: _, + extensions, evidence, reply, } => { @@ -221,7 +242,7 @@ where evidence = ?evidence, "Finalized" ); - let next = match self.process_finalized(certificate).await { + let next = match self.process_finalized(certificate, extensions).await { Ok(()) => { let h = self.state.current_height; Next::Start( @@ -405,12 +426,47 @@ where Ok(locally) } - fn process_extend_vote(&self) -> Option { - None + async fn process_extend_vote(&self, value_id: ValueId) -> Result> { + let Some(block) = self.block_by_value_id(value_id)? else { + return Ok(None); + }; + self.externalities + .extend_vote(block.hash(), block) + .await + .context("extend vote") } - fn process_verify_vote_extension(&self) -> Result<(), VoteExtensionError> { - Ok(()) + async fn process_verify_vote_extension( + &self, + value_id: ValueId, + extension: Bytes, + ) -> Result> { + let Some(block) = self.block_by_value_id(value_id)? else { + return Ok(Err(VoteExtensionError::InvalidVoteExtension)); + }; + let is_valid = self + .externalities + .verify_vote_extension(block.hash(), block, extension) + .await + .context("verify vote extension")?; + Ok(if is_valid { + Ok(()) + } else { + Err(VoteExtensionError::InvalidVoteExtension) + }) + } + + fn block_by_value_id(&self, value_id: ValueId) -> Result> { + let Some(proposal) = self + .state + .store + .get_undecided_proposal_by_value_id(&value_id)? + else { + return Ok(None); + }; + let block = Block::decode(&mut &proposal.value.block_bytes[..]) + .map_err(|e| anyhow!("decoding Block for vote extension: {e}"))?; + Ok(Some(block)) } // TODO: #5475 add per-peer token-bucket rate limit before `ingest_proposal_part` @@ -467,12 +523,13 @@ where async fn process_finalized( &mut self, certificate: EngineCert, + extensions: VoteExtensions, ) -> Result<(), FinalizationError> { let (block_bytes, _cert) = self .state .commit(certificate.clone()) .map_err(FinalizationError::NonFatal)?; - self.ingest_finalized(certificate, block_bytes) + self.ingest_finalized(certificate, block_bytes, extensions) .await .context("ingest finalized") .map_err(FinalizationError::Fatal) @@ -668,11 +725,21 @@ where /// [`Store::cascade_finalize`] silently no-ops on an unsaved /// ancestor (the `finalize_chain` walk returns `None`), and the /// `errors_tx` channel surfaces the contract breach upstream. - async fn ingest_finalized(&self, cert: EngineCert, block_bytes: Vec) -> Result<()> { + async fn ingest_finalized( + &self, + cert: EngineCert, + block_bytes: Vec, + extensions: VoteExtensions, + ) -> Result<()> { let block = Block::decode(&mut &block_bytes[..]) .map_err(|e| anyhow!("decoding Block at finalize: {e}"))?; let block_hash = block.hash(); let height = cert.height.as_u64(); + let finalized_extensions: Vec<_> = extensions + .extensions + .into_iter() + .map(|(address, extension)| (address, extension.message)) + .collect(); let app_cert = CommitCertificate { height, @@ -716,7 +783,12 @@ where .store .cascade_finalize(vec![block_hash], |hash, cert| { let ext = Arc::clone(&self.externalities); - async move { ext.process_mb_finalized(hash, cert).await } + let extensions = if hash == block_hash { + finalized_extensions.clone() + } else { + Vec::new() + }; + async move { ext.process_mb_finalized(hash, cert, extensions).await } }) .await?; Ok(()) @@ -788,7 +860,12 @@ mod tests { async fn process_mb_proposal(&self, _: H256, _: Block) -> Result<()> { Ok(()) } - async fn process_mb_finalized(&self, _: H256, _: CommitCertificate) -> Result<()> { + async fn process_mb_finalized( + &self, + _: H256, + _: CommitCertificate, + _: Vec<(Address, Bytes)>, + ) -> Result<()> { Ok(()) } async fn build_block_above(&self, _: H256) -> Result { @@ -971,7 +1048,12 @@ mod tests { async fn process_mb_proposal(&self, _: H256, _: Block) -> Result<()> { Ok(()) } - async fn process_mb_finalized(&self, _: H256, _: CommitCertificate) -> Result<()> { + async fn process_mb_finalized( + &self, + _: H256, + _: CommitCertificate, + _: Vec<(Address, Bytes)>, + ) -> Result<()> { Err(anyhow!("application: finalize-side store write failed")) } async fn build_block_above(&self, _: H256) -> Result { @@ -1078,7 +1160,10 @@ mod tests { commit_signatures: Vec::new(), }; - match handler.process_finalized(cert).await { + match handler + .process_finalized(cert, VoteExtensions::default()) + .await + { Err(FinalizationError::Fatal(_)) => { // Expected: app::run propagates the error and the // service tears down rather than silently moving on. @@ -1160,7 +1245,10 @@ mod tests { value_id, commit_signatures: Vec::new(), }; - match handler.process_finalized(cert).await { + match handler + .process_finalized(cert, VoteExtensions::default()) + .await + { Ok(()) => {} Err(FinalizationError::Fatal(e)) => panic!("Fatal: {e:?}"), Err(FinalizationError::NonFatal(e)) => panic!("NonFatal: {e:?}"), diff --git a/ethexe/malachite/core/src/codec.rs b/ethexe/malachite/core/src/codec.rs index 859f82ddcc2..1a83b9651be 100644 --- a/ethexe/malachite/core/src/codec.rs +++ b/ethexe/malachite/core/src/codec.rs @@ -25,8 +25,8 @@ use malachitebft_codec::{Codec, HasEncodedLen}; use malachitebft_core_consensus::{LivenessMsg, ProposedValue, SignedConsensusMsg}; use malachitebft_core_types::{ CommitCertificate, CommitSignature, NilOrVal, PolkaCertificate, PolkaSignature, Round, - RoundCertificate, RoundCertificateType, RoundSignature, SignedProposal, SignedVote, - ValidatorProof, Validity, VoteType, + RoundCertificate, RoundCertificateType, RoundSignature, SignedExtension, SignedMessage, + SignedProposal, SignedVote, ValidatorProof, Validity, VoteType, }; use malachitebft_engine::util::streaming::{StreamContent, StreamMessage}; use malachitebft_sync::{ @@ -228,19 +228,57 @@ struct RawSignedMessage { signature: RawSignature, } +#[derive(Encode, Decode)] +struct RawSignedVote { + message: Vec, + signature: RawSignature, + extension: Option, +} + +#[derive(Encode, Decode)] +struct RawSignedExtension { + message: Vec, + signature: RawSignature, +} + +impl From> for RawSignedExtension { + fn from(value: SignedExtension) -> Self { + Self { + message: value.message.to_vec(), + signature: RawSignature::from(&value.signature), + } + } +} + +impl TryFrom for SignedExtension { + type Error = CodecError; + + fn try_from(value: RawSignedExtension) -> Result { + Ok(SignedMessage::new( + Bytes::from(value.message), + Signature::try_from(value.signature)?, + )) + } +} + #[derive(Encode, Decode)] enum RawSignedConsensusMsg { - Vote(RawSignedMessage), + Vote(RawSignedVote), Proposal(RawSignedMessage), } impl From> for RawSignedConsensusMsg { fn from(value: SignedConsensusMsg) -> Self { match value { - SignedConsensusMsg::Vote(vote) => Self::Vote(RawSignedMessage { - message: vote.message.to_sign_bytes().to_vec(), - signature: RawSignature::from(&vote.signature), - }), + SignedConsensusMsg::Vote(vote) => { + let mut message = vote.message; + let extension = message.extension.take().map(RawSignedExtension::from); + Self::Vote(RawSignedVote { + message: message.to_sign_bytes().to_vec(), + signature: RawSignature::from(&vote.signature), + extension, + }) + } SignedConsensusMsg::Proposal(proposal) => Self::Proposal(RawSignedMessage { message: proposal.message.to_sign_bytes().to_vec(), signature: RawSignature::from(&proposal.signature), @@ -253,10 +291,14 @@ impl TryFrom for SignedConsensusMsg { type Error = CodecError; fn try_from(value: RawSignedConsensusMsg) -> Result { match value { - RawSignedConsensusMsg::Vote(raw) => Ok(SignedConsensusMsg::Vote(SignedVote { - message: Vote::from_sign_bytes(&raw.message)?, - signature: Signature::try_from(raw.signature)?, - })), + RawSignedConsensusMsg::Vote(raw) => { + let mut message = Vote::from_sign_bytes(&raw.message)?; + message.extension = raw.extension.map(SignedExtension::try_from).transpose()?; + Ok(SignedConsensusMsg::Vote(SignedVote { + message, + signature: Signature::try_from(raw.signature)?, + })) + } RawSignedConsensusMsg::Proposal(raw) => { Ok(SignedConsensusMsg::Proposal(SignedProposal { message: Proposal::from_sign_bytes(&raw.message)?, @@ -544,7 +586,7 @@ struct RawRoundCertificate { #[derive(Encode, Decode)] enum RawLivenessMsg { - Vote(RawSignedMessage), + Vote(RawSignedVote), PolkaCertificate(RawPolkaCertificate), SkipRoundCertificate(RawRoundCertificate), } @@ -552,10 +594,15 @@ enum RawLivenessMsg { impl From> for RawLivenessMsg { fn from(value: LivenessMsg) -> Self { match value { - LivenessMsg::Vote(vote) => Self::Vote(RawSignedMessage { - message: vote.message.to_sign_bytes().to_vec(), - signature: RawSignature::from(&vote.signature), - }), + LivenessMsg::Vote(vote) => { + let mut message = vote.message; + let extension = message.extension.take().map(RawSignedExtension::from); + Self::Vote(RawSignedVote { + message: message.to_sign_bytes().to_vec(), + signature: RawSignature::from(&vote.signature), + extension, + }) + } LivenessMsg::PolkaCertificate(polka) => Self::PolkaCertificate(RawPolkaCertificate { height: polka.height.as_u64(), round: round_to_i64(polka.round), @@ -594,10 +641,14 @@ impl TryFrom for LivenessMsg { type Error = CodecError; fn try_from(value: RawLivenessMsg) -> Result { Ok(match value { - RawLivenessMsg::Vote(raw) => LivenessMsg::Vote(SignedVote { - message: Vote::from_sign_bytes(&raw.message)?, - signature: Signature::try_from(raw.signature)?, - }), + RawLivenessMsg::Vote(raw) => { + let mut message = Vote::from_sign_bytes(&raw.message)?; + message.extension = raw.extension.map(SignedExtension::try_from).transpose()?; + LivenessMsg::Vote(SignedVote { + message, + signature: Signature::try_from(raw.signature)?, + }) + } RawLivenessMsg::PolkaCertificate(cert) => { let mut polka_signatures = Vec::with_capacity(cert.polka_signatures.len()); for s in cert.polka_signatures { diff --git a/ethexe/malachite/core/src/externalities.rs b/ethexe/malachite/core/src/externalities.rs index 87a494e317f..658ea5e8966 100644 --- a/ethexe/malachite/core/src/externalities.rs +++ b/ethexe/malachite/core/src/externalities.rs @@ -5,8 +5,9 @@ use anyhow::Result; use async_trait::async_trait; +use bytes::Bytes; -use crate::types::{Block, BlockPayload, CommitCertificate, H256}; +use crate::types::{Address, Block, BlockPayload, CommitCertificate, H256}; /// Application-side callbacks the consensus service requires. /// @@ -53,7 +54,28 @@ pub trait Externalities: Send + Sync + 'static { /// `cert` is the BFT commit certificate for the height of /// `mb_hash`. The application typically forwards `cert` to /// downstream layers (on-chain commits, light clients, etc.). - async fn process_mb_finalized(&self, mb_hash: H256, cert: CommitCertificate) -> Result<()>; + async fn process_mb_finalized( + &self, + mb_hash: H256, + cert: CommitCertificate, + extensions: Vec<(Address, Bytes)>, + ) -> Result<()>; + + /// Build an optional opaque vote extension for the block this node is about + /// to precommit. + async fn extend_vote(&self, _mb_hash: H256, _block: Block) -> Result> { + Ok(None) + } + + /// Application-side validation for an opaque vote extension. + async fn verify_vote_extension( + &self, + _mb_hash: H256, + _block: Block, + _extension: Bytes, + ) -> Result { + Ok(false) + } /// Build a fresh block payload whose parent has hash /// `parent_mb_hash`. Called only when this node has been elected diff --git a/ethexe/malachite/core/tests/multi_validators.rs b/ethexe/malachite/core/tests/multi_validators.rs index e1a744dd89b..0a464e60741 100644 --- a/ethexe/malachite/core/tests/multi_validators.rs +++ b/ethexe/malachite/core/tests/multi_validators.rs @@ -35,9 +35,10 @@ fn init_tracing() { use anyhow::Result; use async_trait::async_trait; +use bytes::Bytes; use ethexe_malachite_core::{ - Block, BlockPayload, CommitCertificate, Externalities, H256, MalachiteConfig, MalachiteService, - Multiaddr, NodeRole, ValidatorEntry, libp2p_peer_id, + Address, Block, BlockPayload, CommitCertificate, Externalities, H256, MalachiteConfig, + MalachiteService, Multiaddr, NodeRole, ValidatorEntry, libp2p_peer_id, }; use proptest::prelude::*; use tempfile::TempDir; @@ -124,7 +125,12 @@ impl Externalities for TestExt { Ok(()) } - async fn process_mb_finalized(&self, hash: H256, cert: CommitCertificate) -> Result<()> { + async fn process_mb_finalized( + &self, + hash: H256, + cert: CommitCertificate, + _: Vec<(Address, Bytes)>, + ) -> Result<()> { let mut s = self.state.lock().unwrap(); if cert.block_hash != hash { s.violations diff --git a/ethexe/malachite/service/Cargo.toml b/ethexe/malachite/service/Cargo.toml index ab1043762c9..0503bf9c0f3 100644 --- a/ethexe/malachite/service/Cargo.toml +++ b/ethexe/malachite/service/Cargo.toml @@ -12,6 +12,7 @@ repository.workspace = true alloy = { workspace = true, features = ["eips"] } anyhow.workspace = true async-trait.workspace = true +bytes.workspace = true derive_more.workspace = true futures.workspace = true parity-scale-codec.workspace = true diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 840a304c609..4b80f836b7b 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -44,14 +44,17 @@ use crate::{ }; use anyhow::{Result, anyhow}; use async_trait::async_trait; +use bytes::Bytes; use ethexe_common::{ MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, db::{CompactMb, GlobalsStorageRO, GlobalsStorageRW, MbStorageRO, MbStorageRW}, injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, Transaction}, - malachite::{Operation, Operations}, + malachite::{Operation, Operations, VotingExtension}, }; use ethexe_db::Database; -use ethexe_malachite_core::{Block, BlockPayload, Externalities, MAX_BLOCK_PAYLOAD_BYTES}; +use ethexe_malachite_core::{ + Address as MalachiteAddress, Block, BlockPayload, Externalities, MAX_BLOCK_PAYLOAD_BYTES, +}; use gprimitives::H256; use parity_scale_codec::{DecodeAll, Encode}; use std::{ @@ -75,6 +78,19 @@ fn transaction_to_operation(transaction: Transaction) -> Operation { } } +fn decode_voting_extensions( + extensions: Vec<(MalachiteAddress, Bytes)>, +) -> Result> { + extensions + .into_iter() + .map(|(address, bytes)| { + VotingExtension::decode_all(&mut bytes.as_ref()) + .map(|extension| (address, extension)) + .map_err(|e| anyhow!("decoding voting extension from {address}: {e}")) + }) + .collect() +} + /// Inputs the externalities need to satisfy the [`ethexe_malachite_core::Externalities`] /// contract. Constructed by [`crate::MalachiteService::new`] and /// handed to the inner ethexe-malachite-core service inside an [`Arc`]. @@ -190,6 +206,7 @@ impl Externalities for EthexeExternalities { &self, mb_hash: H256, cert: ethexe_malachite_core::CommitCertificate, + extensions: Vec<(MalachiteAddress, Bytes)>, ) -> Result<()> { let compact = self.db.mb_compact_block(mb_hash).ok_or_else(|| { anyhow!( @@ -226,6 +243,13 @@ impl Externalities for EthexeExternalities { mb_hash, signatures: cert.signatures, }; + let voting_extensions = decode_voting_extensions(extensions)?; + if !voting_extensions.is_empty() { + info!( + validators = voting_extensions.len(), + "process_mb_finalized: received voting extensions", + ); + } // Same prerequisite as the matching BlockProposal — by the // time `process_mb_finalized` runs, `process_mb_proposal` has // already populated `mb_meta(block_hash).last_advanced_eb`. @@ -241,6 +265,35 @@ impl Externalities for EthexeExternalities { Ok(()) } + async fn extend_vote(&self, _mb_hash: H256, mb: Block) -> Result> { + let payload = Operations::decode_all(&mut mb.payload.as_ref()) + .map_err(|e| anyhow!("decoding Operations for voting extension: {e}"))?; + + let has_shielded = payload + .iter() + .any(|op| matches!(op, Operation::Shielded(_))); + if !has_shielded { + return Ok(None); + } + + Ok(Some(Bytes::from(VotingExtension::default().encode()))) + } + + async fn verify_vote_extension( + &self, + _mb_hash: H256, + _mb: Block, + extension: Bytes, + ) -> Result { + match VotingExtension::decode_all(&mut extension.as_ref()) { + Ok(_) => Ok(true), + Err(e) => { + warn!(error = %e, "verify_vote_extension: undecodable extension"); + Ok(false) + } + } + } + async fn build_block_above(&self, parent_mb_hash: H256) -> Result { // `parent_hash` is the consensus envelope hash of the parent // (zero for genesis). Use it directly to seed the producer's @@ -896,7 +949,7 @@ mod tests { let mb_hash = block.hash(); ext.process_mb_proposal(mb_hash, block).await.unwrap(); let _ = rx.recv().await; // BlockProposal - ext.process_mb_finalized(mb_hash, fake_cert(1)) + ext.process_mb_finalized(mb_hash, fake_cert(1), Vec::new()) .await .unwrap(); assert_eq!(db.globals().latest_finalized_mb_hash, mb_hash); @@ -934,7 +987,7 @@ mod tests { let mb_hash = block.hash(); ext_a.process_mb_proposal(mb_hash, block).await.unwrap(); ext_a - .process_mb_finalized(mb_hash, fake_cert(i)) + .process_mb_finalized(mb_hash, fake_cert(i), Vec::new()) .await .unwrap(); chain.push((mb_hash, p)); @@ -966,7 +1019,10 @@ mod tests { let mb4 = block4.hash(); ext_b.process_mb_proposal(mb4, block4).await.unwrap(); let _ = rx_b.recv().await; // proposal - ext_b.process_mb_finalized(mb4, fake_cert(4)).await.unwrap(); + ext_b + .process_mb_finalized(mb4, fake_cert(4), Vec::new()) + .await + .unwrap(); assert_eq!(db.mb_compact_block(mb4).unwrap().parent, last_pre); assert_eq!(db.globals().latest_finalized_mb_hash, mb4); } @@ -992,7 +1048,7 @@ mod tests { let block = wrap(p.clone(), height, parent); let mb_hash = block.hash(); ext.process_mb_proposal(mb_hash, block).await.unwrap(); - ext.process_mb_finalized(mb_hash, fake_cert(height)) + ext.process_mb_finalized(mb_hash, fake_cert(height), Vec::new()) .await .unwrap(); chain.push(mb_hash); @@ -1241,6 +1297,7 @@ mod tests { block_hash: mb_hash, signatures: vec![], }, + Vec::new(), ) .await .unwrap(); From 6c5f9dda1d7328f5946b6b3b60b0fec236f22246 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 15 Jun 2026 17:17:31 +0300 Subject: [PATCH 03/41] intermediate changes --- ethexe/common/src/injected.rs | 10 ++++++- ethexe/common/src/malachite.rs | 28 +++++++++++++++++-- ethexe/malachite/service/src/externalities.rs | 8 +++++- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index 7a3188bab5e..88f486b8ed6 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -116,7 +116,7 @@ impl InjectedTransaction { } /// Returns the hash of [`InjectedTransaction`]. - pub fn to_hash(&self) -> HashOf { + pub fn to_hash(&self) -> HashOf { let hashable_bytes = self.to_hashable_bytes(); unsafe { HashOf::new(gear_core::utils::hash(hashable_bytes.as_ref()).into()) } } @@ -433,6 +433,14 @@ pub struct ShieldedTransaction { pub salt: LimitedVec, } +#[cfg(feature = "shielded")] +impl ShieldedTransaction { + /// Constructs blake2b hash over [ShieldedTransaction]. + pub fn to_hash(&self) -> HashOf { + todo!() + } +} + #[cfg(feature = "shielded")] pub type SignedShieldedTransaction = SignedMessage; diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index 8b4b0a11660..2fc8c93a2cb 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -27,7 +27,7 @@ //! `ethexe-malachite`) so `ethexe-processor` can accept them without //! depending on the consensus layer. -use crate::injected::SignedInjectedTransaction; +use crate::injected::{ShieldedTransaction, SignedInjectedTransaction}; use alloc::vec::Vec; use derive_more::{Deref, DerefMut, IntoIterator}; use gprimitives::H256; @@ -80,6 +80,14 @@ impl Operation { Self::Shielded(_) => 4, } } + + /// Returns `Some` if `Self` contains shielded transaction. + pub fn as_shielded(&self) -> Option<&SignedShieldedTransaction> { + match self { + Self::Shielded(tx) => Some(tx), + _ => None, + } + } } // Custom encoder/decoder so the discriminant is always a fixed-width `u32` @@ -161,9 +169,25 @@ pub struct VotingExtension { #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct VotingDecryptionShare { - pub tx_hash: H256, + pub tx_hash: HashOf, pub share: DecryptionShareSimple, } + +#[cfg(feature = "shielded")] +impl VotingDecryptionShare { + pub fn from_shielded_tx(shielded_tx: &ShieldedTransaction) -> gear_tdec::Result { + let ciphertext_header = shielded_tx.ciphertext.header()?; + let share = DecryptionShareSimple::create( + validator_decryption_key, + private_key_share, + &ciphertext_header, + shielded_tx.aad.as_ref(), + )?; + let tx_hash = shielded_tx.to_hash(); + Ok(Self { tx_hash, share }) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 4b80f836b7b..3c8c572010d 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -49,7 +49,7 @@ use ethexe_common::{ MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, db::{CompactMb, GlobalsStorageRO, GlobalsStorageRW, MbStorageRO, MbStorageRW}, injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, Transaction}, - malachite::{Operation, Operations, VotingExtension}, + malachite::{Operation, Operations, VotingDecryptionShare, VotingExtension}, }; use ethexe_db::Database; use ethexe_malachite_core::{ @@ -269,6 +269,12 @@ impl Externalities for EthexeExternalities { let payload = Operations::decode_all(&mut mb.payload.as_ref()) .map_err(|e| anyhow!("decoding Operations for voting extension: {e}"))?; + let shares = payload + .iter() + .filter_map(|op| op.as_shielded()) + .map(|shielded_tx| { + let share = VotingDecryptionShare::from_shielded_tx(shielded_tx.data()); + }); let has_shielded = payload .iter() .any(|op| matches!(op, Operation::Shielded(_))); From cf55e18078134e96fd9cb71c89ea7f22b2c4e1bf Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 15 Jun 2026 17:52:26 +0300 Subject: [PATCH 04/41] ai generated tdec store for gsigner --- Cargo.lock | 3 + protocol/gsigner/Cargo.toml | 5 + protocol/gsigner/src/lib.rs | 17 ++ protocol/gsigner/src/tdec.rs | 309 +++++++++++++++++++++++++++++++++++ 4 files changed, 334 insertions(+) create mode 100644 protocol/gsigner/src/tdec.rs diff --git a/Cargo.lock b/Cargo.lock index 497d7235f2c..4ceb596f95d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6280,6 +6280,7 @@ dependencies = [ "ark-serialize 0.6.0", "ark-std 0.6.0", "bincode", + "const-hex", "generic-array 0.14.7", "rand 0.8.5", "serde", @@ -8600,6 +8601,8 @@ dependencies = [ "colored", "derive_more 2.1.1", "dirs", + "ferveo-gear-common", + "ferveo-gear-tdec", "gear-workspace-hack", "gprimitives", "hex", diff --git a/protocol/gsigner/Cargo.toml b/protocol/gsigner/Cargo.toml index b4d6a59e677..1559ac2253b 100644 --- a/protocol/gsigner/Cargo.toml +++ b/protocol/gsigner/Cargo.toml @@ -32,6 +32,10 @@ k256 = { version = "0.13.4", default-features = false, features = [ ], optional = true } nacl = { workspace = true, optional = true } dirs = { workspace = true, optional = true } +ferveo-common = { package = "ferveo-gear-common", git = "https://github.com/gear-tech/ferveo-nucypher.git", branch = "more-codec", features = [ + "ark-serde-hex", +], optional = true } +gear-tdec = { workspace = true, optional = true } parity-scale-codec = { workspace = true, default-features = false, features = [ "derive", ], optional = true } @@ -106,6 +110,7 @@ codec = ["dep:parity-scale-codec", "dep:scale-info"] keyring = ["std", "serde", "dep:nacl"] serde = ["dep:serde"] peer-id = ["dep:libp2p-identity"] +tdec = ["std", "keyring", "serde", "dep:ferveo-common", "dep:gear-tdec"] [package.metadata.cargo-shear] # we need it for applying full_crypto feature diff --git a/protocol/gsigner/src/lib.rs b/protocol/gsigner/src/lib.rs index b65469de14e..e7278a0a4f0 100644 --- a/protocol/gsigner/src/lib.rs +++ b/protocol/gsigner/src/lib.rs @@ -52,6 +52,13 @@ pub mod scheme; pub mod schemes; #[cfg(all(feature = "std", feature = "keyring", feature = "serde"))] pub mod signer; +#[cfg(all( + feature = "std", + feature = "keyring", + feature = "serde", + feature = "tdec" +))] +pub mod tdec; pub mod utils; #[cfg(feature = "cli")] @@ -84,6 +91,16 @@ pub use scheme::KeystoreOps; pub use signer::Signer; #[cfg(all(feature = "std", feature = "keyring"))] pub use storage::{FilesystemBackend, MemoryBackend, StorageBackend, StorageError, StorageResult}; +#[cfg(all( + feature = "std", + feature = "keyring", + feature = "serde", + feature = "tdec" +))] +pub use tdec::{ + TdecBlindedKeyShare, TdecCiphertextHeader, TdecDecryptionKey, TdecDecryptionShare, + TdecKeyStore, TdecKeypair, TdecKeystore, TdecPublicDecryptionContext, TdecPublicKey, +}; #[cfg(feature = "secp256k1")] pub use schemes::secp256k1::{ diff --git a/protocol/gsigner/src/tdec.rs b/protocol/gsigner/src/tdec.rs new file mode 100644 index 00000000000..453f6845349 --- /dev/null +++ b/protocol/gsigner/src/tdec.rs @@ -0,0 +1,309 @@ +// Copyright (C) Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Threshold-decryption key storage. +//! +//! This module stores validator threshold-decryption private material separately +//! from signing schemes. It intentionally does not implement [`crate::CryptoScheme`]: +//! these keys create decryption shares, not signatures. + +use crate::{ + error::{Result, SignerError}, + keyring::{self, KeystoreEntry}, +}; +use ferveo_common::{Keypair, PublicKey, from_bytes, to_bytes}; +use gear_tdec::{ + BlindedKeyShare, CiphertextHeader, DecryptionShareSimple, DomainPoint, + PublicDecryptionContextSimple, bls12_381::E, +}; +use hex::ToHex; +use serde::{Deserialize, Serialize}; +use std::{ + fmt, + path::PathBuf, + sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, +}; +use tempfile::TempDir; + +pub type TdecPublicKey = PublicKey; +pub type TdecKeypair = Keypair; +pub type TdecDecryptionKey = DomainPoint; +pub type TdecBlindedKeyShare = BlindedKeyShare; +pub type TdecCiphertextHeader = CiphertextHeader; +pub type TdecDecryptionShare = DecryptionShareSimple; +pub type TdecPublicDecryptionContext = PublicDecryptionContextSimple; + +const NAMESPACE_TDEC: &str = "tdec"; + +/// JSON keystore entry for one validator threshold-decryption key. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TdecKeystore { + pub name: String, + pub public_key: String, + pub validator_decryption_key: String, +} + +impl TdecKeystore { + fn from_keypair(name: &str, keypair: &TdecKeypair) -> Result { + Ok(Self { + name: name.to_string(), + public_key: encode_public_key(&keypair.public_key())?, + validator_decryption_key: encode_decryption_key(&keypair.decryption_key)?, + }) + } + + fn public_key(&self) -> Result { + decode_public_key(&self.public_key) + } + + fn keypair(&self) -> Result { + Ok(TdecKeypair { + decryption_key: decode_decryption_key(&self.validator_decryption_key)?, + }) + } +} + +impl KeystoreEntry for TdecKeystore { + fn name(&self) -> &str { + &self.name + } + + fn set_name(&mut self, name: &str) { + self.name = name.to_string(); + } +} + +/// Store for validator threshold-decryption keys. +#[derive(Clone)] +pub struct TdecKeyStore { + keyring: Arc>>, + _tmp_dir: Option>, +} + +impl fmt::Debug for TdecKeyStore { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TdecKeyStore") + .field("keys", &self.list_public_keys().ok()) + .finish() + } +} + +impl TdecKeyStore { + pub fn new(keyring: keyring::Keyring) -> Self { + Self { + keyring: Arc::new(RwLock::new(keyring)), + _tmp_dir: None, + } + } + + fn with_tempdir(keyring: keyring::Keyring, tmp_dir: Option) -> Self { + Self { + keyring: Arc::new(RwLock::new(keyring)), + _tmp_dir: tmp_dir.map(Arc::new), + } + } + + pub fn memory() -> Self { + let keyring = keyring::Keyring::try_memory().expect("memory keyring should not fail"); + Self::new(keyring) + } + + pub fn fs(path: PathBuf) -> Result { + let keyring = keyring::Keyring::load(Self::namespaced_path(path))?; + Ok(Self::new(keyring)) + } + + pub fn fs_temporary() -> Result { + let temp_dir = tempfile::tempdir()?; + let keyring = keyring::Keyring::load(Self::namespaced_path(temp_dir.path().to_path_buf()))?; + Ok(Self::with_tempdir(keyring, Some(temp_dir))) + } + + pub fn namespaced_path(path: PathBuf) -> PathBuf { + keyring::Keyring::::namespaced_path(path, NAMESPACE_TDEC) + } + + fn keyring(&self) -> Result>> { + self.keyring + .read() + .map_err(|err| SignerError::Other(format!("Failed to acquire read lock: {err}"))) + } + + fn keyring_mut(&self) -> Result>> { + self.keyring + .write() + .map_err(|err| SignerError::Other(format!("Failed to acquire write lock: {err}"))) + } + + fn key_name(public_key: &TdecPublicKey) -> Result { + Ok(format!( + "key-{}", + public_key + .to_bytes() + .map_err(|err| SignerError::Serialization(err.to_string()))? + .encode_hex::() + )) + } + + /// Store a validator decryption scalar and return its public key. + pub fn import_decryption_key( + &self, + validator_decryption_key: TdecDecryptionKey, + ) -> Result { + let keypair = TdecKeypair { + decryption_key: validator_decryption_key, + }; + self.import_keypair(keypair) + } + + /// Store a full tdec keypair and return its public key. + pub fn import_keypair(&self, keypair: TdecKeypair) -> Result { + let public_key = keypair.public_key(); + let name = Self::key_name(&public_key)?; + let keystore = TdecKeystore::from_keypair(&name, &keypair)?; + self.keyring_mut()?.store(&name, keystore)?; + Ok(public_key) + } + + /// Get the private validator decryption scalar by public key. + pub fn validator_decryption_key( + &self, + public_key: &TdecPublicKey, + ) -> Result { + Ok(self.keypair(public_key)?.decryption_key) + } + + /// Get the full tdec keypair by public key. + pub fn keypair(&self, public_key: &TdecPublicKey) -> Result { + let storage = self.keyring()?; + for keystore in storage.list() { + if keystore.public_key()? == *public_key { + return keystore.keypair(); + } + } + Err(SignerError::KeyNotFound(format!("{public_key}"))) + } + + /// Create a decryption share using the local private key matching + /// `public_context.validator_public_key`. + pub fn create_share( + &self, + public_context: &TdecPublicDecryptionContext, + ciphertext_header: &TdecCiphertextHeader, + aad: &[u8], + ) -> Result { + self.create_share_with_blinded_key( + &public_context.validator_public_key, + &public_context.blinded_key_share, + ciphertext_header, + aad, + ) + } + + /// Create a decryption share from explicit public key + blinded key share. + pub fn create_share_with_blinded_key( + &self, + public_key: &TdecPublicKey, + blinded_key_share: &TdecBlindedKeyShare, + ciphertext_header: &TdecCiphertextHeader, + aad: &[u8], + ) -> Result { + let keypair = self.keypair(public_key)?; + blinded_key_share + .create_decryption_share_simple(ciphertext_header, aad, &keypair) + .map_err(|err| SignerError::Crypto(err.to_string())) + } + + pub fn has_key(&self, public_key: &TdecPublicKey) -> Result { + let storage = self.keyring()?; + for keystore in storage.list() { + if keystore.public_key()? == *public_key { + return Ok(true); + } + } + Ok(false) + } + + pub fn list_public_keys(&self) -> Result> { + self.keyring()? + .list() + .iter() + .map(TdecKeystore::public_key) + .collect() + } + + pub fn clear_keys(&self) -> Result<()> { + let mut storage = self.keyring_mut()?; + let names: Vec = storage + .list() + .iter() + .map(|keystore| keystore.name().to_string()) + .collect(); + for name in names { + storage.remove(&name)?; + } + Ok(()) + } +} + +fn encode_public_key(public_key: &TdecPublicKey) -> Result { + Ok(hex::encode(public_key.to_bytes().map_err(|err| { + SignerError::Serialization(err.to_string()) + })?)) +} + +fn decode_public_key(encoded: &str) -> Result { + let bytes = hex::decode(encoded)?; + TdecPublicKey::from_bytes(&bytes).map_err(|err| SignerError::InvalidKey(err.to_string())) +} + +fn encode_decryption_key(key: &TdecDecryptionKey) -> Result { + Ok(hex::encode(to_bytes(key).map_err(|err| { + SignerError::Serialization(err.to_string()) + })?)) +} + +fn decode_decryption_key(encoded: &str) -> Result { + let bytes = hex::decode(encoded)?; + from_bytes(&bytes).map_err(|err| SignerError::InvalidKey(err.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn imports_and_gets_validator_decryption_key_by_public_key() { + let mut rng = gear_tdec::rand_utils::test_rng(); + let keypair = TdecKeypair::new(&mut rng); + let store = TdecKeyStore::memory(); + + let public_key = store.import_keypair(keypair).unwrap(); + assert!(store.has_key(&public_key).unwrap()); + assert_eq!( + store.validator_decryption_key(&public_key).unwrap(), + keypair.decryption_key + ); + } + + #[test] + fn creates_decryption_share_from_public_context() { + let mut rng = gear_tdec::rand_utils::test_rng(); + let dealer = gear_tdec::deal::(3, 2, &mut rng); + let context = dealer.private_contexts[0].clone(); + let public_context = context.public_decryption_contexts[context.index].clone(); + let ciphertext = + gear_tdec::encrypt_raw::(b"hello", b"aad", &dealer.public_key, &mut rng).unwrap(); + let header = ciphertext.header().unwrap(); + let store = TdecKeyStore::memory(); + store + .import_decryption_key(context.validator_decryption_key) + .unwrap(); + + let expected = context.create_share(&header, b"aad").unwrap(); + let actual = store + .create_share(&public_context, &header, b"aad") + .unwrap(); + assert_eq!(actual, expected); + } +} From 73a7745616819509605babb2b6bde12d85b6448d Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 15 Jun 2026 18:12:55 +0300 Subject: [PATCH 05/41] rename TdecKeystore to TdecKeyEntry --- ethexe/common/src/malachite.rs | 33 ++--- ethexe/compute/src/compute.rs | 2 +- ethexe/malachite/service/src/externalities.rs | 8 +- protocol/gsigner/src/lib.rs | 2 +- protocol/gsigner/src/tdec.rs | 118 ++++++++++++------ 5 files changed, 100 insertions(+), 63 deletions(-) diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index 2fc8c93a2cb..0e22348b6cc 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -27,7 +27,7 @@ //! `ethexe-malachite`) so `ethexe-processor` can accept them without //! depending on the consensus layer. -use crate::injected::{ShieldedTransaction, SignedInjectedTransaction}; +use crate::injected::SignedInjectedTransaction; use alloc::vec::Vec; use derive_more::{Deref, DerefMut, IntoIterator}; use gprimitives::H256; @@ -82,6 +82,7 @@ impl Operation { } /// Returns `Some` if `Self` contains shielded transaction. + #[cfg(feature = "shielded")] pub fn as_shielded(&self) -> Option<&SignedShieldedTransaction> { match self { Self::Shielded(tx) => Some(tx), @@ -169,24 +170,24 @@ pub struct VotingExtension { #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct VotingDecryptionShare { - pub tx_hash: HashOf, + pub tx_hash: H256, pub share: DecryptionShareSimple, } -#[cfg(feature = "shielded")] -impl VotingDecryptionShare { - pub fn from_shielded_tx(shielded_tx: &ShieldedTransaction) -> gear_tdec::Result { - let ciphertext_header = shielded_tx.ciphertext.header()?; - let share = DecryptionShareSimple::create( - validator_decryption_key, - private_key_share, - &ciphertext_header, - shielded_tx.aad.as_ref(), - )?; - let tx_hash = shielded_tx.to_hash(); - Ok(Self { tx_hash, share }) - } -} +// #[cfg(feature = "shielded")] +// impl VotingDecryptionShare { +// pub fn from_shielded_tx(shielded_tx: &ShieldedTransaction) -> gear_tdec::Result { +// let ciphertext_header = shielded_tx.ciphertext.header()?; +// let share = DecryptionShareSimple::create( +// validator_decryption_key, +// private_key_share, +// &ciphertext_header, +// shielded_tx.aad.as_ref(), +// )?; +// let tx_hash = shielded_tx.to_hash(); +// Ok(Self { tx_hash, share }) +// } +// } #[cfg(test)] mod tests { diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 1591b11c74b..debb3746c18 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -289,7 +289,7 @@ fn build_executable_data( } Operation::Shielded(shielded) => { let _verified = shielded.into_verified(); - }, + } Operation::ProgressTasks => {} Operation::ProcessQueues { gas_allowance: op_gas_allowance, diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 3c8c572010d..4b80f836b7b 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -49,7 +49,7 @@ use ethexe_common::{ MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, db::{CompactMb, GlobalsStorageRO, GlobalsStorageRW, MbStorageRO, MbStorageRW}, injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, Transaction}, - malachite::{Operation, Operations, VotingDecryptionShare, VotingExtension}, + malachite::{Operation, Operations, VotingExtension}, }; use ethexe_db::Database; use ethexe_malachite_core::{ @@ -269,12 +269,6 @@ impl Externalities for EthexeExternalities { let payload = Operations::decode_all(&mut mb.payload.as_ref()) .map_err(|e| anyhow!("decoding Operations for voting extension: {e}"))?; - let shares = payload - .iter() - .filter_map(|op| op.as_shielded()) - .map(|shielded_tx| { - let share = VotingDecryptionShare::from_shielded_tx(shielded_tx.data()); - }); let has_shielded = payload .iter() .any(|op| matches!(op, Operation::Shielded(_))); diff --git a/protocol/gsigner/src/lib.rs b/protocol/gsigner/src/lib.rs index e7278a0a4f0..b59670dc238 100644 --- a/protocol/gsigner/src/lib.rs +++ b/protocol/gsigner/src/lib.rs @@ -99,7 +99,7 @@ pub use storage::{FilesystemBackend, MemoryBackend, StorageBackend, StorageError ))] pub use tdec::{ TdecBlindedKeyShare, TdecCiphertextHeader, TdecDecryptionKey, TdecDecryptionShare, - TdecKeyStore, TdecKeypair, TdecKeystore, TdecPublicDecryptionContext, TdecPublicKey, + TdecKeyEntry, TdecKeyStore, TdecKeypair, TdecPublicDecryptionContext, TdecPublicKey, }; #[cfg(feature = "secp256k1")] diff --git a/protocol/gsigner/src/tdec.rs b/protocol/gsigner/src/tdec.rs index 453f6845349..8a41446c757 100644 --- a/protocol/gsigner/src/tdec.rs +++ b/protocol/gsigner/src/tdec.rs @@ -35,15 +35,15 @@ pub type TdecPublicDecryptionContext = PublicDecryptionContextSimple; const NAMESPACE_TDEC: &str = "tdec"; -/// JSON keystore entry for one validator threshold-decryption key. +/// JSON keyring entry for one validator threshold-decryption key. #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TdecKeystore { +pub struct TdecKeyEntry { pub name: String, pub public_key: String, pub validator_decryption_key: String, } -impl TdecKeystore { +impl TdecKeyEntry { fn from_keypair(name: &str, keypair: &TdecKeypair) -> Result { Ok(Self { name: name.to_string(), @@ -63,7 +63,7 @@ impl TdecKeystore { } } -impl KeystoreEntry for TdecKeystore { +impl KeystoreEntry for TdecKeyEntry { fn name(&self) -> &str { &self.name } @@ -74,9 +74,24 @@ impl KeystoreEntry for TdecKeystore { } /// Store for validator threshold-decryption keys. +/// +/// `TdecKeyStore` keeps only the validator's private decryption scalar and the +/// corresponding public key. It does not store +/// [`gear_tdec::PrivateDecryptionContextSimple`]; callers should keep or obtain +/// [`TdecPublicDecryptionContext`] separately and pass it to [`Self::create_share`]. +/// +/// Typical usage: +/// +/// 1. Import the local validator's `validator_decryption_key` with +/// [`Self::import_decryption_key`]. +/// 2. Receive or load a [`TdecPublicDecryptionContext`] containing +/// `validator_public_key` and `blinded_key_share`. +/// 3. Call [`Self::create_share`] with the public context, ciphertext header, +/// and AAD. The store finds the matching local private scalar by public key +/// and creates a [`TdecDecryptionShare`]. #[derive(Clone)] pub struct TdecKeyStore { - keyring: Arc>>, + keyring: Arc>>, _tmp_dir: Option>, } @@ -89,63 +104,55 @@ impl fmt::Debug for TdecKeyStore { } impl TdecKeyStore { - pub fn new(keyring: keyring::Keyring) -> Self { + /// Create a store from an existing keyring backend. + pub fn new(keyring: keyring::Keyring) -> Self { Self { keyring: Arc::new(RwLock::new(keyring)), _tmp_dir: None, } } - fn with_tempdir(keyring: keyring::Keyring, tmp_dir: Option) -> Self { + fn with_tempdir(keyring: keyring::Keyring, tmp_dir: Option) -> Self { Self { keyring: Arc::new(RwLock::new(keyring)), _tmp_dir: tmp_dir.map(Arc::new), } } + /// Create an in-memory store. + /// + /// This is useful for tests and short-lived processes. Keys are not + /// persisted. pub fn memory() -> Self { let keyring = keyring::Keyring::try_memory().expect("memory keyring should not fail"); Self::new(keyring) } + /// Load or create a filesystem-backed store under the `tdec` namespace. pub fn fs(path: PathBuf) -> Result { let keyring = keyring::Keyring::load(Self::namespaced_path(path))?; Ok(Self::new(keyring)) } + /// Create a temporary filesystem-backed store. + /// + /// The temporary directory is held for the lifetime of the returned store. pub fn fs_temporary() -> Result { let temp_dir = tempfile::tempdir()?; let keyring = keyring::Keyring::load(Self::namespaced_path(temp_dir.path().to_path_buf()))?; Ok(Self::with_tempdir(keyring, Some(temp_dir))) } + /// Return the path used by the TDEC keyring namespace. pub fn namespaced_path(path: PathBuf) -> PathBuf { - keyring::Keyring::::namespaced_path(path, NAMESPACE_TDEC) + keyring::Keyring::::namespaced_path(path, NAMESPACE_TDEC) } - fn keyring(&self) -> Result>> { - self.keyring - .read() - .map_err(|err| SignerError::Other(format!("Failed to acquire read lock: {err}"))) - } - - fn keyring_mut(&self) -> Result>> { - self.keyring - .write() - .map_err(|err| SignerError::Other(format!("Failed to acquire write lock: {err}"))) - } - - fn key_name(public_key: &TdecPublicKey) -> Result { - Ok(format!( - "key-{}", - public_key - .to_bytes() - .map_err(|err| SignerError::Serialization(err.to_string()))? - .encode_hex::() - )) - } - - /// Store a validator decryption scalar and return its public key. + /// Store a validator decryption scalar and return its derived public key. + /// + /// This is the preferred import path when the caller already has the + /// validator private TDEC scalar but does not want to keep a full private + /// decryption context in memory. pub fn import_decryption_key( &self, validator_decryption_key: TdecDecryptionKey, @@ -156,11 +163,13 @@ impl TdecKeyStore { self.import_keypair(keypair) } - /// Store a full tdec keypair and return its public key. + /// Store a full TDEC keypair and return its public key. + /// + /// Only the decryption scalar and public key are persisted. pub fn import_keypair(&self, keypair: TdecKeypair) -> Result { let public_key = keypair.public_key(); let name = Self::key_name(&public_key)?; - let keystore = TdecKeystore::from_keypair(&name, &keypair)?; + let keystore = TdecKeyEntry::from_keypair(&name, &keypair)?; self.keyring_mut()?.store(&name, keystore)?; Ok(public_key) } @@ -173,7 +182,11 @@ impl TdecKeyStore { Ok(self.keypair(public_key)?.decryption_key) } - /// Get the full tdec keypair by public key. + /// Reconstruct the TDEC keypair for the given public key. + /// + /// The keypair is reconstructed from the stored private scalar. This method + /// returns [`SignerError::KeyNotFound`] when the store has no matching + /// public key. pub fn keypair(&self, public_key: &TdecPublicKey) -> Result { let storage = self.keyring()?; for keystore in storage.list() { @@ -184,8 +197,12 @@ impl TdecKeyStore { Err(SignerError::KeyNotFound(format!("{public_key}"))) } - /// Create a decryption share using the local private key matching - /// `public_context.validator_public_key`. + /// Create a decryption share for a public decryption context. + /// + /// The store uses `public_context.validator_public_key` to find the local + /// validator private scalar, combines it with + /// `public_context.blinded_key_share`, and delegates share creation to + /// `gear-tdec`. pub fn create_share( &self, public_context: &TdecPublicDecryptionContext, @@ -200,7 +217,10 @@ impl TdecKeyStore { ) } - /// Create a decryption share from explicit public key + blinded key share. + /// Create a decryption share from an explicit public key and blinded share. + /// + /// Use this when the caller already split the fields out of a public + /// decryption context. pub fn create_share_with_blinded_key( &self, public_key: &TdecPublicKey, @@ -214,6 +234,7 @@ impl TdecKeyStore { .map_err(|err| SignerError::Crypto(err.to_string())) } + /// Return whether the store contains a key for the given public key. pub fn has_key(&self, public_key: &TdecPublicKey) -> Result { let storage = self.keyring()?; for keystore in storage.list() { @@ -224,14 +245,16 @@ impl TdecKeyStore { Ok(false) } + /// List all public TDEC keys known by the store. pub fn list_public_keys(&self) -> Result> { self.keyring()? .list() .iter() - .map(TdecKeystore::public_key) + .map(TdecKeyEntry::public_key) .collect() } + /// Remove all TDEC keys from the store. pub fn clear_keys(&self) -> Result<()> { let mut storage = self.keyring_mut()?; let names: Vec = storage @@ -244,6 +267,25 @@ impl TdecKeyStore { } Ok(()) } + + fn keyring(&self) -> Result>> { + self.keyring + .read() + .map_err(|err| SignerError::Other(format!("Failed to acquire read lock: {err}"))) + } + + fn keyring_mut(&self) -> Result>> { + self.keyring + .write() + .map_err(|err| SignerError::Other(format!("Failed to acquire write lock: {err}"))) + } + + fn key_name(public_key: &TdecPublicKey) -> Result { + let key_bytes = public_key + .to_bytes() + .map_err(|err| SignerError::Serialization(err.to_string()))?; + Ok(format!("key-{}", key_bytes.encode_hex::())) + } } fn encode_public_key(public_key: &TdecPublicKey) -> Result { From b66081252ef0ca64735e48b2fe82abb8edb8fc83 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 16 Jun 2026 13:10:53 +0300 Subject: [PATCH 06/41] chore: implement TransactionRef type --- ethexe/common/src/injected.rs | 77 ++++++++------ ethexe/malachite/service/src/externalities.rs | 49 +++++---- ethexe/malachite/service/src/mempool.rs | 100 ++++++++---------- ethexe/malachite/service/src/tx_validity.rs | 59 ++++++----- .../service/tests/restart_resilience.rs | 11 +- ethexe/network/src/injected.rs | 2 +- ethexe/rpc/src/apis/injected/relay.rs | 2 +- ethexe/rpc/src/apis/injected/server.rs | 2 +- ethexe/service/src/lib.rs | 2 +- 9 files changed, 158 insertions(+), 146 deletions(-) diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index 88f486b8ed6..99aa8ea8049 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -444,6 +444,35 @@ impl ShieldedTransaction { #[cfg(feature = "shielded")] pub type SignedShieldedTransaction = SignedMessage; +#[cfg(feature = "shielded")] +impl ToDigest for ShieldedTransaction { + fn update_hasher(&self, _hasher: &mut sha3::Keccak256) { + todo!("Shielded transaction digest") + } +} + +#[cfg(feature = "shielded")] +impl ShieldedTransaction { + /// Decrypts [Ciphertext] with provided [SharedSecret]. + /// Returns initial [InjectedTransaction]. + pub fn unshield(self, shared_secret: &SharedSecret) -> TdecResult { + let unshielded_fields = + gear_tdec::decrypt(&self.ciphertext, self.aad.as_ref(), shared_secret)?; + + if unshielded_fields.to_digest() != self.aad { + return Err(gear_tdec::Error::CiphertextVerificationFailed); + } + + Ok(InjectedTransaction { + destination: unshielded_fields.destination, + payload: unshielded_fields.payload, + value: unshielded_fields.value, + reference_block: self.reference_block, + salt: self.salt, + }) + } +} + #[cfg(feature = "shielded")] #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[derive(Debug, Clone, Encode, Decode, Eq, PartialEq, derive_more::From)] @@ -454,17 +483,10 @@ pub enum Transaction { #[cfg(feature = "shielded")] impl Transaction { - pub fn hash(&self) -> HashOf { + pub fn as_ref(&self) -> TransactionRef<'_> { match self { - Self::Injected(tx) => tx.data().to_hash(), - Self::Shielded(_) => todo!("Shielded transaction hash"), - } - } - - pub fn reference_block(&self) -> H256 { - match self { - Self::Injected(tx) => tx.data().reference_block, - Self::Shielded(tx) => tx.data().reference_block, + Self::Injected(tx) => TransactionRef::Injected(tx), + Self::Shielded(tx) => TransactionRef::Shielded(tx), } } @@ -484,31 +506,26 @@ impl Transaction { } #[cfg(feature = "shielded")] -impl ToDigest for ShieldedTransaction { - fn update_hasher(&self, _hasher: &mut sha3::Keccak256) { - todo!("Shielded transaction digest") - } +#[derive(Clone, Copy)] +pub enum TransactionRef<'t> { + Injected(&'t SignedInjectedTransaction), + Shielded(&'t SignedShieldedTransaction), } #[cfg(feature = "shielded")] -impl ShieldedTransaction { - /// Decrypts [Ciphertext] with provided [SharedSecret]. - /// Returns initial [InjectedTransaction]. - pub fn unshield(self, shared_secret: &SharedSecret) -> TdecResult { - let unshielded_fields = - gear_tdec::decrypt(&self.ciphertext, self.aad.as_ref(), shared_secret)?; - - if unshielded_fields.to_digest() != self.aad { - return Err(gear_tdec::Error::CiphertextVerificationFailed); +impl<'t> TransactionRef<'t> { + pub fn hash(&self) -> HashOf { + match self { + Self::Injected(tx) => tx.data().to_hash(), + Self::Shielded(_) => todo!("Shielded transaction hash"), } + } - Ok(InjectedTransaction { - destination: unshielded_fields.destination, - payload: unshielded_fields.payload, - value: unshielded_fields.value, - reference_block: self.reference_block, - salt: self.salt, - }) + pub fn reference_block(&self) -> H256 { + match self { + Self::Injected(tx) => tx.data().reference_block, + Self::Shielded(tx) => tx.data().reference_block, + } } } diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 55a20931157..acca6b7781b 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -48,7 +48,7 @@ use bytes::Bytes; use ethexe_common::{ MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, db::{CompactMb, GlobalsStorageRO, GlobalsStorageRW, MbStorageRO, MbStorageRW}, - injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, Transaction}, + injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, Transaction, TransactionRef}, malachite::{Operation, Operations, VotingExtension}, }; use ethexe_db::Database; @@ -64,9 +64,10 @@ use std::{ use tokio::sync::{Notify, mpsc}; use tracing::{debug, error, info, warn}; -fn operation_to_transaction(operation: &Operation) -> Option { +fn operation_to_transaction(operation: &Operation) -> Option> { match operation { - Operation::Injected(tx) => Some(Transaction::Injected(tx.clone())), + Operation::Injected(tx) => Some(TransactionRef::Injected(tx)), + Operation::Shielded(tx) => Some(TransactionRef::Shielded(tx)), _ => None, } } @@ -224,12 +225,12 @@ impl Externalities for EthexeExternalities { // Flush the committed injected txs from the mempool and add // their hashes to the seen-set so a re-gossip can't slip them // back in before they age out. - let injected: Vec = payload + let transactions = payload .iter() .filter_map(operation_to_transaction) - .collect(); - if !injected.is_empty() { - self.mempool.forget(&injected).await; + .collect::>(); + if !transactions.is_empty() { + self.mempool.forget(&transactions).await; } // Advance the canonical pointer downstream consumers @@ -323,11 +324,11 @@ impl Externalities for EthexeExternalities { let checker = TxValidityChecker::new_for_mb(self.db.clone(), head, parent_mb_hash)?; let mut accepted = Vec::with_capacity(injected.len()); for tx in injected { - match checker.check_tx_validity(&tx)? { + match checker.check_tx_validity(tx.as_ref())? { TxValidity::Valid => accepted.push(tx), reason => { warn!( - tx_hash = %tx.hash(), + tx_hash = %tx.as_ref().hash(), ?reason, "build_block_above: dropping injected tx — fails TxValidity", ); @@ -631,7 +632,7 @@ impl Externalities for EthexeExternalities { // is absent from CAS). Every malicious-tx-data path returns // `Ok(TxValidity::)` instead of `Err`, so this `?` // can't be triggered by what the proposer placed in the MB. - match checker.check_tx_validity(&transaction)? { + match checker.check_tx_validity(transaction)? { TxValidity::Valid => {} reason => { warn!( @@ -827,9 +828,9 @@ mod tests { use crate::{MalachiteEvent, mempool::EmptyMempool}; use anyhow::Context; use ethexe_common::{ - BlockHeader, + BlockHeader, HashOf, db::{BlockMetaStorageRW, OnChainStorageRW}, - injected::{PurgedTransaction, SignedInjectedTransaction}, + injected::{InjectedTransaction, PurgedTransaction, SignedInjectedTransaction}, }; fn to_payload(bytes: Vec) -> BlockPayload { @@ -1212,7 +1213,7 @@ mod tests { /// can assert which txs reached the mempool eviction path. #[derive(Default)] struct ForgetTracker { - seen: tokio::sync::Mutex>, + seen: tokio::sync::Mutex>>, } #[async_trait::async_trait] @@ -1228,8 +1229,11 @@ mod tests { async fn fetch(&self, _head: SimpleBlockData) -> Vec { Vec::new() } - async fn forget(&self, committed: &[Transaction]) { - self.seen.lock().await.extend_from_slice(committed); + async fn forget(&self, committed: &[TransactionRef<'_>]) { + self.seen + .lock() + .await + .extend(committed.iter().map(TransactionRef::hash)); } async fn wait_for_new_tx(&self) { std::future::pending().await @@ -1318,10 +1322,9 @@ mod tests { .await .unwrap(); - let seen = tracker.seen.lock().await.clone(); - let seen_hashes: Vec<_> = seen.iter().map(Transaction::hash).collect(); + let seen_hashes = tracker.seen.lock().await.clone(); assert_eq!( - seen.len(), + seen_hashes.len(), 2, "exactly two injected txs should be forgotten" ); @@ -1484,9 +1487,9 @@ mod tests { ) .unwrap(); - mempool.insert(valid.clone()); + mempool.insert(valid.clone().into()); assert_eq!( - mempool.insert(value_tx.clone()), + mempool.insert(value_tx.clone().into()), crate::mempool::TxInsertionStatus::NonZeroValue, ); assert_eq!(mempool.len(), 1); @@ -1555,12 +1558,12 @@ mod tests { let push_start = MAX_TOUCHED_PROGRAMS_PER_MB / 2 + 1; let push_end = MAX_TOUCHED_PROGRAMS_PER_MB + 1; for i in push_start..push_end { - mempool.insert(signed_injected_tx( + mempool.insert(Transaction::Injected(signed_injected_tx( &pk, ActorId::from(i as u64), chain.blocks[9].hash, i as u8, - )); + ))); } let (ext, _rx) = make_externalities_with_pool(db.clone(), mempool); @@ -1703,7 +1706,7 @@ mod tests { }, ) .unwrap(); - mempool.insert(tx); + mempool.insert(tx.into()); } assert_eq!(mempool.len(), 3); diff --git a/ethexe/malachite/service/src/mempool.rs b/ethexe/malachite/service/src/mempool.rs index 43714f394bd..70d5f3fe3d6 100644 --- a/ethexe/malachite/service/src/mempool.rs +++ b/ethexe/malachite/service/src/mempool.rs @@ -41,7 +41,7 @@ use ethexe_common::{ db::{GlobalsStorageRO, InjectedStorageRW, OnChainStorageRO}, injected::{ InjectedTransaction, InjectedTransactionAcceptance, PurgedTransaction, Transaction, - TransactionPurgedReason, VALIDITY_WINDOW, + TransactionPurgedReason, TransactionRef, VALIDITY_WINDOW, }, }; use ethexe_db::Database; @@ -130,7 +130,7 @@ pub trait Mempool: Send + Sync + 'static { async fn fetch(&self, head: SimpleBlockData) -> Vec; /// Drop committed txs and remember their hashes for dedup. - async fn forget(&self, committed: &[Transaction]); + async fn forget(&self, committed: &[TransactionRef<'_>]); /// Best-effort wake-up on new tx; spurious wake-ups allowed. async fn wait_for_new_tx(&self); @@ -158,7 +158,7 @@ impl Mempool for EmptyMempool { Vec::new() } - async fn forget(&self, _committed: &[Transaction]) {} + async fn forget(&self, _committed: &[TransactionRef<'_>]) {} async fn wait_for_new_tx(&self) { std::future::pending().await @@ -214,22 +214,6 @@ impl InjectedTxMempool { self.inner.lock().expect("poisoned mempool").pool.is_empty() } - pub fn insert(&self, tx: impl Into) -> TxInsertionStatus { - ::insert(self, tx.into()) - } - - pub async fn forget(&self, committed: &[T]) - where - T: Clone + Into, - { - let committed = committed - .iter() - .cloned() - .map(Into::into) - .collect::>(); - ::forget(self, &committed).await - } - /// Resolve `reference_block` to its canonical height via the DB. /// Returns `None` if the block isn't in the DB yet. fn ref_block_height(&self, reference_block: H256) -> Option { @@ -286,7 +270,7 @@ impl InjectedTxMempool { }); let mut purged_txs = Vec::new(); inner.pool.retain(|tx_hash, tx| { - let ref_block = tx.reference_block(); + let ref_block = tx.as_ref().reference_block(); match db.block_header(ref_block).map(|h| h.height) { Some(h) if !Self::is_expired(head_height, h) => true, Some(h) => { @@ -320,8 +304,8 @@ impl InjectedTxMempool { #[async_trait] impl Mempool for InjectedTxMempool { fn insert(&self, tx: Transaction) -> TxInsertionStatus { - let tx_hash = tx.hash(); - let ref_block = tx.reference_block(); + let tx_hash = tx.as_ref().hash(); + let ref_block = tx.as_ref().reference_block(); // Reject non-zero-value txs unconditionally (#5083 — value-bearing // injected txs are not supported yet). Done first so a malicious @@ -440,7 +424,7 @@ impl Mempool for InjectedTxMempool { let result: Vec<_> = inner .pool .values() - .filter(|tx| ancestors.contains(&tx.reference_block())) + .filter(|tx| ancestors.contains(&tx.as_ref().reference_block())) .cloned() .collect(); info!( @@ -454,13 +438,13 @@ impl Mempool for InjectedTxMempool { result } - async fn forget(&self, committed: &[Transaction]) { + async fn forget(&self, committed: &[TransactionRef<'_>]) { let mut inner = self.inner.lock().expect("poisoned mempool"); - for tx in committed { - let tx_hash = tx.hash(); + committed.iter().for_each(|tx_ref| { + let tx_hash = tx_ref.hash(); inner.pool.remove(&tx_hash); - inner.seen.insert(tx_hash, tx.reference_block()); - } + inner.seen.insert(tx_hash, tx_ref.reference_block()); + }); } async fn wait_for_new_tx(&self) { @@ -532,7 +516,7 @@ mod tests { let pk = PrivateKey::random(); // Fill to capacity with a valid tx so PoolFull would normally fire. - pool.insert(signed_tx(&pk, ActorId::zero(), chain[1].hash, 0)); + pool.insert(signed_tx(&pk, ActorId::zero(), chain[1].hash, 0).into()); let value_tx = SignedMessage::create( pk.clone(), @@ -546,7 +530,10 @@ mod tests { ) .unwrap(); - assert_eq!(pool.insert(value_tx), TxInsertionStatus::NonZeroValue,); + assert_eq!( + pool.insert(value_tx.into()), + TxInsertionStatus::NonZeroValue, + ); assert_eq!(pool.len(), 1, "non-zero-value tx must not enter the pool"); } @@ -559,7 +546,7 @@ mod tests { let pk = PrivateKey::random(); let tx = signed_tx(&pk, ActorId::zero(), chain[1].hash, 0); - assert_eq!(pool.insert(tx), TxInsertionStatus::Inserted); + assert_eq!(pool.insert(tx.into()), TxInsertionStatus::Inserted); assert_eq!(pool.len(), 1); } @@ -573,8 +560,8 @@ mod tests { let pk = PrivateKey::random(); let tx = signed_tx(&pk, ActorId::zero(), chain[1].hash, 5); - assert_eq!(pool.insert(tx.clone()), TxInsertionStatus::Inserted); - assert_eq!(pool.insert(tx), TxInsertionStatus::AlreadyInPool,); + assert_eq!(pool.insert(tx.clone().into()), TxInsertionStatus::Inserted); + assert_eq!(pool.insert(tx.into()), TxInsertionStatus::AlreadyInPool,); assert_eq!(pool.len(), 1); } @@ -588,11 +575,12 @@ mod tests { let pk = PrivateKey::random(); let tx = signed_tx(&pk, ActorId::zero(), chain[1].hash, 11); - pool.insert(tx.clone()); - futures::executor::block_on(pool.forget(std::slice::from_ref(&tx))); + let transaction: Transaction = tx.clone().into(); + pool.insert(transaction.clone()); + futures::executor::block_on(pool.forget(std::slice::from_ref(&transaction.as_ref()))); assert_eq!(pool.len(), 0); - assert_eq!(pool.insert(tx), TxInsertionStatus::AlreadyIncluded,); + assert_eq!(pool.insert(tx.into()), TxInsertionStatus::AlreadyIncluded,); assert_eq!(pool.len(), 0); } @@ -610,7 +598,7 @@ mod tests { let _ = pool.set_chain_head(chain[head_idx]); let tx = signed_tx(&pk, ActorId::zero(), chain[1].hash, 0); - assert_eq!(pool.insert(tx), TxInsertionStatus::ExpiredRefBlock,); + assert_eq!(pool.insert(tx.into()), TxInsertionStatus::ExpiredRefBlock,); assert_eq!(pool.len(), 0); } @@ -663,7 +651,7 @@ mod tests { let db = Database::memory(); let pool = InjectedTxMempool::new(db); let pk = PrivateKey::random(); - let tx = signed_tx(&pk, ActorId::zero(), H256::random(), 1); + let tx = signed_tx(&pk, ActorId::zero(), H256::random(), 1).into(); pool.insert(tx); assert_eq!(pool.len(), 1); } @@ -675,8 +663,8 @@ mod tests { let pool = InjectedTxMempool::new(db); let pk = PrivateKey::random(); - let tx = signed_tx(&pk, ActorId::zero(), chain[2].hash, 1); - let tx_hash = tx.data().to_hash(); + let tx: Transaction = signed_tx(&pk, ActorId::zero(), chain[2].hash, 1).into(); + let tx_hash = tx.as_ref().hash(); pool.insert(tx.clone()); assert_eq!(pool.len(), 1); @@ -703,10 +691,10 @@ mod tests { let pool = InjectedTxMempool::with_capacity(db, 2); let pk = PrivateKey::random(); - pool.insert(signed_tx(&pk, ActorId::zero(), chain[1].hash, 0)); - pool.insert(signed_tx(&pk, ActorId::zero(), chain[1].hash, 1)); + pool.insert(signed_tx(&pk, ActorId::zero(), chain[1].hash, 0).into()); + pool.insert(signed_tx(&pk, ActorId::zero(), chain[1].hash, 1).into()); assert_eq!( - pool.insert(signed_tx(&pk, ActorId::zero(), chain[1].hash, 2)), + pool.insert(signed_tx(&pk, ActorId::zero(), chain[1].hash, 2).into()), TxInsertionStatus::PoolFull, ); assert_eq!(pool.len(), 2, "third insert must hit the capacity cap"); @@ -724,7 +712,7 @@ mod tests { // 100 txs each anchored at a random ref_block NOT in our DB. for salt in 0..100u8 { let bogus_ref_block = H256::random(); - pool.insert(signed_tx(&pk, ActorId::zero(), bogus_ref_block, salt)); + pool.insert(signed_tx(&pk, ActorId::zero(), bogus_ref_block, salt).into()); } assert_eq!(pool.len(), 100); @@ -754,7 +742,7 @@ mod tests { let pk = PrivateKey::random(); // tx anchored at block 1 — height 1 - let tx = signed_tx(&pk, ActorId::zero(), chain[1].hash, 0); + let tx: Transaction = signed_tx(&pk, ActorId::zero(), chain[1].hash, 0).into(); pool.insert(tx); assert_eq!(pool.len(), 1); @@ -776,11 +764,11 @@ mod tests { let pool = InjectedTxMempool::new(db); let pk = PrivateKey::random(); - let tx = signed_tx(&pk, ActorId::zero(), chain[1].hash, 99); + let tx: Transaction = signed_tx(&pk, ActorId::zero(), chain[1].hash, 99).into(); pool.insert(tx.clone()); assert_eq!(pool.len(), 1); - futures::executor::block_on(pool.forget(std::slice::from_ref(&tx))); + futures::executor::block_on(pool.forget(std::slice::from_ref(&tx.as_ref()))); assert_eq!(pool.len(), 0); // Re-inserting the same tx is a seen-hash no-op. @@ -814,7 +802,7 @@ mod tests { let pk = PrivateKey::random(); // tx anchored to the ALT branch - let tx_alt = signed_tx(&pk, ActorId::zero(), alt_hash, 1); + let tx_alt: Transaction = signed_tx(&pk, ActorId::zero(), alt_hash, 1).into(); pool.insert(tx_alt); assert_eq!(pool.len(), 1); @@ -847,7 +835,7 @@ mod tests { tokio::time::sleep(Duration::from_millis(10)).await; let pk = PrivateKey::random(); - pool.insert(signed_tx(&pk, ActorId::zero(), chain[1].hash, 0)); + pool.insert(signed_tx(&pk, ActorId::zero(), chain[1].hash, 0).into()); // Waiter should now wake up promptly. tokio::time::timeout(Duration::from_secs(1), waiter) @@ -865,7 +853,7 @@ mod tests { let chain = linear_chain(&db, 2); let pool = std::sync::Arc::new(InjectedTxMempool::new(db)); let pk = PrivateKey::random(); - let tx = signed_tx(&pk, ActorId::zero(), chain[1].hash, 0); + let tx: Transaction = signed_tx(&pk, ActorId::zero(), chain[1].hash, 0).into(); // Seed one accepted insert and consume the resulting permit so // the next `.notified()` re-blocks until the next signal. @@ -973,11 +961,11 @@ mod tests { let pk = PrivateKey::random(); // Track inserted (and not-yet-forgotten) txs so Forget // can target a real entry. - let mut live: Vec = Vec::new(); + let mut live: Vec = Vec::new(); for action in actions { match action { Action::Insert { ref_idx, salt } => { - let tx = signed_tx(&pk, ActorId::zero(), chain[ref_idx].hash, salt); + let tx: Transaction = signed_tx(&pk, ActorId::zero(), chain[ref_idx].hash, salt).into(); // Only track txs that actually entered the pool — // `AlreadyInPool` / `AlreadyIncluded` / capacity // rejects must not feed `live`, otherwise Forget @@ -990,7 +978,7 @@ mod tests { if !live.is_empty() { let idx = which % live.len(); let victim = live.swap_remove(idx); - futures::executor::block_on(pool.forget(std::slice::from_ref(&victim))); + futures::executor::block_on(pool.forget(std::slice::from_ref(&victim.as_ref()))); } } } @@ -1036,7 +1024,7 @@ mod tests { // Inserts: alternating canonical-tail and alt anchors. for i in 0..n_txs { let anchor = if i % 2 == 0 { chain[3].hash } else { alt_hash }; - pool.insert(signed_tx(&pk, ActorId::zero(), anchor, i as u8)); + pool.insert(signed_tx(&pk, ActorId::zero(), anchor, i as u8).into()); } let head = chain[3]; @@ -1065,10 +1053,10 @@ mod tests { let chain = linear_chain_seeded(&db, 2, seed); let pool = InjectedTxMempool::new(db); let pk = PrivateKey::random(); - let tx = signed_tx(&pk, ActorId::zero(), chain[1].hash, salt); + let tx: Transaction = signed_tx(&pk, ActorId::zero(), chain[1].hash, salt).into(); pool.insert(tx.clone()); prop_assert_eq!(pool.len(), 1); - futures::executor::block_on(pool.forget(std::slice::from_ref(&tx))); + futures::executor::block_on(pool.forget(std::slice::from_ref(&tx.as_ref()))); prop_assert_eq!(pool.len(), 0); // Re-insert: idempotent no-op because the hash sits in // the seen-set and `reference_block` hasn't aged out. diff --git a/ethexe/malachite/service/src/tx_validity.rs b/ethexe/malachite/service/src/tx_validity.rs index a2dba48546a..0a5280e7fce 100644 --- a/ethexe/malachite/service/src/tx_validity.rs +++ b/ethexe/malachite/service/src/tx_validity.rs @@ -30,7 +30,7 @@ use ethexe_common::{ db::{GlobalsStorageRO, MbStorageRO, OnChainStorageRO}, events::{BlockRequestEvent, RouterRequestEvent, router::ProgramCreatedEvent}, gear::INJECTED_MESSAGE_PANIC_GAS_CHARGE_THRESHOLD, - injected::{InjectedTransaction, Transaction, VALIDITY_WINDOW}, + injected::{InjectedTransaction, TransactionRef, VALIDITY_WINDOW}, malachite::Operation, }; use ethexe_db::Database; @@ -130,14 +130,13 @@ impl TxValidityChecker { } /// Determine [`TxValidity`] for one injected transaction. - pub fn check_tx_validity(&self, tx: &Transaction) -> Result { - // let tx = tx.into(); - let Transaction::Injected(tx) = tx else { + pub fn check_tx_validity(&self, tx: TransactionRef<'_>) -> Result { + let TransactionRef::Injected(injected_tx) = tx else { todo!("Shielded transaction validity"); }; - let reference_block = tx.data().reference_block; + let reference_block = tx.reference_block(); - if tx.data().value != 0 { + if injected_tx.data().value != 0 { return Ok(TxValidity::NonZeroValue); } @@ -149,18 +148,20 @@ impl TxValidityChecker { return Ok(TxValidity::NotOnCurrentBranch); } - if self.recent_included_txs.contains(&tx.data().to_hash()) { + let tx_hash = tx.hash(); + if self.recent_included_txs.contains(&tx_hash) { return Ok(TxValidity::Duplicate); } - let Some(destination_state_hash) = self.latest_states.get(&tx.data().destination) else { + let Some(destination_state_hash) = self.latest_states.get(&injected_tx.data().destination) + else { return Ok(TxValidity::UnknownDestination); }; let Some(state) = self.db.program_state(destination_state_hash.hash) else { anyhow::bail!( "program state not found for actor({}) by valid hash({})", - tx.data().destination, + injected_tx.data().destination, destination_state_hash.hash ); }; @@ -367,7 +368,7 @@ mod tests { MaybeHashOf, PrivateKey, SignedMessage, StateHashWithQueueSize, db::{CompactMb, MbStorageRW, OnChainStorageRW}, gear_core::program::MemoryInfix, - injected::{InjectedTransaction, SignedInjectedTransaction}, + injected::{InjectedTransaction, SignedInjectedTransaction, Transaction}, malachite::Operations, mock::{BlockChain, Mock, Tap}, }; @@ -520,7 +521,7 @@ mod tests { let tx = mock_tx(block.hash); assert_eq!( TxValidity::Valid, - tx_checker.check_tx_validity(&tx).unwrap() + tx_checker.check_tx_validity(tx.as_ref()).unwrap() ); } } @@ -534,13 +535,13 @@ mod tests { let chain_head = chain.blocks[9].to_simple(); let injected_tx = sign_injected_tx(test_injected_transaction(chain_head.hash, ActorId::zero())); - let tx = injected_tx.clone().into(); + let tx = Transaction::Injected(injected_tx.clone()); let parent_mb = setup_mb(&db, vec![injected_tx], true, chain.mb_hash_at(8)); let tx_checker = TxValidityChecker::new_for_mb(db.clone(), chain_head, parent_mb).unwrap(); assert_eq!( TxValidity::Duplicate, - tx_checker.check_tx_validity(&tx).unwrap() + tx_checker.check_tx_validity(tx.as_ref()).unwrap() ); } @@ -563,7 +564,7 @@ mod tests { let tx = mock_tx(block.hash); assert_eq!( TxValidity::Outdated, - tx_checker.check_tx_validity(&tx).unwrap() + tx_checker.check_tx_validity(tx.as_ref()).unwrap() ); } } @@ -594,14 +595,14 @@ mod tests { let tx = mock_tx(block.hash); assert_eq!( TxValidity::NotOnCurrentBranch, - tx_checker.check_tx_validity(&tx).unwrap() + tx_checker.check_tx_validity(tx.as_ref()).unwrap() ); } for block in chain.blocks.iter().rev().take(VALIDITY_WINDOW as usize) { let tx = mock_tx(block.hash); assert_eq!( TxValidity::Valid, - tx_checker.check_tx_validity(&tx).unwrap() + tx_checker.check_tx_validity(tx.as_ref()).unwrap() ); } } @@ -619,7 +620,7 @@ mod tests { assert_eq!( TxValidity::UninitializedDestination, - tx_checker.check_tx_validity(&tx).unwrap() + tx_checker.check_tx_validity(tx.as_ref()).unwrap() ); } @@ -632,14 +633,14 @@ mod tests { let chain_head = chain.blocks[9].to_simple(); let tx = test_injected_transaction(chain.blocks[5].hash, ActorId::zero()) .tap_mut(|tx| tx.value = 100); - let tx = sign_injected_tx(tx).into(); + let tx: Transaction = sign_injected_tx(tx).into(); let parent_mb = setup_mb(&db, vec![], true, chain.mb_hash_at(8)); let tx_checker = TxValidityChecker::new_for_mb(db.clone(), chain_head, parent_mb).unwrap(); assert_eq!( TxValidity::NonZeroValue, - tx_checker.check_tx_validity(&tx).unwrap() + tx_checker.check_tx_validity(tx.as_ref()).unwrap() ); } @@ -650,14 +651,14 @@ mod tests { let chain = test_block_chain(10).setup(&db); let chain_head = chain.blocks[9].to_simple(); - let tx = mock_injected_tx().into(); + let tx: Transaction = mock_injected_tx().into(); let parent_mb = setup_mb(&db, vec![], true, chain.mb_hash_at(8)); let tx_checker = TxValidityChecker::new_for_mb(db.clone(), chain_head, parent_mb).unwrap(); assert_eq!( TxValidity::Outdated, - tx_checker.check_tx_validity(&tx).unwrap() + tx_checker.check_tx_validity(tx.as_ref()).unwrap() ); } @@ -686,7 +687,7 @@ mod tests { assert_eq!( TxValidity::NotOnCurrentBranch, - tx_checker.check_tx_validity(&tx).unwrap() + tx_checker.check_tx_validity(tx.as_ref()).unwrap() ); } @@ -712,7 +713,7 @@ mod tests { let checker = TxValidityChecker::new_for_mb(db.clone(), chain_head, parent_mb).unwrap(); let tx = mock_tx(chain.blocks[5].hash); assert_eq!( - checker.check_tx_validity(&tx).unwrap(), + checker.check_tx_validity(tx.as_ref()).unwrap(), TxValidity::InsufficientBalanceForInjectedMessages, ); } @@ -728,7 +729,7 @@ mod tests { .unwrap(); let tx = mock_tx(chain.blocks[1].hash); assert_eq!( - checker.check_tx_validity(&tx).unwrap(), + checker.check_tx_validity(tx.as_ref()).unwrap(), TxValidity::UnknownDestination, ); } @@ -756,7 +757,10 @@ mod tests { let chain_head = chain.blocks[9].to_simple(); let checker = TxValidityChecker::new_for_mb(db.clone(), chain_head, mb_parent).unwrap(); let tx = mock_tx(chain.blocks[5].hash); - assert_eq!(checker.check_tx_validity(&tx).unwrap(), TxValidity::Valid); + assert_eq!( + checker.check_tx_validity(tx.as_ref()).unwrap(), + TxValidity::Valid + ); } /// Pin evaluation order: NonZeroValue short-circuits ahead of all @@ -772,9 +776,10 @@ mod tests { .unwrap(); // value != 0 AND ref_block not in DB. NonZeroValue wins. - let tx = sign_injected_tx(InjectedTransaction::mock(()).tap_mut(|tx| tx.value = 1)).into(); + let tx: Transaction = + sign_injected_tx(InjectedTransaction::mock(()).tap_mut(|tx| tx.value = 1)).into(); assert_eq!( - checker.check_tx_validity(&tx).unwrap(), + checker.check_tx_validity(tx.as_ref()).unwrap(), TxValidity::NonZeroValue, ); } diff --git a/ethexe/malachite/service/tests/restart_resilience.rs b/ethexe/malachite/service/tests/restart_resilience.rs index b2f05cfb06a..5436f7c1943 100644 --- a/ethexe/malachite/service/tests/restart_resilience.rs +++ b/ethexe/malachite/service/tests/restart_resilience.rs @@ -22,7 +22,7 @@ use async_trait::async_trait; use ethexe_common::{ BlockHeader, SimpleBlockData, db::{BlockMetaStorageRW, CompactMb, GlobalsStorageRO, MbStorageRO, OnChainStorageRW}, - injected::{PurgedTransaction, Transaction}, + injected::{PurgedTransaction, Transaction, TransactionRef}, }; use ethexe_db::Database; use ethexe_malachite::{ @@ -52,7 +52,7 @@ impl Mempool for EmptyMempool { Vec::new() } - async fn forget(&self, _committed: &[Transaction]) {} + async fn forget(&self, _committed: &[TransactionRef<'_>]) {} async fn wait_for_new_tx(&self) { std::future::pending().await @@ -116,7 +116,7 @@ fn build_signer(home: &Path) -> (Signer, gsigner::schemes::secp256k1: /// Build the MalachiteConfig used by the resilience tests: /// quarantine-off (so the producer can advance immediately on each -/// new chain head), default listen address, no persistent peers, +/// new chain head), ephemeral listen port, no persistent peers, /// single-validator set so the local node can decide on its own. fn build_config( home: &Path, @@ -211,10 +211,9 @@ async fn single_validator_finalizes_and_recovers_after_restart() { let chain = seed_chain(&db, 64, 0xDEAD_BEEF); let (signer, pub_key) = build_signer(home.path()); - // ---- first run ------------------------------------------------- let mut svc = MalachiteService::new( - build_config(home.path(), 30_001, pub_key), + build_config(home.path(), 0, pub_key), db.clone(), signer.clone(), Some(pub_key), @@ -256,7 +255,7 @@ async fn single_validator_finalizes_and_recovers_after_restart() { // ---- second run on the SAME home dir + DB ---------------------- let mut svc2 = MalachiteService::new( - build_config(home.path(), 30_001, pub_key), + build_config(home.path(), 0, pub_key), db.clone(), signer, Some(pub_key), diff --git a/ethexe/network/src/injected.rs b/ethexe/network/src/injected.rs index 2495c7804aa..5372983b4b6 100644 --- a/ethexe/network/src/injected.rs +++ b/ethexe/network/src/injected.rs @@ -162,7 +162,7 @@ impl Behaviour { identities: &ValidatorIdentities, transaction: Transaction, ) -> Result { - let tx_hash = transaction.hash(); + let tx_hash = transaction.as_ref().hash(); if identities.is_empty() { return Err(SendTransactionError::NoValidatorsFound); diff --git a/ethexe/rpc/src/apis/injected/relay.rs b/ethexe/rpc/src/apis/injected/relay.rs index 3610cd81ceb..52d80e4f7ae 100644 --- a/ethexe/rpc/src/apis/injected/relay.rs +++ b/ethexe/rpc/src/apis/injected/relay.rs @@ -27,7 +27,7 @@ impl TransactionsRelayer { &self, transaction: Transaction, ) -> RpcResult { - let tx_hash = transaction.hash(); + let tx_hash = transaction.as_ref().hash(); tracing::trace!(%tx_hash, ?transaction, "Called injected_sendTransaction with vars"); match &transaction { diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index a05ba08d821..7754fe7d693 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -101,7 +101,7 @@ impl InjectedApi { pending: PendingSubscriptionSink, transaction: Transaction, ) -> SubscriptionResult { - let tx_hash = transaction.hash(); + let tx_hash = transaction.as_ref().hash(); let pending_subscriber = match self.manager.try_register_subscriber(tx_hash) { Ok(subscriber) => subscriber, diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 0e48e984f6a..0ec40b4e28e 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -916,7 +916,7 @@ impl Service { } _ => { // no local malachite or malachite reject transaction, wait for other acceptances - let tx_hash = transaction.hash(); + let tx_hash = transaction.as_ref().hash(); if let Some(pending) = network_injected_txs.get_mut(&tx_hash) { From 3d8be57bfc11a0544231263d19803f28fa6a6d01 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 16 Jun 2026 13:41:28 +0300 Subject: [PATCH 07/41] chore: rename RpcEvent::InjectedTransaction -> RpcEvent::Transaction --- ethexe/rpc/src/apis/injected/relay.rs | 4 ++-- ethexe/rpc/src/lib.rs | 2 +- ethexe/rpc/src/tests.rs | 2 +- ethexe/service/src/lib.rs | 2 +- ethexe/service/src/tests/mod.rs | 2 +- ethexe/service/src/tests/utils/events.rs | 6 +++--- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ethexe/rpc/src/apis/injected/relay.rs b/ethexe/rpc/src/apis/injected/relay.rs index 52d80e4f7ae..0a81019723c 100644 --- a/ethexe/rpc/src/apis/injected/relay.rs +++ b/ethexe/rpc/src/apis/injected/relay.rs @@ -46,14 +46,14 @@ impl TransactionsRelayer { } let (response_sender, response_receiver) = oneshot::channel(); - let event = RpcEvent::InjectedTransaction { + let event = RpcEvent::Transaction { transaction, response_sender, }; if let Err(err) = self.rpc_sender.send(event) { tracing::error!( - "Failed to send `RpcEvent::InjectedTransaction` event task: {err}. \ + "Failed to send `RpcEvent::Transaction` event task: {err}. \ The receiving end in the main service might have been dropped." ); return Err(errors::internal()); diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index 5be6fac8372..0831e214698 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -95,7 +95,7 @@ pub const DEFAULT_BLOCK_GAS_LIMIT_MULTIPLIER: u64 = 10; #[cfg(feature = "server")] #[derive(Debug)] pub enum RpcEvent { - InjectedTransaction { + Transaction { transaction: Transaction, response_sender: oneshot::Sender, }, diff --git a/ethexe/rpc/src/tests.rs b/ethexe/rpc/src/tests.rs index 54d41785c7b..715edb5f229 100644 --- a/ethexe/rpc/src/tests.rs +++ b/ethexe/rpc/src/tests.rs @@ -77,7 +77,7 @@ impl MockService { unreachable!("RPC server should not be stopped during the test") }, event = self.rpc.next() => { - let RpcEvent::InjectedTransaction {transaction, response_sender} = event.expect("RPC event will be valid"); + let RpcEvent::Transaction {transaction, response_sender} = event.expect("RPC event will be valid"); response_sender.send(InjectedTransactionAcceptance::Accept).expect("Response sender will be valid"); match transaction { diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 0ec40b4e28e..cb8baadd3b3 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -883,7 +883,7 @@ impl Service { log::trace!("Received RPC event: {event:?}"); match event { - RpcEvent::InjectedTransaction { + RpcEvent::Transaction { transaction, response_sender, } => { diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 4746c9102bc..7aa050e9b31 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -1890,7 +1890,7 @@ async fn send_injected_tx() { node1 .events() .find(|event| { - if let TestingEvent::Rpc(TestingRpcEvent::InjectedTransaction { transaction }) = event + if let TestingEvent::Rpc(TestingRpcEvent::Transaction { transaction }) = event && transaction.as_injected() == Some(&signed_tx) { true diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index 3b036896b8a..8ef6add251c 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -102,16 +102,16 @@ impl TestingNetworkEvent { #[derive(Debug, Clone, Eq, PartialEq)] pub enum TestingRpcEvent { - InjectedTransaction { transaction: Transaction }, + Transaction { transaction: Transaction }, } impl TestingRpcEvent { fn new(event: &RpcEvent) -> Self { match event { - RpcEvent::InjectedTransaction { + RpcEvent::Transaction { transaction, response_sender: _, - } => Self::InjectedTransaction { + } => Self::Transaction { transaction: transaction.clone(), }, } From 607b26f799fdaa319321785f1fdaa5438af539b0 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 16 Jun 2026 16:43:16 +0300 Subject: [PATCH 08/41] test compatibility with JS library @noble/curves --- Cargo.lock | 219 ++++++++++++++-------------------- Cargo.toml | 12 +- ethexe/common/Cargo.toml | 5 +- ethexe/common/src/injected.rs | 107 +++++++++++++++-- 4 files changed, 197 insertions(+), 146 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ceb596f95d..98414a2914f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -946,7 +946,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -957,7 +957,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1383,14 +1383,14 @@ dependencies = [ [[package]] name = "ark-bls12-381" -version = "0.6.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2ede2c0c96fa37d5d3484e8a59fec566c4a52b8c84bf993eaa6c67d7225a4c" +checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" dependencies = [ - "ark-ec 0.6.0", - "ark-ff 0.6.0", - "ark-serialize 0.6.0", - "ark-std 0.6.0", + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", ] [[package]] @@ -1452,19 +1452,19 @@ dependencies = [ [[package]] name = "ark-ec" -version = "0.6.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8352a2b2aedf6ba2cc38f7520fc51191d518dde96175c729af19f2d059f191c4" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" dependencies = [ "ahash", - "ark-ff 0.6.0", - "ark-poly 0.6.0", - "ark-serialize 0.6.0", - "ark-std 0.6.0", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", "educe", "fnv", - "hashbrown 0.17.1", - "itertools 0.14.0", + "hashbrown 0.15.5", + "itertools 0.13.0", "num-bigint", "num-integer", "num-traits", @@ -1579,23 +1579,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ark-ff" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" -dependencies = [ - "ark-ff-asm 0.6.0", - "ark-ff-macros 0.6.0", - "ark-serialize 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "educe", - "num-bigint", - "num-traits", - "zeroize", -] - [[package]] name = "ark-ff-asm" version = "0.3.0" @@ -1626,16 +1609,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "ark-ff-asm" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" -dependencies = [ - "quote", - "syn 2.0.114", -] - [[package]] name = "ark-ff-macros" version = "0.3.0" @@ -1674,19 +1647,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "ark-ff-macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "ark-models-ext" version = "0.4.1" @@ -1715,17 +1675,17 @@ dependencies = [ [[package]] name = "ark-poly" -version = "0.6.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f55af10b672002b8d953e230282c51206842e20e5791a94432219b4201de5c" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" dependencies = [ "ahash", - "ark-ff 0.6.0", - "ark-serialize 0.6.0", - "ark-std 0.6.0", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", "educe", "fnv", - "hashbrown 0.17.1", + "hashbrown 0.15.5", ] [[package]] @@ -1742,6 +1702,20 @@ dependencies = [ "scale-info", ] +[[package]] +name = "ark-scale" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985c81a9c7b23a72f62b7b20686d5326d2a9956806f37de9ee35cb1238faf0c0" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "parity-scale-codec", + "scale-info", +] + [[package]] name = "ark-serialize" version = "0.3.0" @@ -1770,24 +1744,13 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" dependencies = [ + "ark-serialize-derive 0.5.0", "ark-std 0.5.0", "arrayvec 0.7.6", "digest 0.10.7", "num-bigint", ] -[[package]] -name = "ark-serialize" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" -dependencies = [ - "ark-serialize-derive 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "num-bigint", -] - [[package]] name = "ark-serialize-derive" version = "0.4.2" @@ -1801,9 +1764,9 @@ dependencies = [ [[package]] name = "ark-serialize-derive" -version = "0.6.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" dependencies = [ "proc-macro2", "quote", @@ -1841,16 +1804,6 @@ dependencies = [ "rand 0.8.5", ] -[[package]] -name = "ark-std" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - [[package]] name = "array-bytes" version = "6.2.3" @@ -3405,7 +3358,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -4241,7 +4194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.114", + "syn 1.0.109", ] [[package]] @@ -5591,7 +5544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5662,6 +5615,8 @@ version = "2.0.0" dependencies = [ "alloy-primitives", "anyhow", + "ark-ec 0.5.0", + "ark-ff 0.5.0", "auto_impl", "derive_more 2.1.1", "ferveo-gear-tdec", @@ -6274,11 +6229,11 @@ dependencies = [ [[package]] name = "ferveo-gear-common" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#8674e4f1009600f96016320a28882c4cc57d1ab1" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#521a2462205e1995a2d7c69675fca96ab80b9321" dependencies = [ - "ark-ec 0.6.0", - "ark-serialize 0.6.0", - "ark-std 0.6.0", + "ark-ec 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", "bincode", "const-hex", "generic-array 0.14.7", @@ -6290,16 +6245,17 @@ dependencies = [ [[package]] name = "ferveo-gear-tdec" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#8674e4f1009600f96016320a28882c4cc57d1ab1" -dependencies = [ - "ark-bls12-381 0.6.0", - "ark-ec 0.6.0", - "ark-ff 0.6.0", - "ark-poly 0.6.0", - "ark-serialize 0.6.0", - "ark-std 0.6.0", +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#521a2462205e1995a2d7c69675fca96ab80b9321" +dependencies = [ + "ark-bls12-381 0.5.0", + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", "bincode", "chacha20poly1305", + "const-hex", "ferveo-gear-common", "itertools 0.10.5", "parity-scale-codec", @@ -7085,11 +7041,11 @@ dependencies = [ name = "gbuiltin-bls381" version = "2.0.0" dependencies = [ - "ark-bls12-381 0.4.0", - "ark-ec 0.4.2", - "ark-ff 0.4.2", - "ark-scale", - "ark-serialize 0.4.2", + "ark-bls12-381 0.5.0", + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-scale 0.0.13", + "ark-serialize 0.5.0", "gear-workspace-hack", "parity-scale-codec", "scale-info", @@ -7951,7 +7907,7 @@ dependencies = [ "ark-ec 0.4.2", "ark-ff 0.4.2", "ark-models-ext", - "ark-scale", + "ark-scale 0.0.12", "ark-serialize 0.4.2", "ark-std 0.4.0", "arrayvec 0.7.6", @@ -8536,11 +8492,11 @@ dependencies = [ name = "gsdk" version = "2.0.0" dependencies = [ - "ark-bls12-381 0.4.0", - "ark-ec 0.4.2", - "ark-scale", - "ark-serialize 0.4.2", - "ark-std 0.4.0", + "ark-bls12-381 0.5.0", + "ark-ec 0.5.0", + "ark-scale 0.0.13", + "ark-serialize 0.5.0", + "ark-std 0.5.0", "colored", "demo-bls381", "demo-constructor", @@ -8674,7 +8630,7 @@ dependencies = [ name = "gtest" version = "2.0.0" dependencies = [ - "ark-std 0.4.0", + "ark-std 0.5.0", "builtins-common", "cargo_toml", "colored", @@ -8844,7 +8800,6 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "allocator-api2", "foldhash 0.2.0", ] @@ -9711,7 +9666,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -12249,7 +12204,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9224be3459a0c1d6e9b0f42ab0e76e98b29aef5aba33c0487dfcf47ea08b5150" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 1.1.3", "proc-macro2", "quote", "syn 1.0.109", @@ -12261,7 +12216,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -12902,7 +12857,7 @@ dependencies = [ name = "pallet-gear-builtin" version = "2.0.0" dependencies = [ - "ark-std 0.4.0", + "ark-std 0.5.0", "builtins-common", "demo-proxy-broker", "demo-staking-broker", @@ -14619,7 +14574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck 0.5.0", - "itertools 0.14.0", + "itertools 0.11.0", "log", "multimap 0.10.1", "once_cell", @@ -14665,7 +14620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.114", @@ -15671,7 +15626,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -15684,7 +15639,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -15787,7 +15742,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -15808,7 +15763,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 1.0.5", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -18161,7 +18116,7 @@ dependencies = [ "ark-ed-on-bls12-377-ext", "ark-ed-on-bls12-381-bandersnatch", "ark-ed-on-bls12-381-bandersnatch-ext", - "ark-scale", + "ark-scale 0.0.12", "sp-runtime-interface", ] @@ -18919,13 +18874,13 @@ dependencies = [ [[package]] name = "subproductdomain-gear" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#8674e4f1009600f96016320a28882c4cc57d1ab1" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#521a2462205e1995a2d7c69675fca96ab80b9321" dependencies = [ "anyhow", - "ark-ec 0.6.0", - "ark-ff 0.6.0", - "ark-poly 0.6.0", - "ark-std 0.6.0", + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-std 0.5.0", ] [[package]] @@ -19395,7 +19350,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -21377,7 +21332,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d26b6d78be5..c1b508c047c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -239,12 +239,12 @@ which = "4.4.2" winapi = "0.3.9" paste = "1.0" tempfile = "3.19" -ark-std = { version = "0.4.0", default-features = false } -ark-bls12-381 = { version = "0.4.0", default-features = false } -ark-serialize = { version = "0.4", default-features = false } -ark-ec = { version = "0.4.2", default-features = false } -ark-ff = { version = "0.4.2", default-features = false } -ark-scale = { version = "0.0.12", default-features = false } +ark-std = { version = "0.5", default-features = false } +ark-bls12-381 = { version = "0.5", default-features = false } +ark-serialize = { version = "0.5", default-features = false } +ark-ec = { version = "0.5", default-features = false } +ark-ff = { version = "0.5", default-features = false } +ark-scale = { version = "0.0.13", default-features = false } sha2 = { version = "0.10.8", default-features = false } sha3 = { version = "0.10.8", default-features = false } arrayvec = { version = "0.7.4", default-features = false } diff --git a/ethexe/common/Cargo.toml b/ethexe/common/Cargo.toml index 8a70f236edf..6bc70a27d7d 100644 --- a/ethexe/common/Cargo.toml +++ b/ethexe/common/Cargo.toml @@ -30,7 +30,10 @@ gsigner = { workspace = true, default-features = false, features = [ sha3.workspace = true k256 = { version = "0.13.4", features = ["ecdsa"], default-features = false } nonempty.workspace = true -gear-tdec = { workspace = true, optional = true } +gear-tdec = { workspace = true, optional = true, features = ["serde-hex"]} + +ark-ec.workspace = true +ark-ff.workspace = true # mock deps itertools = { workspace = true, optional = true } diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index 99aa8ea8049..beb32b3a695 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -3,12 +3,18 @@ use crate::{Address, HashOf, ToDigest, ecdsa::SignedMessage}; use alloc::string::{String, ToString}; +use ark_ec::AffineRepr; +#[cfg(feature = "shielded")] +use ark_ec::pairing::Pairing; +#[cfg(feature = "shielded")] +use ark_ff::Fp; +use ark_ff::{BigInteger, PrimeField}; use core::hash::Hash; use gear_core::{limited::LimitedVec, rpc::ReplyInfo}; #[cfg(feature = "shielded")] use gear_tdec::{ Result as TdecResult, - bls12_381::{Ciphertext, DkgPublicKey, SharedSecret}, + bls12_381::{Ciphertext, DkgPublicKey, E as Bls12_381, SharedSecret}, rand_utils::Rng, }; use gprimitives::{ActorId, H256, MessageId}; @@ -395,9 +401,9 @@ impl TransactionPurgedReason { #[cfg_attr(feature = "serde", derive(Hash))] #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TypeInfo)] pub struct ShieldedFields { - pub(crate) destination: ActorId, - pub(crate) value: u128, - pub(crate) payload: LimitedVec, + pub destination: ActorId, + pub value: u128, + pub payload: LimitedVec, } #[cfg(feature = "shielded")] @@ -435,6 +441,33 @@ pub struct ShieldedTransaction { #[cfg(feature = "shielded")] impl ShieldedTransaction { + pub(crate) fn to_hashable_bytes(&self) -> Vec { + let mut buffer = Vec::new(); + + // /// Helper macro to extend G1Affine or G2Affine point coordinate. + // macro_rules! extend_affine_coordinate { + // ($coord:expr) => {{ + // let bytes = $coord.into_bigint().to_bytes_be(); + // buffer.extend_from_slice(&bytes); + // }}; + // } + + // extend_affine_coordinate!(self.ciphertext.commitment.x); + // extend_affine_coordinate!(self.ciphertext.commitment.y); + + // extend_affine_coordinate!(self.ciphertext.auth_tag.x); + // extend_affine_coordinate!(self.ciphertext.auth_tag.y); + + // let v = self.ciphertext.auth_tag.x; + + // buffer.extend(self.ciphertext.ciphertext.as_slice()); + + // buffer.extend_from_slice(self.aad.as_ref()); + // buffer.extend_from_slice(self.reference_block.as_bytes()); + + buffer + } + /// Constructs blake2b hash over [ShieldedTransaction]. pub fn to_hash(&self) -> HashOf { todo!() @@ -505,11 +538,18 @@ impl Transaction { } } +/// Mirroring [Transaction] type, but stores internally references to +/// transactions variants. +/// +/// # Usage +/// This type must be used to transform [Operation] type into [Option]. +/// +/// [Operation]: crate::malachite::Operation #[cfg(feature = "shielded")] #[derive(Clone, Copy)] -pub enum TransactionRef<'t> { - Injected(&'t SignedInjectedTransaction), - Shielded(&'t SignedShieldedTransaction), +pub enum TransactionRef<'op> { + Injected(&'op SignedInjectedTransaction), + Shielded(&'op SignedShieldedTransaction), } #[cfg(feature = "shielded")] @@ -575,11 +615,64 @@ mod digest_hex { #[cfg(all(test, feature = "mock"))] mod tests { + use std::ops::Mul; + + use gear_tdec::bls12_381::Fr; use gsigner::PrivateKey; use super::*; use crate::mock::Mock; + /// You can use this javascript code to reproduce serialize/deserialize paths. + /// ```rust,no_run,ignore + /// import { bls12_381 } from '@noble/curves/bls12-381.js'; + /// const { G1, G2 } = bls12_381; + /// + /// function bytesToHex(bytes) { + /// return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); + /// } + /// function dumpPoint(name, point) { + /// const compressed = point.toBytes(true); + /// console.log(`\n${name}`); + /// console.log(`compressed hex: 0x${bytesToHex(compressed)}`); + /// } + /// + /// dumpPoint('G1 * 123', G1.Point.BASE.multiply(123n)); + /// dumpPoint('G2 * 123', G2.Point.BASE.multiply(123n)); + // ``` + #[test] + fn ark_noble_js_compatible_serialization() { + const NOBLE_JS_G1_123_COMPRESSED_SERIALIZED: &'static str = r#""0xa0ec3e71a719a25208adc97106b122809210faf45a17db24f10ffb1ac014fac1ab95a4a1967e55b185d4df622685b9e8""#; + const NOBLE_JS_G2_123_COMPRESSED_SERIALIZED: &'static str = r#""0x95e18bbdb8b7bd39ea677ee923d7e87af449c45209e635907a4a8a2e4c65fff97c46d038cff53a994da273310ac85866096a5e13fd3ebf4e140e26f6ddfac66651e04e530e6045572acab753bb1bcef990fe14b4426caee41016af69d313750d""#; + #[derive(serde::Serialize, serde::Deserialize)] + #[serde(transparent)] + struct G1Wrapper { + #[serde(with = "gear_tdec::serialization::ark_serde_hex")] + pub point: ::G1, + } + + let g1_123 = ::G1Affine::generator().mul(Fr::from(123)); + let wrapped_g1 = G1Wrapper { point: g1_123 }; + assert_eq!( + serde_json::to_string(&wrapped_g1).unwrap(), + NOBLE_JS_G1_123_COMPRESSED_SERIALIZED + ); + + #[derive(serde::Serialize, serde::Deserialize)] + #[serde(transparent)] + struct G2Wrapper { + #[serde(with = "gear_tdec::serialization::ark_serde_hex")] + pub point: ::G2, + } + + let g2_123 = ::G2Affine::generator().mul(Fr::from(123)); + let wrapped_g2 = G2Wrapper { point: g2_123 }; + assert_eq!( + serde_json::to_string(&wrapped_g2).unwrap(), + NOBLE_JS_G2_123_COMPRESSED_SERIALIZED + ); + } + #[test] fn signed_message_and_injected_transactions() { const RPC_INPUT: &str = r#"{ From e7766130e21cd47a174977fb40df6a8a13a248d4 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 16 Jun 2026 17:30:38 +0300 Subject: [PATCH 09/41] chore: implement HashOf --- Cargo.lock | 1 + ethexe/common/Cargo.toml | 3 +- ethexe/common/src/injected.rs | 93 ++++++++++++++++++++++------------- 3 files changed, 61 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98414a2914f..f8e264871a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5617,6 +5617,7 @@ dependencies = [ "anyhow", "ark-ec 0.5.0", "ark-ff 0.5.0", + "ark-serialize 0.5.0", "auto_impl", "derive_more 2.1.1", "ferveo-gear-tdec", diff --git a/ethexe/common/Cargo.toml b/ethexe/common/Cargo.toml index 6bc70a27d7d..41e0541a1e8 100644 --- a/ethexe/common/Cargo.toml +++ b/ethexe/common/Cargo.toml @@ -34,6 +34,7 @@ gear-tdec = { workspace = true, optional = true, features = ["serde-hex"]} ark-ec.workspace = true ark-ff.workspace = true +ark-serialize = { workspace = true, optional = true } # mock deps itertools = { workspace = true, optional = true } @@ -62,5 +63,5 @@ std = [ "gsigner/keyring", "shielded" ] -shielded = ["dep:gear-tdec"] +shielded = ["dep:gear-tdec", "dep:ark-serialize"] mock = ["std", "itertools/use_std", "tap", "proptest"] diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index beb32b3a695..3fac99775aa 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -2,19 +2,18 @@ // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 use crate::{Address, HashOf, ToDigest, ecdsa::SignedMessage}; -use alloc::string::{String, ToString}; -use ark_ec::AffineRepr; -#[cfg(feature = "shielded")] -use ark_ec::pairing::Pairing; +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; #[cfg(feature = "shielded")] -use ark_ff::Fp; -use ark_ff::{BigInteger, PrimeField}; +use ark_serialize::CanonicalSerialize; use core::hash::Hash; use gear_core::{limited::LimitedVec, rpc::ReplyInfo}; #[cfg(feature = "shielded")] use gear_tdec::{ Result as TdecResult, - bls12_381::{Ciphertext, DkgPublicKey, E as Bls12_381, SharedSecret}, + bls12_381::{Ciphertext, DkgPublicKey, SharedSecret}, rand_utils::Rng, }; use gprimitives::{ActorId, H256, MessageId}; @@ -441,36 +440,36 @@ pub struct ShieldedTransaction { #[cfg(feature = "shielded")] impl ShieldedTransaction { - pub(crate) fn to_hashable_bytes(&self) -> Vec { - let mut buffer = Vec::new(); - - // /// Helper macro to extend G1Affine or G2Affine point coordinate. - // macro_rules! extend_affine_coordinate { - // ($coord:expr) => {{ - // let bytes = $coord.into_bigint().to_bytes_be(); - // buffer.extend_from_slice(&bytes); - // }}; - // } - - // extend_affine_coordinate!(self.ciphertext.commitment.x); - // extend_affine_coordinate!(self.ciphertext.commitment.y); - - // extend_affine_coordinate!(self.ciphertext.auth_tag.x); - // extend_affine_coordinate!(self.ciphertext.auth_tag.y); - - // let v = self.ciphertext.auth_tag.x; + fn append_compressed_point(buffer: &mut Vec, point: &P) { + point + .serialize_compressed(buffer) + .expect("serializing to Vec should not fail"); + } - // buffer.extend(self.ciphertext.ciphertext.as_slice()); + pub(crate) fn to_hashable_bytes(&self) -> Vec { + let mut buffer = Vec::with_capacity( + self.ciphertext.commitment.compressed_size() + + self.ciphertext.auth_tag.compressed_size() + + size_of::() + + size_of::() + + size_of::() + + size_of::(), + ); - // buffer.extend_from_slice(self.aad.as_ref()); - // buffer.extend_from_slice(self.reference_block.as_bytes()); + Self::append_compressed_point(&mut buffer, &self.ciphertext.commitment); + Self::append_compressed_point(&mut buffer, &self.ciphertext.auth_tag); + buffer.extend_from_slice(gear_core::utils::hash(&self.ciphertext.ciphertext).as_ref()); + buffer.extend_from_slice(self.aad.as_ref()); + buffer.extend_from_slice(self.reference_block.0.as_ref()); + buffer.extend_from_slice(gear_core::utils::hash(&self.salt).as_ref()); buffer } /// Constructs blake2b hash over [ShieldedTransaction]. pub fn to_hash(&self) -> HashOf { - todo!() + let hashable_bytes = self.to_hashable_bytes(); + unsafe { HashOf::new(gear_core::utils::hash(hashable_bytes.as_ref()).into()) } } } @@ -479,8 +478,8 @@ pub type SignedShieldedTransaction = SignedMessage; #[cfg(feature = "shielded")] impl ToDigest for ShieldedTransaction { - fn update_hasher(&self, _hasher: &mut sha3::Keccak256) { - todo!("Shielded transaction digest") + fn update_hasher(&self, hasher: &mut sha3::Keccak256) { + hasher.update(self.to_hashable_bytes()); } } @@ -617,14 +616,15 @@ mod digest_hex { mod tests { use std::ops::Mul; - use gear_tdec::bls12_381::Fr; + use ark_ec::{AffineRepr, pairing::Pairing}; + use gear_tdec::bls12_381::{E as Bls12_381, Fr}; use gsigner::PrivateKey; use super::*; use crate::mock::Mock; - /// You can use this javascript code to reproduce serialize/deserialize paths. - /// ```rust,no_run,ignore + /// You can use this JavaScript code to reproduce serialize/deserialize paths. + /// ```no_run,ignore /// import { bls12_381 } from '@noble/curves/bls12-381.js'; /// const { G1, G2 } = bls12_381; /// @@ -639,11 +639,12 @@ mod tests { /// /// dumpPoint('G1 * 123', G1.Point.BASE.multiply(123n)); /// dumpPoint('G2 * 123', G2.Point.BASE.multiply(123n)); - // ``` + /// ``` #[test] fn ark_noble_js_compatible_serialization() { const NOBLE_JS_G1_123_COMPRESSED_SERIALIZED: &'static str = r#""0xa0ec3e71a719a25208adc97106b122809210faf45a17db24f10ffb1ac014fac1ab95a4a1967e55b185d4df622685b9e8""#; const NOBLE_JS_G2_123_COMPRESSED_SERIALIZED: &'static str = r#""0x95e18bbdb8b7bd39ea677ee923d7e87af449c45209e635907a4a8a2e4c65fff97c46d038cff53a994da273310ac85866096a5e13fd3ebf4e140e26f6ddfac66651e04e530e6045572acab753bb1bcef990fe14b4426caee41016af69d313750d""#; + #[derive(serde::Serialize, serde::Deserialize)] #[serde(transparent)] struct G1Wrapper { @@ -836,4 +837,26 @@ mod tests { let deserialized: ShieldedTransaction = serde_json::from_str(&serialized).unwrap(); assert_eq!(shielded_tx, deserialized); } + + #[test] + fn signed_message_and_shielded_transactions() { + let injected_tx = InjectedTransaction::mock(()); + let mut rng = gear_tdec::rand_utils::test_rng(); + let dealer_out = gear_tdec::deal::(3, 2, &mut rng); + let shielded_tx = injected_tx + .shield(&dealer_out.public_key, &mut rng) + .unwrap(); + + let signed_tx = + SignedShieldedTransaction::create(PrivateKey::random(), shielded_tx).unwrap(); + + assert_eq!( + signed_tx + .signature() + .recover_message(signed_tx.data()) + .expect("failed to recover message") + .to_address(), + signed_tx.address() + ); + } } From ebba7a877a05a9997fcc7e0ad80b84bbde95e568 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 17 Jun 2026 12:15:21 +0300 Subject: [PATCH 10/41] chore: handling Shielded transactions in mempool --- Cargo.lock | 1 + ethexe/common/Cargo.toml | 6 +- ethexe/common/src/db.rs | 6 +- ethexe/common/src/hash.rs | 31 +++ ethexe/common/src/injected.rs | 41 ++- ethexe/malachite/service/Cargo.toml | 1 + ethexe/malachite/service/src/mempool.rs | 348 ++++++++++++++++++------ ethexe/service/src/tests/mod.rs | 4 +- 8 files changed, 349 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8e264871a4..b2f30c7f0d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5761,6 +5761,7 @@ dependencies = [ "ethexe-db", "ethexe-malachite-core", "ethexe-runtime-common", + "ferveo-gear-tdec", "futures", "gear-workspace-hack", "gprimitives", diff --git a/ethexe/common/Cargo.toml b/ethexe/common/Cargo.toml index 41e0541a1e8..053c8811b45 100644 --- a/ethexe/common/Cargo.toml +++ b/ethexe/common/Cargo.toml @@ -18,7 +18,6 @@ gprimitives.workspace = true parity-scale-codec.workspace = true scale-info = { workspace = true, features = ["derive"] } hex.workspace = true -serde = { workspace = true, optional = true } derive_more.workspace = true anyhow.workspace = true auto_impl.workspace = true @@ -30,10 +29,13 @@ gsigner = { workspace = true, default-features = false, features = [ sha3.workspace = true k256 = { version = "0.13.4", features = ["ecdsa"], default-features = false } nonempty.workspace = true -gear-tdec = { workspace = true, optional = true, features = ["serde-hex"]} ark-ec.workspace = true ark-ff.workspace = true + +# optional dependencies +serde = { workspace = true, optional = true } +gear-tdec = { workspace = true, optional = true, features = ["serde-hex"]} ark-serialize = { workspace = true, optional = true } # mock deps diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index 85e28d82071..88b96be0a41 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -3,12 +3,14 @@ //! Common db types and traits. +#[cfg(feature = "shielded")] +use crate::injected::SignedTxReceipt; use crate::{ Address, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, ProtocolTimelines, Schedule, SimpleBlockData, ValidatorsVec, events::BlockEvent, gear::StateTransition, - injected::{InjectedTransaction, Promise, SignedInjectedTransaction, SignedTxReceipt}, + injected::{InjectedTransaction, Promise, SignedInjectedTransaction}, malachite::Operations, }; use alloc::{ @@ -118,6 +120,7 @@ pub trait InjectedStorageRO { /// Returns the promise by its transaction hash. fn promise(&self, hash: HashOf) -> Option; + #[cfg(feature = "shielded")] /// Returns the receipt by its transaction hash. fn receipt(&self, hash: HashOf) -> Option; } @@ -128,6 +131,7 @@ pub trait InjectedStorageRW: InjectedStorageRO { fn set_promise(&self, promise: &Promise); + #[cfg(feature = "shielded")] fn set_receipt(&self, receipt: &SignedTxReceipt); } diff --git a/ethexe/common/src/hash.rs b/ethexe/common/src/hash.rs index 7ef4d7db52c..24fbd094099 100644 --- a/ethexe/common/src/hash.rs +++ b/ethexe/common/src/hash.rs @@ -1,6 +1,7 @@ // Copyright (C) Gear Technologies Inc. // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +use crate::ToDigest; use alloc::string::{String, ToString}; use anyhow::Result; use core::{ @@ -13,6 +14,7 @@ use core::{ use gprimitives::H256; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; +use sha3::Digest; fn option_string(value: &Option) -> String { value @@ -200,3 +202,32 @@ impl From> for MaybeHashOf { Self(Some(value)) } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, derive_more::Display)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +pub enum EitherHashOf { + #[display("Left({_0})")] + Left(HashOf), + #[display("Right({_0})")] + Right(HashOf), +} + +impl EitherHashOf { + pub fn inner(&self) -> H256 { + match self { + Self::Left(left_hash) => left_hash.inner(), + Self::Right(right_hash) => right_hash.inner(), + } + } +} + +impl ToDigest for EitherHashOf { + fn update_hasher(&self, hasher: &mut sha3::Keccak256) { + let prefix = match self { + Self::Left(_) => 0u8, + Self::Right(_) => 1u8, + }; + hasher.update(&[prefix]); + hasher.update(self.inner().as_ref()); + } +} diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index 3fac99775aa..09951ade945 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -1,7 +1,7 @@ // Copyright (C) Gear Technologies Inc. // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 -use crate::{Address, HashOf, ToDigest, ecdsa::SignedMessage}; +use crate::{Address, EitherHashOf, HashOf, ToDigest, ecdsa::SignedMessage}; use alloc::{ string::{String, ToString}, vec::Vec, @@ -250,6 +250,7 @@ impl PromiseKind for CompactPromise { /// **Important**: `Receipt` and `Receipt` have the same /// digest. So it helps to reuses the producer's signature to construct the full /// version from compact. +#[cfg(feature = "shielded")] #[derive( Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::IsVariant, derive_more::Unwrap, )] @@ -260,15 +261,22 @@ pub enum Receipt

{ Purged(PurgedTransaction), } +#[cfg(feature = "shielded")] impl Receipt

{ pub fn tx_hash(&self) -> HashOf { match self { Self::Promise(promise) => promise.tx_hash(), - Self::Purged(purged) => purged.tx_hash, + Self::Purged(purged) => { + let TransactionHash::Left(tx_hash) = purged.tx_hash else { + todo!() + }; + tx_hash + } } } } +#[cfg(feature = "shielded")] impl ToDigest for Receipt

{ fn update_hasher(&self, hasher: &mut sha3::Keccak256) { match self { @@ -286,6 +294,7 @@ impl ToDigest for Receipt

{ /// Signed [Receipt] with a [Promise] generic. /// End RPC user always receives this object. +#[cfg(feature = "shielded")] #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::From, derive_more::Deref)] #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", serde(transparent))] @@ -293,6 +302,7 @@ pub struct SignedTxReceipt(SignedMessage>); /// Signed [Receipt] with a [CompactPromise] generic. /// It is used as a lightweight transfer type +#[cfg(feature = "shielded")] #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Deref, derive_more::From)] pub struct SignedCompactTxReceipt(SignedMessage>); @@ -301,12 +311,14 @@ pub struct SignedCompactTxReceipt(SignedMessage>); /// to full version. /// [Pending](Self::Pending) means that receipt contains a promise and requires the /// full promise body to restore receipt. +#[cfg(feature = "shielded")] #[derive(Debug, PartialEq, Eq, derive_more::From)] pub enum UpgradedReceipt { Pending(UnfilledPromiseReceipt), Ready(SignedTxReceipt), } +#[cfg(feature = "shielded")] impl SignedCompactTxReceipt { /// Upgrades the compact receipt to its full version ([SignedTxReceipt]). pub fn upgrade(self) -> UpgradedReceipt { @@ -334,11 +346,13 @@ pub struct UnfilledPromiseReceipt(#[deref] CompactPromise, Signature, Address); /// The result of [try_fill_with](UnfilledPromiseReceipt::try_fill_with) function. /// [Filled](Self::Filled) means the successful result. /// [HashesMismatch](Self::HashesMismatch) means that raw promise body and stored compact are not the same promise. +#[cfg(feature = "shielded")] pub enum TryFillPromiseResult { Filled(SignedTxReceipt), HashesMismatch(UnfilledPromiseReceipt), } +#[cfg(feature = "shielded")] impl UnfilledPromiseReceipt { pub fn try_fill_with(self, promise: Promise) -> TryFillPromiseResult { if self.0 != promise.to_compact() { @@ -352,19 +366,23 @@ impl UnfilledPromiseReceipt { } } -/// Represents the reason why [InjectedTransaction] was not included. +/// Represents the reason why transaction was not included. +#[cfg(feature = "shielded")] #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Display)] #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[display("Injected transaction wasn't executed: tx_hash={tx_hash}, reason={reason}")] pub struct PurgedTransaction { - pub tx_hash: HashOf, + /// Has of [InjectedTransaction] or [ShieldedTransaction]. + pub tx_hash: TransactionHash, + /// Reason why transaction was purged from mempool. pub reason: TransactionPurgedReason, } +#[cfg(feature = "shielded")] impl ToDigest for PurgedTransaction { fn update_hasher(&self, hasher: &mut sha3::Keccak256) { let Self { tx_hash, reason } = self; - hasher.update(tx_hash.inner().0); + tx_hash.update_hasher(hasher); hasher.update([reason.variant_index()]); } } @@ -513,6 +531,10 @@ pub enum Transaction { Shielded(SignedShieldedTransaction), } +#[cfg(feature = "shielded")] +/// Type alias over [EitherHashOf]. +pub type TransactionHash = EitherHashOf; + #[cfg(feature = "shielded")] impl Transaction { pub fn as_ref(&self) -> TransactionRef<'_> { @@ -814,7 +836,7 @@ mod tests { #[test] fn tx_receipt_has_the_same_hash_for_error() { let purged = PurgedTransaction { - tx_hash: unsafe { HashOf::new(H256::random()) }, + tx_hash: unsafe { TransactionHash::Left(HashOf::new(H256::random())) }, reason: TransactionPurgedReason::Outdated, }; let receipt1 = Receipt::::Purged(purged.clone()); @@ -859,4 +881,11 @@ mod tests { signed_tx.address() ); } + + #[test] + fn mock_display() { + let hash = InjectedTransaction::mock(()).to_hash(); + let h = TransactionHash::Left(hash); + println!("{h}"); + } } diff --git a/ethexe/malachite/service/Cargo.toml b/ethexe/malachite/service/Cargo.toml index 0503bf9c0f3..8025dc4a40b 100644 --- a/ethexe/malachite/service/Cargo.toml +++ b/ethexe/malachite/service/Cargo.toml @@ -37,6 +37,7 @@ gear-workspace-hack.workspace = true # Enable the `mock` feature on the in-mem database so tests can call # `Database::memory()` without `unsafe`. ethexe-db = { workspace = true, features = ["mock"] } +gear-tdec.workspace = true proptest.workspace = true tempfile.workspace = true tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "sync", "test-util", "time"] } diff --git a/ethexe/malachite/service/src/mempool.rs b/ethexe/malachite/service/src/mempool.rs index 70d5f3fe3d6..71545a8411e 100644 --- a/ethexe/malachite/service/src/mempool.rs +++ b/ethexe/malachite/service/src/mempool.rs @@ -40,7 +40,8 @@ use ethexe_common::{ HashOf, SimpleBlockData, db::{GlobalsStorageRO, InjectedStorageRW, OnChainStorageRO}, injected::{ - InjectedTransaction, InjectedTransactionAcceptance, PurgedTransaction, Transaction, + InjectedTransaction, InjectedTransactionAcceptance, PurgedTransaction, ShieldedTransaction, + SignedInjectedTransaction, SignedShieldedTransaction, Transaction, TransactionHash, TransactionPurgedReason, TransactionRef, VALIDITY_WINDOW, }, }; @@ -176,13 +177,25 @@ pub const DEFAULT_POOL_CAPACITY: usize = 10_000; /// Pool state behind a single mutex — operations are short, contention low. #[derive(Debug, Default)] struct Inner { - pool: HashMap, Transaction>, - /// Recently committed txs (tx_hash → ref_block) for dedup. Aged out with the validity window. - seen: HashMap, H256>, + /// Injected transactions waiting for its inclusion in chain. + injected_pool: HashMap, SignedInjectedTransaction>, + /// Recently committed injected txs (tx_hash → ref_block) for dedup. Aged out with the validity window. + injected_seen: HashMap, H256>, + /// Shielded transactions waiting for its inclusion in chain. + shielded_pool: HashMap, SignedShieldedTransaction>, + /// Recently committed shielded txs (tx_hash → ref_block) for dedup. Aged out with the validity window. + shielded_seen: HashMap, H256>, /// Latest chain head height — drives age-out of pool/seen entries. latest_head_height: Option, } +impl Inner { + /// Returns number of transactions in `injected_pool` + `shielded_pool`. + pub fn len(&self) -> usize { + self.injected_pool.len() + self.shielded_pool.len() + } +} + #[derive(Debug)] pub struct InjectedTxMempool { inner: Mutex, @@ -206,12 +219,13 @@ impl InjectedTxMempool { } } + /// Delegates call to [Inner::len]. pub fn len(&self) -> usize { - self.inner.lock().expect("poisoned mempool").pool.len() + self.inner.lock().expect("poisoned mempool").len() } pub fn is_empty(&self) -> bool { - self.inner.lock().expect("poisoned mempool").pool.is_empty() + self.len() == 0 } /// Resolve `reference_block` to its canonical height via the DB. @@ -259,64 +273,75 @@ impl InjectedTxMempool { /// Evict pool entries and seen-hashes whose `reference_block` has /// aged out relative to `head_height`. fn purge_expired(inner: &mut Inner, head_height: u32, db: &Database) -> Vec { - inner.seen.retain(|tx_hash, ref_block| { - match db.block_header(*ref_block).map(|h| h.height) { - Some(h) if !Self::is_expired(head_height, h) => true, - _ => { - trace!(%tx_hash, ref_block = %ref_block, "dropping expired seen-hash"); - false - } + let keep_seen = |tx_type: &'static str, tx_hash: H256, reference_block: &H256| match db + .block_header(*reference_block) + .map(|header| header.height) + { + Some(height) if !Self::is_expired(head_height, height) => true, + _ => { + trace!(%tx_type, %tx_hash, %reference_block, "dropping expired seen-hash"); + false } - }); + }; + inner + .injected_seen + .retain(|tx_hash, ref_block| keep_seen("injected", tx_hash.inner(), ref_block)); + inner + .shielded_seen + .retain(|tx_hash, ref_block| keep_seen("shielded", tx_hash.inner(), ref_block)); + let mut purged_txs = Vec::new(); - inner.pool.retain(|tx_hash, tx| { - let ref_block = tx.as_ref().reference_block(); - match db.block_header(ref_block).map(|h| h.height) { - Some(h) if !Self::is_expired(head_height, h) => true, - Some(h) => { - trace!( - %tx_hash, %ref_block, ref_height = h, head_height, - "dropping expired tx from pool", - ); - purged_txs.push(PurgedTransaction { - tx_hash: *tx_hash, - reason: TransactionPurgedReason::Outdated, - }); - false - } - None => { - trace!( - %tx_hash, %ref_block, - "dropping tx with unknown ref_block from pool", - ); - purged_txs.push(PurgedTransaction { - tx_hash: *tx_hash, - reason: TransactionPurgedReason::UnknownReferenceBlock, - }); - false - } + let mut purge_fn = |tx_hash: TransactionHash, ref_block: H256| match db + .block_header(ref_block) + .map(|h| h.height) + { + Some(h) if !Self::is_expired(head_height, h) => true, + Some(h) => { + trace!( + %tx_hash, %ref_block, ref_height = h, head_height, + "dropping expired tx from pool", + ); + purged_txs.push(PurgedTransaction { + tx_hash, + reason: TransactionPurgedReason::Outdated, + }); + false } + None => { + trace!( + %tx_hash, %ref_block, + "dropping tx with unknown ref_block from pool", + ); + purged_txs.push(PurgedTransaction { + tx_hash, + reason: TransactionPurgedReason::UnknownReferenceBlock, + }); + false + } + }; + + inner.injected_pool.retain(|tx_hash, tx| { + purge_fn(TransactionHash::Left(*tx_hash), tx.data().reference_block) }); + inner.shielded_pool.retain(|tx_hash, tx| { + purge_fn(TransactionHash::Right(*tx_hash), tx.data().reference_block) + }); + purged_txs } -} -#[async_trait] -impl Mempool for InjectedTxMempool { - fn insert(&self, tx: Transaction) -> TxInsertionStatus { - let tx_hash = tx.as_ref().hash(); - let ref_block = tx.as_ref().reference_block(); + fn insert_injected(&self, tx: SignedInjectedTransaction) -> TxInsertionStatus { + let tx_hash = tx.data().to_hash(); + let ref_block = tx.data().reference_block; // Reject non-zero-value txs unconditionally (#5083 — value-bearing // injected txs are not supported yet). Done first so a malicious // sender can't burn pool capacity with txs that will never be // selectable. - if let Transaction::Injected(tx_data) = &tx - && tx_data.data().value != 0 - { + if tx.data().value != 0 { info!( %tx_hash, - value = tx_data.data().value, + value = tx.data().value, "mempool: rejecting tx — non-zero value (#5083 not supported)", ); return TxInsertionStatus::NonZeroValue; @@ -324,13 +349,13 @@ impl Mempool for InjectedTxMempool { let inner = self.inner.lock().expect("poisoned mempool"); - if inner.seen.contains_key(&tx_hash) { + if inner.injected_seen.contains_key(&tx_hash) { info!(%tx_hash, "mempool: idempotent no-op — hash already committed within validity window"); return TxInsertionStatus::AlreadyIncluded; } - if inner.pool.contains_key(&tx_hash) { - info!(%tx_hash, pool_len = inner.pool.len(), "mempool: idempotent no-op — duplicate insert"); + if inner.injected_pool.contains_key(&tx_hash) { + info!(%tx_hash, pool_len = inner.len(), "mempool: idempotent no-op — duplicate insert"); return TxInsertionStatus::AlreadyInPool; } @@ -350,7 +375,7 @@ impl Mempool for InjectedTxMempool { return TxInsertionStatus::ExpiredRefBlock; } - if inner.pool.len() >= self.capacity { + if inner.len() >= self.capacity { info!(%tx_hash, capacity = self.capacity, "mempool: rejecting tx — pool at capacity"); return TxInsertionStatus::PoolFull; } @@ -369,26 +394,23 @@ impl Mempool for InjectedTxMempool { // immediately picks the tx is guaranteed to find it in the DB. // The DB row is content-addressed by tx_hash, so two racing // writes converge on the same byte content. - match &tx { - Transaction::Injected(tx) => self.db.set_injected_transaction(tx.clone()), - Transaction::Shielded(_) => todo!("Shielded transaction storage"), - } + self.db.set_injected_transaction(tx.clone()); let mut inner = self.inner.lock().expect("poisoned mempool"); // Recheck dedup / capacity after the lock-free window. - if inner.seen.contains_key(&tx_hash) { + if inner.injected_seen.contains_key(&tx_hash) { return TxInsertionStatus::AlreadyIncluded; } - if inner.pool.contains_key(&tx_hash) { + if inner.injected_pool.contains_key(&tx_hash) { return TxInsertionStatus::AlreadyInPool; } - if inner.pool.len() >= self.capacity { + if inner.len() >= self.capacity { return TxInsertionStatus::PoolFull; } - let pool_len_after = inner.pool.len() + 1; - inner.pool.insert(tx_hash, tx); + let pool_len_after = inner.len() + 1; + inner.injected_pool.insert(tx_hash, tx); info!( %tx_hash, %ref_block, @@ -397,13 +419,80 @@ impl Mempool for InjectedTxMempool { "mempool: insert accepted", ); - // Drop the lock before signaling so a waiter resumed - // immediately doesn't have to bounce on the mutex. drop(inner); self.new_tx_notify.notify_one(); TxInsertionStatus::Inserted } + fn insert_shielded(&self, tx: SignedShieldedTransaction) -> TxInsertionStatus { + let tx_hash = tx.data().to_hash(); + let ref_block = tx.data().reference_block; + let inner = self.inner.lock().expect("poisoned mempool"); + + if inner.shielded_seen.contains_key(&tx_hash) { + info!(tx_hash = %tx_hash.inner(), "mempool: idempotent no-op — shielded hash already committed within validity window"); + return TxInsertionStatus::AlreadyIncluded; + } + + if inner.shielded_pool.contains_key(&tx_hash) { + info!(tx_hash = %tx_hash.inner(), pool_len = inner.len(), "mempool: idempotent no-op — duplicate shielded insert"); + return TxInsertionStatus::AlreadyInPool; + } + + let ref_height_opt = self.ref_block_height(ref_block); + if let Some(ref_height) = ref_height_opt + && let Some(head_height) = inner.latest_head_height + && Self::is_expired(head_height, ref_height) + { + info!( + tx_hash = %tx_hash.inner(), %ref_block, ref_height, head_height, + "mempool: rejecting shielded tx — reference_block past VALIDITY_WINDOW" + ); + return TxInsertionStatus::ExpiredRefBlock; + } + + if inner.len() >= self.capacity { + info!(tx_hash = %tx_hash.inner(), capacity = self.capacity, "mempool: rejecting shielded tx — pool at capacity"); + return TxInsertionStatus::PoolFull; + } + drop(inner); + + let mut inner = self.inner.lock().expect("poisoned mempool"); + if inner.shielded_seen.contains_key(&tx_hash) { + return TxInsertionStatus::AlreadyIncluded; + } + if inner.shielded_pool.contains_key(&tx_hash) { + return TxInsertionStatus::AlreadyInPool; + } + if inner.len() >= self.capacity { + return TxInsertionStatus::PoolFull; + } + + let pool_len_after = inner.len() + 1; + inner.shielded_pool.insert(tx_hash, tx); + info!( + tx_hash = %tx_hash.inner(), + %ref_block, + ref_height = ?ref_height_opt, + pool_len = pool_len_after, + "mempool: shielded insert accepted", + ); + + drop(inner); + self.new_tx_notify.notify_one(); + TxInsertionStatus::Inserted + } +} + +#[async_trait] +impl Mempool for InjectedTxMempool { + fn insert(&self, tx: Transaction) -> TxInsertionStatus { + match tx { + Transaction::Injected(tx) => self.insert_injected(tx), + Transaction::Shielded(tx) => self.insert_shielded(tx), + } + } + fn set_chain_head(&self, head: SimpleBlockData) -> Vec { let mut inner = self.inner.lock().expect("poisoned mempool"); let h = head.header.height; @@ -420,30 +509,49 @@ impl Mempool for InjectedTxMempool { let ancestors = self.recent_ancestors(&head); let inner = self.inner.lock().expect("poisoned mempool"); - let pool_len = inner.pool.len(); - let result: Vec<_> = inner - .pool + let pool_len = inner.len(); + + let mut transactions = Vec::new(); + inner + .injected_pool .values() - .filter(|tx| ancestors.contains(&tx.as_ref().reference_block())) - .cloned() - .collect(); + .filter(|tx| ancestors.contains(&tx.data().reference_block)) + .for_each(|tx| transactions.push(Transaction::Injected(tx.clone()))); + + inner + .shielded_pool + .values() + .filter(|tx| ancestors.contains(&tx.data().reference_block)) + .for_each(|tx| transactions.push(Transaction::Shielded(tx.clone()))); + info!( head_hash = %head.hash, head_height = head.header.height, ancestors = ancestors.len(), pool_len, - returned = result.len(), + returned = transactions.len(), "mempool: fetch", ); - result + transactions } async fn forget(&self, committed: &[TransactionRef<'_>]) { let mut inner = self.inner.lock().expect("poisoned mempool"); - committed.iter().for_each(|tx_ref| { - let tx_hash = tx_ref.hash(); - inner.pool.remove(&tx_hash); - inner.seen.insert(tx_hash, tx_ref.reference_block()); + committed.iter().for_each(|tx_ref| match tx_ref { + TransactionRef::Injected(tx) => { + let tx_hash = tx.data().to_hash(); + inner.injected_pool.remove(&tx_hash); + inner + .injected_seen + .insert(tx_hash, tx.data().reference_block); + } + TransactionRef::Shielded(tx) => { + let tx_hash = tx.data().to_hash(); + inner.shielded_pool.remove(&tx_hash); + inner + .shielded_seen + .insert(tx_hash, tx.data().reference_block); + } }); } @@ -466,7 +574,10 @@ mod tests { use ethexe_common::{ BlockHeader, PrivateKey, SignedMessage, SimpleBlockData, db::{BlockMetaStorageRW, GlobalsStorageRW, OnChainStorageRW}, - injected::{InjectedTransaction, InjectedTransactionAcceptance, SignedInjectedTransaction}, + injected::{ + InjectedTransaction, InjectedTransactionAcceptance, SignedInjectedTransaction, + SignedShieldedTransaction, + }, }; use gprimitives::ActorId; use std::time::Duration; @@ -646,6 +757,28 @@ mod tests { .unwrap() } + fn signed_shielded_tx( + pk: &PrivateKey, + destination: ActorId, + ref_block: H256, + salt: u8, + ) -> SignedShieldedTransaction { + let injected_tx = InjectedTransaction { + destination, + payload: vec![1, 2, 3].try_into().unwrap(), + value: 0, + reference_block: ref_block, + salt: vec![salt; 32].try_into().unwrap(), + }; + let mut rng = gear_tdec::rand_utils::test_rng(); + let dealer_out = gear_tdec::deal::(3, 2, &mut rng); + let shielded_tx = injected_tx + .shield(&dealer_out.public_key, &mut rng) + .unwrap(); + + SignedMessage::create(pk.clone(), shielded_tx).unwrap() + } + #[test] fn insert_unknown_ref_block_is_accepted() { let db = Database::memory(); @@ -700,6 +833,65 @@ mod tests { assert_eq!(pool.len(), 2, "third insert must hit the capacity cap"); } + #[test] + fn capacity_is_shared_between_injected_and_shielded_pools() { + let db = Database::memory(); + let chain = linear_chain(&db, 2); + let pool = InjectedTxMempool::with_capacity(db, 1); + let pk = PrivateKey::random(); + + pool.insert(signed_shielded_tx(&pk, ActorId::zero(), chain[1].hash, 0).into()); + + assert_eq!( + pool.insert(signed_tx(&pk, ActorId::zero(), chain[1].hash, 1).into()), + TxInsertionStatus::PoolFull, + ); + assert_eq!(pool.len(), 1); + } + + #[test] + fn shielded_insert_fetch_and_forget_round_trip() { + let db = Database::memory(); + let chain = linear_chain(&db, 3); + let pool = InjectedTxMempool::new(db); + let pk = PrivateKey::random(); + let tx: Transaction = signed_shielded_tx(&pk, ActorId::zero(), chain[2].hash, 1).into(); + let Transaction::Shielded(signed) = &tx else { + unreachable!("helper creates shielded transaction"); + }; + let tx_hash = signed.data().to_hash(); + + assert_eq!(pool.insert(tx.clone()), TxInsertionStatus::Inserted); + assert_eq!(pool.len(), 1); + + let fetched = futures::executor::block_on(pool.fetch(chain[2])); + assert_eq!(fetched.len(), 1); + let Transaction::Shielded(fetched) = &fetched[0] else { + panic!("expected shielded transaction"); + }; + assert_eq!(fetched.data().to_hash(), tx_hash); + + futures::executor::block_on(pool.forget(std::slice::from_ref(&tx.as_ref()))); + assert_eq!(pool.len(), 0); + assert_eq!(pool.insert(tx), TxInsertionStatus::AlreadyIncluded); + assert_eq!(pool.len(), 0); + } + + #[test] + fn set_chain_head_purges_expired_shielded() { + let db = Database::memory(); + let chain = linear_chain(&db, (VALIDITY_WINDOW as usize) + 5); + let pool = InjectedTxMempool::new(db); + let pk = PrivateKey::random(); + let tx: Transaction = signed_shielded_tx(&pk, ActorId::zero(), chain[1].hash, 0).into(); + pool.insert(tx); + assert_eq!(pool.len(), 1); + + let head_idx = (VALIDITY_WINDOW as usize) + 1; + let _ = pool.set_chain_head(chain[head_idx]); + assert_eq!(pool.len(), 0); + } + #[test] fn pool_retains_unresolved_ref_block_indefinitely() { let db = Database::memory(); diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 7aa050e9b31..ab1b3b32d14 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -23,7 +23,7 @@ use ethexe_common::{ }, gear::BatchCommitment, injected::{ - InjectedTransaction, InjectedTransactionAcceptance, Receipt, TransactionPurgedReason, + InjectedTransaction, InjectedTransactionAcceptance, Receipt, TransactionHash, TransactionPurgedReason }, mock::*, }; @@ -1964,7 +1964,7 @@ async fn injected_tx_purged_receipt() { subscription_receipt.data() ); }; - assert_eq!(purged.tx_hash, tx_hash); + assert_eq!(purged.tx_hash, TransactionHash::Left(tx_hash)); assert_eq!( purged.reason, TransactionPurgedReason::UnknownReferenceBlock From 1cc817e5bc8e65c53304d1a322e22fdfac3f4566 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 17 Jun 2026 15:16:40 +0300 Subject: [PATCH 11/41] chore: adopt ValidityChecker for ShieldedTransaction --- ethexe/malachite/service/src/externalities.rs | 38 ++-- ethexe/malachite/service/src/service.rs | 5 +- ethexe/malachite/service/src/tx_validity.rs | 170 +++++++++++++++--- ethexe/service/src/lib.rs | 5 +- 4 files changed, 168 insertions(+), 50 deletions(-) diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index acca6b7781b..623fe9c8f80 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -64,6 +64,7 @@ use std::{ use tokio::sync::{Notify, mpsc}; use tracing::{debug, error, info, warn}; +/// Optimization for reducing `clone` operation for potentially large transactions. fn operation_to_transaction(operation: &Operation) -> Option> { match operation { Operation::Injected(tx) => Some(TransactionRef::Injected(tx)), @@ -72,6 +73,13 @@ fn operation_to_transaction(operation: &Operation) -> Option> } } +fn transaction_ref_hash(transaction: TransactionRef<'_>) -> H256 { + match transaction { + TransactionRef::Injected(tx) => tx.data().to_hash().inner(), + TransactionRef::Shielded(tx) => tx.data().to_hash().inner(), + } +} + fn transaction_to_operation(transaction: Transaction) -> Operation { match transaction { Transaction::Injected(tx) => Operation::Injected(tx), @@ -305,13 +313,13 @@ impl Externalities for EthexeExternalities { self.db.mb_meta(parent_mb_hash).last_advanced_eb }; - let (advance, injected) = self.wait_for_proposable_content(parent_advanced).await; + let (advance, transactions) = self.wait_for_proposable_content(parent_advanced).await; info!( %parent_mb_hash, %parent_advanced, advance = ?advance, - injected_count = injected.len(), + transactions_count = transactions.len(), "build_block_above: proposable content resolved", ); @@ -319,18 +327,18 @@ impl Externalities for EthexeExternalities { // run through TxValidityChecker so we don't waste an MB // round-trip on a tx the participant would reject. let chain_head_snapshot = *self.chain_head.read().expect("chain_head poisoned"); - let valid: Vec = match chain_head_snapshot { + let valid = match chain_head_snapshot { Some(head) => { let checker = TxValidityChecker::new_for_mb(self.db.clone(), head, parent_mb_hash)?; - let mut accepted = Vec::with_capacity(injected.len()); - for tx in injected { + let mut accepted = Vec::with_capacity(transactions.len()); + for tx in transactions { match checker.check_tx_validity(tx.as_ref())? { TxValidity::Valid => accepted.push(tx), reason => { warn!( - tx_hash = %tx.as_ref().hash(), + tx_hash = %transaction_ref_hash(tx.as_ref()), ?reason, - "build_block_above: dropping injected tx — fails TxValidity", + "build_block_above: dropping transaction — fails TxValidity", ); } } @@ -341,10 +349,10 @@ impl Externalities for EthexeExternalities { // for `is_reference_block_*`). Skip injected txs entirely // rather than emit unvalidated ones. None => { - if !injected.is_empty() { + if !transactions.is_empty() { warn!( - injected_count = injected.len(), - "build_block_above: no chain head — dropping injected txs (unvalidated)", + transactions_count = transactions.len(), + "build_block_above: no chain head — dropping transactions (unvalidated)", ); } Vec::new() @@ -636,9 +644,9 @@ impl Externalities for EthexeExternalities { TxValidity::Valid => {} reason => { warn!( - tx_hash = %transaction.hash(), + tx_hash = %transaction_ref_hash(transaction), ?reason, - "validate: injected tx fails TxValidity — rejecting MB", + "validate: transaction fails TxValidity — rejecting MB", ); return Ok(false); } @@ -768,13 +776,13 @@ impl EthexeExternalities { let advance = self.find_eb_candidate_for_advancing(prev_advanced_eb_hash); let head_snapshot = *self.chain_head.read().expect("chain_head poisoned"); - let injected = match head_snapshot { + let transactions = match head_snapshot { Some(head) => self.mempool.fetch(head).await, None => Vec::new(), }; - if advance.is_some() || !injected.is_empty() { - return (advance, injected); + if advance.is_some() || !transactions.is_empty() { + return (advance, transactions); } tokio::select! { diff --git a/ethexe/malachite/service/src/service.rs b/ethexe/malachite/service/src/service.rs index bde19cde3ea..113ba8eac41 100644 --- a/ethexe/malachite/service/src/service.rs +++ b/ethexe/malachite/service/src/service.rs @@ -196,10 +196,7 @@ impl MalachiteService { /// Every outcome — including rejecting ones — is a /// [`crate::mempool::TxInsertionStatus`] value; group membership is /// queried via [`crate::mempool::TxInsertionStatus::is_accepted`]. - pub fn receive_injected_transaction( - &self, - tx: Transaction, - ) -> crate::mempool::TxInsertionStatus { + pub fn receive_transaction(&self, tx: Transaction) -> crate::mempool::TxInsertionStatus { self.mempool.insert(tx) } diff --git a/ethexe/malachite/service/src/tx_validity.rs b/ethexe/malachite/service/src/tx_validity.rs index 0a5280e7fce..baa7e479ced 100644 --- a/ethexe/malachite/service/src/tx_validity.rs +++ b/ethexe/malachite/service/src/tx_validity.rs @@ -30,7 +30,10 @@ use ethexe_common::{ db::{GlobalsStorageRO, MbStorageRO, OnChainStorageRO}, events::{BlockRequestEvent, RouterRequestEvent, router::ProgramCreatedEvent}, gear::INJECTED_MESSAGE_PANIC_GAS_CHARGE_THRESHOLD, - injected::{InjectedTransaction, TransactionRef, VALIDITY_WINDOW}, + injected::{ + InjectedTransaction, ShieldedTransaction, SignedInjectedTransaction, + SignedShieldedTransaction, TransactionRef, VALIDITY_WINDOW, + }, malachite::Operation, }; use ethexe_db::Database; @@ -83,7 +86,8 @@ pub struct TxValidityChecker { db: Database, chain_head: SimpleBlockData, start_block_hash: H256, - recent_included_txs: HashSet>, + recent_included_injected_txs: HashSet>, + recent_included_shielded_txs: HashSet>, latest_states: ProgramStates, } @@ -117,26 +121,32 @@ impl TxValidityChecker { anyhow!("MB {cursor} marked computed but has no program_states row — DB invariant") })?; - let recent_included_txs = Self::collect_recent_included_txs(&db, parent_mb_hash)?; + let (recent_included_injected_txs, recent_included_shielded_txs) = + Self::collect_recent_included_txs(&db, parent_mb_hash)?; let start_block_hash = db.globals().start_block_hash; Ok(Self { db, chain_head, start_block_hash, - recent_included_txs, + recent_included_injected_txs, + recent_included_shielded_txs, latest_states, }) } - /// Determine [`TxValidity`] for one injected transaction. + /// Determine [`TxValidity`] for one injected or shielded transaction. pub fn check_tx_validity(&self, tx: TransactionRef<'_>) -> Result { - let TransactionRef::Injected(injected_tx) = tx else { - todo!("Shielded transaction validity"); - }; - let reference_block = tx.reference_block(); + match tx { + TransactionRef::Injected(tx) => self.check_injected_validity(tx), + TransactionRef::Shielded(tx) => self.check_shielded_validity(tx), + } + } - if injected_tx.data().value != 0 { + fn check_injected_validity(&self, tx: &SignedInjectedTransaction) -> Result { + let reference_block = tx.data().reference_block; + + if tx.data().value != 0 { return Ok(TxValidity::NonZeroValue); } @@ -148,20 +158,19 @@ impl TxValidityChecker { return Ok(TxValidity::NotOnCurrentBranch); } - let tx_hash = tx.hash(); - if self.recent_included_txs.contains(&tx_hash) { + let tx_hash = tx.data().to_hash(); + if self.recent_included_injected_txs.contains(&tx_hash) { return Ok(TxValidity::Duplicate); } - let Some(destination_state_hash) = self.latest_states.get(&injected_tx.data().destination) - else { + let Some(destination_state_hash) = self.latest_states.get(&tx.data().destination) else { return Ok(TxValidity::UnknownDestination); }; let Some(state) = self.db.program_state(destination_state_hash.hash) else { anyhow::bail!( "program state not found for actor({}) by valid hash({})", - injected_tx.data().destination, + tx.data().destination, destination_state_hash.hash ); }; @@ -177,6 +186,25 @@ impl TxValidityChecker { Ok(TxValidity::Valid) } + fn check_shielded_validity(&self, tx: &SignedShieldedTransaction) -> Result { + let reference_block = tx.data().reference_block; + + if !self.is_reference_block_within_validity_window(reference_block)? { + return Ok(TxValidity::Outdated); + } + + if !self.is_reference_block_on_current_branch(reference_block)? { + return Ok(TxValidity::NotOnCurrentBranch); + } + + let tx_hash = tx.data().to_hash(); + if self.recent_included_shielded_txs.contains(&tx_hash) { + return Ok(TxValidity::Duplicate); + } + + Ok(TxValidity::Valid) + } + fn is_reference_block_within_validity_window(&self, reference_block: H256) -> Result { let Some(reference_block_height) = self.db.block_header(reference_block).map(|h| h.height) else { @@ -211,12 +239,12 @@ impl TxValidityChecker { } /// Walk back `VALIDITY_WINDOW` MBs through `mb_compact_block(..).parent`, - /// decoding each MB's operations blob and harvesting the hashes - /// of every [`Operation::Injected`] for the dedup set. + /// decoding each MB's operations blob and harvesting the hashes of + /// every [`Operation::Injected`] and [`Operation::Shielded`] for the + /// dedup sets. /// - /// NOTE: Not bound to an instance — exposed `pub` so that - /// `[`crate::EthexeExternalities`]` can build the dedup set - /// independently of constructing a full checker. + /// NOTE: Not bound to an instance, so callers can build the dedup + /// sets independently of constructing a full checker. /// /// A missing `mb_compact_block` / `operations` row on the walk is /// treated like reaching the start of our locally-tracked history: @@ -225,8 +253,13 @@ impl TxValidityChecker { pub fn collect_recent_included_txs( db: &Database, parent_mb: H256, - ) -> Result>> { - let mut txs = HashSet::new(); + ) -> Result<( + HashSet>, + HashSet>, + )> { + let mut injected_txs = HashSet::new(); + let mut shielded_txs = HashSet::new(); + let mut mb_hash = parent_mb; for _ in 0..VALIDITY_WINDOW { if mb_hash.is_zero() { @@ -242,13 +275,19 @@ impl TxValidityChecker { break; }; for op in operations.into_iter() { - if let Operation::Injected(signed) = op { - txs.insert(signed.data().to_hash()); + match op { + Operation::Injected(signed) => { + injected_txs.insert(signed.data().to_hash()); + } + Operation::Shielded(signed) => { + shielded_txs.insert(signed.data().to_hash()); + } + _ => {} } } mb_hash = cb.parent; } - Ok(txs) + Ok((injected_txs, shielded_txs)) } } @@ -368,7 +407,9 @@ mod tests { MaybeHashOf, PrivateKey, SignedMessage, StateHashWithQueueSize, db::{CompactMb, MbStorageRW, OnChainStorageRW}, gear_core::program::MemoryInfix, - injected::{InjectedTransaction, SignedInjectedTransaction, Transaction}, + injected::{ + InjectedTransaction, SignedInjectedTransaction, SignedShieldedTransaction, Transaction, + }, malachite::Operations, mock::{BlockChain, Mock, Tap}, }; @@ -410,6 +451,18 @@ mod tests { sign_injected_tx(test_injected_transaction(reference_block, ActorId::zero())).into() } + fn sign_shielded_tx(tx: InjectedTransaction) -> SignedShieldedTransaction { + let mut rng = gear_tdec::rand_utils::test_rng(); + let dealer_out = gear_tdec::deal::(3, 2, &mut rng); + let shielded_tx = tx.shield(&dealer_out.public_key, &mut rng).unwrap(); + + SignedMessage::create(PrivateKey::random(), shielded_tx).unwrap() + } + + fn mock_shielded_tx(reference_block: H256) -> Transaction { + sign_shielded_tx(test_injected_transaction(reference_block, ActorId::zero())).into() + } + fn program_state(initialized: bool, executable_balance: u128) -> ProgramState { ProgramState { program: Program::Active(ActiveProgram { @@ -464,12 +517,26 @@ mod tests { executable_balance: u128, parent_mb: H256, ) -> H256 { - let ops = Operations::new( + setup_mb_with_ops( + db, injected_transactions .into_iter() .map(Operation::Injected) .collect(), - ); + destination_initialized, + executable_balance, + parent_mb, + ) + } + + fn setup_mb_with_ops( + db: &Database, + operations: Vec, + destination_initialized: bool, + executable_balance: u128, + parent_mb: H256, + ) -> H256 { + let ops = Operations::new(operations); let operations_hash = db.set_operations(ops); let mb_hash = H256::random(); db.set_mb_compact_block( @@ -545,6 +612,53 @@ mod tests { ); } + #[test] + fn test_check_shielded_tx_validity() { + let db = Database::memory(); + let chain = test_block_chain(100).setup(&db); + + let chain_head = chain.blocks[VALIDITY_WINDOW as usize].to_simple(); + let parent_mb = setup_mb( + &db, + vec![], + true, + chain.mb_hash_at(VALIDITY_WINDOW as usize - 1), + ); + let tx_checker = TxValidityChecker::new_for_mb(db.clone(), chain_head, parent_mb).unwrap(); + + for block in chain.blocks.iter().skip(1).take(VALIDITY_WINDOW as usize) { + let tx = mock_shielded_tx(block.hash); + assert_eq!( + TxValidity::Valid, + tx_checker.check_tx_validity(tx.as_ref()).unwrap() + ); + } + } + + #[test] + fn test_check_shielded_tx_duplicate() { + let db = Database::memory(); + let chain = test_block_chain(100).setup(&db); + + let chain_head = chain.blocks[9].to_simple(); + let shielded_tx = + sign_shielded_tx(test_injected_transaction(chain_head.hash, ActorId::zero())); + let tx = Transaction::Shielded(shielded_tx.clone()); + let parent_mb = setup_mb_with_ops( + &db, + vec![Operation::Shielded(shielded_tx)], + true, + MIN_EXECUTABLE_BALANCE_FOR_INJECTED_MESSAGES, + chain.mb_hash_at(8), + ); + let tx_checker = TxValidityChecker::new_for_mb(db.clone(), chain_head, parent_mb).unwrap(); + + assert_eq!( + TxValidity::Duplicate, + tx_checker.check_tx_validity(tx.as_ref()).unwrap() + ); + } + /// Port of master's `test_check_tx_outdated`. #[test] fn test_check_tx_outdated() { diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index cb8baadd3b3..dff2ac0989f 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -843,7 +843,7 @@ impl Service { } => { let acceptance = match malachite.as_mut() { Some(malachite) => { - malachite.receive_injected_transaction(*transaction).into() + malachite.receive_transaction(*transaction).into() } None => InjectedTransactionAcceptance::Reject { reason: "no malachite service to handle transaction".into(), @@ -890,8 +890,7 @@ impl Service { let mut local_acceptance = None; if let Some(malachite) = malachite.as_mut() { - let status = - malachite.receive_injected_transaction(transaction.clone()); + let status = malachite.receive_transaction(transaction.clone()); local_acceptance = Some(InjectedTransactionAcceptance::from(status)); } From 8535008d43bdae73743e091681175b22e0c24bc1 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 17 Jun 2026 16:27:05 +0300 Subject: [PATCH 12/41] chore(db): put/get ShieldedTransaction to database --- Cargo.lock | 1 + ethexe/common/src/db.rs | 18 ++- ethexe/common/src/malachite.rs | 22 +-- ethexe/db/Cargo.toml | 1 + ethexe/db/src/database.rs | 66 ++++++--- ethexe/malachite/service/Cargo.toml | 1 + ethexe/malachite/service/src/externalities.rs | 127 +++++++++++------- 7 files changed, 149 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2f30c7f0d1..7af2b7c56ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5704,6 +5704,7 @@ dependencies = [ "ethexe-common", "ethexe-ethereum", "ethexe-runtime-common", + "ferveo-gear-tdec", "flate2", "futures", "gear-core", diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index 88b96be0a41..817ef5d6df1 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -4,14 +4,14 @@ //! Common db types and traits. #[cfg(feature = "shielded")] -use crate::injected::SignedTxReceipt; +use crate::injected::{ShieldedTransaction, SignedShieldedTransaction, SignedTxReceipt}; use crate::{ - Address, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, ProtocolTimelines, Schedule, - SimpleBlockData, ValidatorsVec, events::BlockEvent, gear::StateTransition, injected::{InjectedTransaction, Promise, SignedInjectedTransaction}, malachite::Operations, + Address, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, ProtocolTimelines, Schedule, + SimpleBlockData, ValidatorsVec, }; use alloc::{ collections::{BTreeSet, VecDeque}, @@ -117,6 +117,13 @@ pub trait InjectedStorageRO { hash: HashOf, ) -> Option; + #[cfg(feature = "shielded")] + /// Returns the shielded transaction by its hash. + fn shielded_transaction( + &self, + hash: HashOf, + ) -> Option; + /// Returns the promise by its transaction hash. fn promise(&self, hash: HashOf) -> Option; @@ -129,6 +136,9 @@ pub trait InjectedStorageRO { pub trait InjectedStorageRW: InjectedStorageRO { fn set_injected_transaction(&self, tx: SignedInjectedTransaction); + #[cfg(feature = "shielded")] + fn set_shielded_transaction(&self, tx: SignedShieldedTransaction); + fn set_promise(&self, promise: &Promise); #[cfg(feature = "shielded")] @@ -265,7 +275,7 @@ mod tests { use super::*; // use crate::malachite::Operations; use indoc::formatdoc; - use scale_info::{PortableRegistry, Registry, meta_type}; + use scale_info::{meta_type, PortableRegistry, Registry}; use sha3::{Digest, Sha3_256}; #[test] diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index 4b05ce45195..4c1ae44aa4c 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -28,6 +28,8 @@ //! depending on the consensus layer. use crate::injected::SignedInjectedTransaction; +#[cfg(feature = "shielded")] +use crate::{HashOf, injected::ShieldedTransaction}; use alloc::vec::Vec; use derive_more::{Deref, DerefMut, IntoIterator}; use gprimitives::H256; @@ -172,32 +174,18 @@ pub struct VotingExtension { } /// One validator's decryption-share payload for one shielded transaction. -/// Holds [DecryptionShareSimple] over [ShieldedTransaction]. +/// Holds [`DecryptionShareSimple`] over [`ShieldedTransaction`]. /// /// [ShieldedTransaction]: crate::injected::ShieldedTransaction #[cfg(feature = "shielded")] #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct VotingDecryptionShare { - pub tx_hash: H256, + /// Transaction hash decryption share belongs to. + pub tx_hash: HashOf, pub share: DecryptionShareSimple, } -// #[cfg(feature = "shielded")] -// impl VotingDecryptionShare { -// pub fn from_shielded_tx(shielded_tx: &ShieldedTransaction) -> gear_tdec::Result { -// let ciphertext_header = shielded_tx.ciphertext.header()?; -// let share = DecryptionShareSimple::create( -// validator_decryption_key, -// private_key_share, -// &ciphertext_header, -// shielded_tx.aad.as_ref(), -// )?; -// let tx_hash = shielded_tx.to_hash(); -// Ok(Self { tx_hash, share }) -// } -// } - #[cfg(test)] mod tests { use super::*; diff --git a/ethexe/db/Cargo.toml b/ethexe/db/Cargo.toml index 1e6105b9302..6b1a34af170 100644 --- a/ethexe/db/Cargo.toml +++ b/ethexe/db/Cargo.toml @@ -51,6 +51,7 @@ version = "0.21" scopeguard.workspace = true tempfile.workspace = true ethexe-common = { workspace = true, features = ["mock"] } +gear-tdec.workspace = true indoc.workspace = true scale-info = { workspace = true, features = ["docs"] } sha3.workspace = true diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index 19d3103ed2f..34633a34085 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -19,7 +19,10 @@ use ethexe_common::{ }, events::BlockEvent, gear::StateTransition, - injected::{InjectedTransaction, Promise, SignedInjectedTransaction, SignedTxReceipt}, + injected::{ + InjectedTransaction, Promise, ShieldedTransaction, SignedInjectedTransaction, + SignedShieldedTransaction, SignedTxReceipt, + }, malachite::Operations, }; use ethexe_runtime_common::state::{ @@ -67,6 +70,7 @@ enum Key { Promise(HashOf) = 26, TxReceipt(HashOf) = 27, + ShieldedTransaction(HashOf) = 28, } impl Key { @@ -99,6 +103,7 @@ impl Key { Self::InjectedTransaction(hash) | Self::Promise(hash) | Self::TxReceipt(hash) => { bytes.extend(hash.as_ref()) } + Self::ShieldedTransaction(hash) => bytes.extend(hash.as_ref()), Self::ProgramToCodeId(program_id) => bytes.extend(program_id.as_ref()), @@ -671,6 +676,18 @@ impl InjectedStorageRO for RawDatabase { }) } + fn shielded_transaction( + &self, + hash: HashOf, + ) -> Option { + self.kv + .get(&Key::ShieldedTransaction(hash).to_bytes()) + .map(|data| { + SignedShieldedTransaction::decode(&mut data.as_slice()) + .expect("Failed to decode data into `SignedShieldedTransaction`") + }) + } + 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") @@ -696,6 +713,14 @@ impl InjectedStorageRW for RawDatabase { .put(&Key::InjectedTransaction(tx_hash).to_bytes(), tx.encode()); } + fn set_shielded_transaction(&self, tx: SignedShieldedTransaction) { + let tx_hash = tx.data().to_hash(); + + tracing::trace!(shielded_tx_hash = ?tx_hash, "Set shielded transaction"); + self.kv + .put(&Key::ShieldedTransaction(tx_hash).to_bytes(), tx.encode()); + } + fn set_promise(&self, promise: &Promise) { tracing::trace!(?promise, "Set promise for injected transaction"); @@ -961,6 +986,7 @@ impl OnChainStorageRW for Database { impl InjectedStorageRO for Database { delegate!(to self.raw { fn injected_transaction(&self, hash: HashOf) -> Option; + fn shielded_transaction(&self, hash: HashOf) -> Option; fn promise(&self, hash: HashOf) -> Option; fn receipt(&self, hash: HashOf) -> Option; }); @@ -991,6 +1017,7 @@ impl MbStorageRW for Database { impl InjectedStorageRW for Database { delegate!(to self.raw { fn set_injected_transaction(&self, tx: SignedInjectedTransaction); + fn set_shielded_transaction(&self, tx: SignedShieldedTransaction); fn set_promise(&self, promise: &Promise); fn set_receipt(&self, receipt: &SignedTxReceipt); }); @@ -1079,33 +1106,40 @@ mod tests { use ethexe_common::{ ecdsa::PrivateKey, events::{RouterEvent, router::StorageSlotChangedEvent}, + mock::Mock, }; - use gear_core::{ - code::{InstantiatedSectionSizes, InstrumentationStatus}, - limited::LimitedVec, - }; + use gear_core::code::{InstantiatedSectionSizes, InstrumentationStatus}; + use gsigner::SignedMessage; #[test] fn test_injected_transaction() { let db = Database::memory(); let private_key = PrivateKey::from_seed([1; 32]).expect("valid seed"); - let tx = SignedInjectedTransaction::create( - private_key, - InjectedTransaction { - destination: ActorId::zero(), - payload: LimitedVec::new(), - value: 0, - reference_block: H256::random(), - salt: LimitedVec::new(), - }, - ) - .unwrap(); + let tx = SignedMessage::create(private_key, InjectedTransaction::mock(())).unwrap(); let tx_hash = tx.data().to_hash(); db.set_injected_transaction(tx.clone()); assert_eq!(db.injected_transaction(tx_hash), Some(tx)); } + #[test] + fn test_shielded_transaction() { + let db = Database::memory(); + + let mut rng = gear_tdec::rand_utils::test_rng(); + let dealer_out = gear_tdec::deal::(3, 2, &mut rng); + + let shielded_tx = InjectedTransaction::mock(()) + .shield(&dealer_out.public_key, &mut rng) + .unwrap(); + let tx = SignedMessage::create(PrivateKey::random(), shielded_tx).unwrap(); + let tx_hash = tx.data().to_hash(); + + db.set_shielded_transaction(tx.clone()); + + assert_eq!(db.shielded_transaction(tx_hash), Some(tx)); + } + #[test] fn test_block_events() { let db = Database::memory(); diff --git a/ethexe/malachite/service/Cargo.toml b/ethexe/malachite/service/Cargo.toml index 8025dc4a40b..36984b591ba 100644 --- a/ethexe/malachite/service/Cargo.toml +++ b/ethexe/malachite/service/Cargo.toml @@ -18,6 +18,7 @@ futures.workspace = true parity-scale-codec.workspace = true tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "sync", "time"] } tracing.workspace = true +gear-tdec.workspace = true # Generic Malachite-backed consensus service. Carries the engine, # libp2p swarm, store, and codec; ethexe-malachite only ships the diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 623fe9c8f80..a6879184a5e 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -48,8 +48,8 @@ use bytes::Bytes; use ethexe_common::{ MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, db::{CompactMb, GlobalsStorageRO, GlobalsStorageRW, MbStorageRO, MbStorageRW}, - injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, Transaction, TransactionRef}, - malachite::{Operation, Operations, VotingExtension}, + injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, Transaction}, + malachite::{Operation, Operations, VotingDecryptionShare, VotingExtension}, }; use ethexe_db::Database; use ethexe_malachite_core::{ @@ -64,42 +64,6 @@ use std::{ use tokio::sync::{Notify, mpsc}; use tracing::{debug, error, info, warn}; -/// Optimization for reducing `clone` operation for potentially large transactions. -fn operation_to_transaction(operation: &Operation) -> Option> { - match operation { - Operation::Injected(tx) => Some(TransactionRef::Injected(tx)), - Operation::Shielded(tx) => Some(TransactionRef::Shielded(tx)), - _ => None, - } -} - -fn transaction_ref_hash(transaction: TransactionRef<'_>) -> H256 { - match transaction { - TransactionRef::Injected(tx) => tx.data().to_hash().inner(), - TransactionRef::Shielded(tx) => tx.data().to_hash().inner(), - } -} - -fn transaction_to_operation(transaction: Transaction) -> Operation { - match transaction { - Transaction::Injected(tx) => Operation::Injected(tx), - Transaction::Shielded(_) => todo!("Shielded transaction block inclusion"), - } -} - -fn decode_voting_extensions( - extensions: Vec<(MalachiteAddress, Bytes)>, -) -> Result> { - extensions - .into_iter() - .map(|(address, bytes)| { - VotingExtension::decode_all(&mut bytes.as_ref()) - .map(|extension| (address, extension)) - .map_err(|e| anyhow!("decoding voting extension from {address}: {e}")) - }) - .collect() -} - /// Inputs the externalities need to satisfy the [`ethexe_malachite_core::Externalities`] /// contract. Constructed by [`crate::MalachiteService::new`] and /// handed to the inner ethexe-malachite-core service inside an [`Arc`]. @@ -235,7 +199,7 @@ impl Externalities for EthexeExternalities { // back in before they age out. let transactions = payload .iter() - .filter_map(operation_to_transaction) + .filter_map(utils::operation_to_transaction) .collect::>(); if !transactions.is_empty() { self.mempool.forget(&transactions).await; @@ -252,7 +216,7 @@ impl Externalities for EthexeExternalities { mb_hash, signatures: cert.signatures, }; - let voting_extensions = decode_voting_extensions(extensions)?; + let voting_extensions = utils::decode_voting_extensions(extensions)?; if !voting_extensions.is_empty() { info!( validators = voting_extensions.len(), @@ -294,13 +258,25 @@ impl Externalities for EthexeExternalities { _mb: Block, extension: Bytes, ) -> Result { - match VotingExtension::decode_all(&mut extension.as_ref()) { - Ok(_) => Ok(true), + let decryption_shares = match VotingExtension::decode_all(&mut extension.as_ref()) { + Ok(voting_extension) => voting_extension.decryption_shares, Err(e) => { warn!(error = %e, "verify_vote_extension: undecodable extension"); - Ok(false) + return Ok(false); } + }; + + for VotingDecryptionShare { .. } in decryption_shares { + // let shielded_tx = + // let v = gear_tdec::verify_decryption_shares_simple( + // pub_contexts, + // ciphertext, + // decryption_shares, + // ); + // let _v = share.validator_checksum; } + + Ok(true) } async fn build_block_above(&self, parent_mb_hash: H256) -> Result { @@ -336,7 +312,7 @@ impl Externalities for EthexeExternalities { TxValidity::Valid => accepted.push(tx), reason => { warn!( - tx_hash = %transaction_ref_hash(tx.as_ref()), + tx_hash = %utils::transaction_ref_hash(tx.as_ref()), ?reason, "build_block_above: dropping transaction — fails TxValidity", ); @@ -432,7 +408,7 @@ impl Externalities for EthexeExternalities { operations.push(Operation::AdvanceTillEthereumBlock { block_hash }); } for tx in capped { - operations.push(transaction_to_operation(tx)); + operations.push(utils::transaction_to_operation(tx)); } operations.push(Operation::ProgressTasks); operations.push(Operation::ProcessQueuesV2 { @@ -465,7 +441,8 @@ impl Externalities for EthexeExternalities { Operation::AdvanceTillEthereumBlock { .. } | Operation::ProgressTasks | Operation::ProcessQueuesV2 { .. } - | Operation::Injected(_) => { + | Operation::Injected(_) + | Operation::Shielded(_) => { // Known and allowed. } op => { @@ -615,7 +592,7 @@ impl Externalities for EthexeExternalities { // since the checker has no anchor to walk from. let has_injected = operations .iter() - .any(|tx| operation_to_transaction(tx).is_some()); + .any(|tx| utils::operation_to_transaction(tx).is_some()); if has_injected { warn!("validate: MB carries injected txs but no local chain head — abstaining"); return Ok(false); @@ -632,7 +609,7 @@ impl Externalities for EthexeExternalities { // local DB corruption, not a peer-side issue. let checker = TxValidityChecker::new_for_mb(self.db.clone(), chain_head, parent_hash)?; for op in operations.iter() { - let Some(transaction) = operation_to_transaction(op) else { + let Some(transaction) = utils::operation_to_transaction(op) else { continue; }; // `?` inside `check_tx_validity` only fires on local DB @@ -644,7 +621,7 @@ impl Externalities for EthexeExternalities { TxValidity::Valid => {} reason => { warn!( - tx_hash = %transaction_ref_hash(transaction), + tx_hash = %utils::transaction_ref_hash(transaction), ?reason, "validate: transaction fails TxValidity — rejecting MB", ); @@ -830,6 +807,54 @@ impl EthexeExternalities { } } +mod utils { + use anyhow::{Result, anyhow}; + use bytes::Bytes; + use ethexe_common::{ + injected::{Transaction, TransactionRef}, + malachite::{Operation, VotingExtension}, + }; + use ethexe_malachite_core::Address as MalachiteAddress; + use gprimitives::H256; + use parity_scale_codec::DecodeAll; + + /// Optimization for reducing `clone` operation for potentially large transactions. + pub(crate) fn operation_to_transaction(operation: &Operation) -> Option> { + match operation { + Operation::Injected(tx) => Some(TransactionRef::Injected(tx)), + Operation::Shielded(tx) => Some(TransactionRef::Shielded(tx)), + _ => None, + } + } + + pub(crate) fn transaction_ref_hash(transaction: TransactionRef<'_>) -> H256 { + match transaction { + TransactionRef::Injected(tx) => tx.data().to_hash().inner(), + TransactionRef::Shielded(tx) => tx.data().to_hash().inner(), + } + } + + pub(crate) fn transaction_to_operation(transaction: Transaction) -> Operation { + match transaction { + Transaction::Injected(tx) => Operation::Injected(tx), + Transaction::Shielded(_) => todo!("Shielded transaction block inclusion"), + } + } + + pub(crate) fn decode_voting_extensions( + extensions: Vec<(MalachiteAddress, Bytes)>, + ) -> Result> { + extensions + .into_iter() + .map(|(address, bytes)| { + VotingExtension::decode_all(&mut bytes.as_ref()) + .map(|extension| (address, extension)) + .map_err(|e| anyhow!("decoding voting extension from {address}: {e}")) + }) + .collect() + } +} + #[cfg(test)] mod tests { use super::*; @@ -838,7 +863,9 @@ mod tests { use ethexe_common::{ BlockHeader, HashOf, db::{BlockMetaStorageRW, OnChainStorageRW}, - injected::{InjectedTransaction, PurgedTransaction, SignedInjectedTransaction}, + injected::{ + InjectedTransaction, PurgedTransaction, SignedInjectedTransaction, TransactionRef, + }, }; fn to_payload(bytes: Vec) -> BlockPayload { From e59c9b960ad8056e06949c47c0c210c9dcbd2bd7 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Thu, 18 Jun 2026 13:08:39 +0300 Subject: [PATCH 13/41] chore: add TdecKeyStore to EthexeExternalities --- Cargo.lock | 6 +- ethexe/common/src/injected.rs | 11 +--- ethexe/malachite/service/Cargo.toml | 2 +- ethexe/malachite/service/src/externalities.rs | 60 +++++++++++++------ ethexe/malachite/service/src/service.rs | 2 + protocol/gsigner/src/lib.rs | 18 ++++-- protocol/gsigner/src/tdec.rs | 28 ++++----- 7 files changed, 78 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7af2b7c56ce..0e2aa033108 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6232,7 +6232,7 @@ dependencies = [ [[package]] name = "ferveo-gear-common" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#521a2462205e1995a2d7c69675fca96ab80b9321" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#a8cabb84093e9c2f26e604834146d35a06a90b18" dependencies = [ "ark-ec 0.5.0", "ark-serialize 0.5.0", @@ -6248,7 +6248,7 @@ dependencies = [ [[package]] name = "ferveo-gear-tdec" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#521a2462205e1995a2d7c69675fca96ab80b9321" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#a8cabb84093e9c2f26e604834146d35a06a90b18" dependencies = [ "ark-bls12-381 0.5.0", "ark-ec 0.5.0", @@ -18877,7 +18877,7 @@ dependencies = [ [[package]] name = "subproductdomain-gear" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#521a2462205e1995a2d7c69675fca96ab80b9321" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#a8cabb84093e9c2f26e604834146d35a06a90b18" dependencies = [ "anyhow", "ark-ec 0.5.0", diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index 09951ade945..8968736ae57 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -550,13 +550,6 @@ impl Transaction { Self::Shielded(_) => None, } } - - pub fn into_injected(self) -> Option { - match self { - Self::Injected(tx) => Some(tx), - Self::Shielded(_) => None, - } - } } /// Mirroring [Transaction] type, but stores internally references to @@ -664,8 +657,8 @@ mod tests { /// ``` #[test] fn ark_noble_js_compatible_serialization() { - const NOBLE_JS_G1_123_COMPRESSED_SERIALIZED: &'static str = r#""0xa0ec3e71a719a25208adc97106b122809210faf45a17db24f10ffb1ac014fac1ab95a4a1967e55b185d4df622685b9e8""#; - const NOBLE_JS_G2_123_COMPRESSED_SERIALIZED: &'static str = r#""0x95e18bbdb8b7bd39ea677ee923d7e87af449c45209e635907a4a8a2e4c65fff97c46d038cff53a994da273310ac85866096a5e13fd3ebf4e140e26f6ddfac66651e04e530e6045572acab753bb1bcef990fe14b4426caee41016af69d313750d""#; + const NOBLE_JS_G1_123_COMPRESSED_SERIALIZED: &str = r#""0xa0ec3e71a719a25208adc97106b122809210faf45a17db24f10ffb1ac014fac1ab95a4a1967e55b185d4df622685b9e8""#; + const NOBLE_JS_G2_123_COMPRESSED_SERIALIZED: &str = r#""0x95e18bbdb8b7bd39ea677ee923d7e87af449c45209e635907a4a8a2e4c65fff97c46d038cff53a994da273310ac85866096a5e13fd3ebf4e140e26f6ddfac66651e04e530e6045572acab753bb1bcef990fe14b4426caee41016af69d313750d""#; #[derive(serde::Serialize, serde::Deserialize)] #[serde(transparent)] diff --git a/ethexe/malachite/service/Cargo.toml b/ethexe/malachite/service/Cargo.toml index 36984b591ba..d6c4ecc725f 100644 --- a/ethexe/malachite/service/Cargo.toml +++ b/ethexe/malachite/service/Cargo.toml @@ -29,7 +29,7 @@ ethexe-malachite-core.workspace = true ethexe-common = { workspace = true, features = ["std"] } ethexe-db = { workspace = true, default-features = false } ethexe-runtime-common = { workspace = true, features = ["std"] } -gsigner = { workspace = true, features = ["std", "secp256k1", "codec", "keyring", "serde"] } +gsigner = { workspace = true, features = ["std", "secp256k1", "codec", "keyring", "serde", "tdec"] } gprimitives = { workspace = true, features = ["std"] } gear-workspace-hack.workspace = true diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index a6879184a5e..854cd30c3a6 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -47,8 +47,10 @@ use async_trait::async_trait; use bytes::Bytes; use ethexe_common::{ MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, - db::{CompactMb, GlobalsStorageRO, GlobalsStorageRW, MbStorageRO, MbStorageRW}, - injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, Transaction}, + db::{ + CompactMb, GlobalsStorageRO, GlobalsStorageRW, InjectedStorageRO, MbStorageRO, MbStorageRW, + }, + injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, ShieldedFields, Transaction}, malachite::{Operation, Operations, VotingDecryptionShare, VotingExtension}, }; use ethexe_db::Database; @@ -56,19 +58,24 @@ use ethexe_malachite_core::{ Address as MalachiteAddress, Block, BlockPayload, Externalities, MAX_BLOCK_PAYLOAD_BYTES, }; use gprimitives::H256; +use gsigner::tdec::TdecKeyStore; use parity_scale_codec::{DecodeAll, Encode}; use std::{ collections::VecDeque, sync::{Arc, Mutex, RwLock}, }; use tokio::sync::{Notify, mpsc}; -use tracing::{debug, error, info, warn}; +use tracing::{debug, error, info, trace, warn}; /// Inputs the externalities need to satisfy the [`ethexe_malachite_core::Externalities`] /// contract. Constructed by [`crate::MalachiteService::new`] and /// handed to the inner ethexe-malachite-core service inside an [`Arc`]. pub(crate) struct EthexeExternalities { + /// Ethexe database. pub(crate) db: Database, + /// + pub(crate) tdec_store: TdecKeyStore, + /// Validator's local transaction pool. pub(crate) mempool: Arc, /// Latest Ethereum chain head observed via the outer /// [`crate::MalachiteService::receive_new_chain_head`]. The @@ -242,14 +249,26 @@ impl Externalities for EthexeExternalities { let payload = Operations::decode_all(&mut mb.payload.as_ref()) .map_err(|e| anyhow!("decoding Operations for voting extension: {e}"))?; - let has_shielded = payload + let decryption_shares = payload .iter() - .any(|op| matches!(op, Operation::Shielded(_))); - if !has_shielded { - return Ok(None); - } + .filter_map(|op| op.as_shielded().map(|tx| tx.data())) + .filter_map(|tx| { + let _r = &self.tdec_store; + // self.tdec_store + // .create_share(public_context, &tx.ciphertext.header(), tx.aad.as_ref()) + // .map(|share| VotingDecryptionShare { + // tx_hash: tx.to_hash(), + // share, + // }) + // .ok() + None + }) + .collect::>(); - Ok(Some(Bytes::from(VotingExtension::default().encode()))) + Ok(decryption_shares.is_empty().then(|| { + let extension = VotingExtension { decryption_shares }; + Bytes::from(extension.encode()) + })) } async fn verify_vote_extension( @@ -266,14 +285,17 @@ impl Externalities for EthexeExternalities { } }; - for VotingDecryptionShare { .. } in decryption_shares { - // let shielded_tx = - // let v = gear_tdec::verify_decryption_shares_simple( - // pub_contexts, - // ciphertext, - // decryption_shares, - // ); - // let _v = share.validator_checksum; + for VotingDecryptionShare { tx_hash, share } in decryption_shares { + let Some(shielded_tx) = self.db.shielded_transaction(tx_hash) else { + trace!(%tx_hash, "validator provide decryption share for not existed transaction"); + return Ok(false); + }; + let ciphertext = &shielded_tx.data().ciphertext; + let _shares = [share]; + // let is_correct = gear_tdec::verify_decryption_shares_simple::< + // gear_tdec::bls12_381::E, + // ShieldedFields, + // >(pub_contexts, &ciphertext, &shares); } Ok(true) @@ -904,6 +926,7 @@ mod tests { let (event_tx, event_rx) = mpsc::unbounded_channel(); let ext = EthexeExternalities { db, + tdec_store: TdecKeyStore::memory(), mempool: Arc::new(EmptyMempool), chain_head: Arc::new(RwLock::new(None)), chain_head_notify: Arc::new(Notify::new()), @@ -1320,6 +1343,7 @@ mod tests { let (event_tx, mut event_rx) = mpsc::unbounded_channel(); let ext = EthexeExternalities { db: db.clone(), + tdec_store: TdecKeyStore::memory(), mempool: Arc::clone(&tracker) as Arc, chain_head: Arc::new(RwLock::new(None)), chain_head_notify: Arc::new(Notify::new()), @@ -1386,6 +1410,7 @@ mod tests { let (event_tx, event_rx) = mpsc::unbounded_channel(); let ext = EthexeExternalities { db, + tdec_store: TdecKeyStore::memory(), mempool: mempool as Arc, chain_head: Arc::new(RwLock::new(None)), chain_head_notify: Arc::new(Notify::new()), @@ -2137,6 +2162,7 @@ mod tests { let (event_tx, _event_rx) = mpsc::unbounded_channel(); let ext = EthexeExternalities { db: db.clone(), + tdec_store: TdecKeyStore::memory(), mempool: Arc::new(EmptyMempool), chain_head: Arc::new(RwLock::new(Some(head))), chain_head_notify: Arc::new(Notify::new()), diff --git a/ethexe/malachite/service/src/service.rs b/ethexe/malachite/service/src/service.rs index 113ba8eac41..411d38d30f9 100644 --- a/ethexe/malachite/service/src/service.rs +++ b/ethexe/malachite/service/src/service.rs @@ -156,6 +156,8 @@ impl MalachiteService { let externalities = Arc::new(EthexeExternalities { db, + // TODO: FIXME (temporary solution) + tdec_store: gsigner::TdecKeyStore::memory(), mempool: Arc::clone(&mempool), chain_head: Arc::clone(&chain_head), chain_head_notify: Arc::clone(&chain_head_notify), diff --git a/protocol/gsigner/src/lib.rs b/protocol/gsigner/src/lib.rs index b59670dc238..141a54ad68d 100644 --- a/protocol/gsigner/src/lib.rs +++ b/protocol/gsigner/src/lib.rs @@ -97,10 +97,20 @@ pub use storage::{FilesystemBackend, MemoryBackend, StorageBackend, StorageError feature = "serde", feature = "tdec" ))] -pub use tdec::{ - TdecBlindedKeyShare, TdecCiphertextHeader, TdecDecryptionKey, TdecDecryptionShare, - TdecKeyEntry, TdecKeyStore, TdecKeypair, TdecPublicDecryptionContext, TdecPublicKey, -}; +mod tdec_exports { + pub use crate::tdec::{ + BlindedKeyShare, PublicDecryptionContext, TdecDecryptionKey, TdecKeyEntry, TdecKeyStore, + TdecKeypair, TdecPublicKey, + }; + pub use gear_tdec::bls12_381::{CiphertextHeader, DecryptionShareSimple as DecryptionShare}; +} +#[cfg(all( + feature = "std", + feature = "keyring", + feature = "serde", + feature = "tdec" +))] +pub use tdec_exports::*; #[cfg(feature = "secp256k1")] pub use schemes::secp256k1::{ diff --git a/protocol/gsigner/src/tdec.rs b/protocol/gsigner/src/tdec.rs index 8a41446c757..ba2b2bcf4f1 100644 --- a/protocol/gsigner/src/tdec.rs +++ b/protocol/gsigner/src/tdec.rs @@ -13,8 +13,8 @@ use crate::{ }; use ferveo_common::{Keypair, PublicKey, from_bytes, to_bytes}; use gear_tdec::{ - BlindedKeyShare, CiphertextHeader, DecryptionShareSimple, DomainPoint, - PublicDecryptionContextSimple, bls12_381::E, + DomainPoint, PublicDecryptionContextSimple, + bls12_381::{CiphertextHeader, DecryptionShareSimple as DecryptionShare, E}, }; use hex::ToHex; use serde::{Deserialize, Serialize}; @@ -28,10 +28,8 @@ use tempfile::TempDir; pub type TdecPublicKey = PublicKey; pub type TdecKeypair = Keypair; pub type TdecDecryptionKey = DomainPoint; -pub type TdecBlindedKeyShare = BlindedKeyShare; -pub type TdecCiphertextHeader = CiphertextHeader; -pub type TdecDecryptionShare = DecryptionShareSimple; -pub type TdecPublicDecryptionContext = PublicDecryptionContextSimple; +pub type BlindedKeyShare = gear_tdec::BlindedKeyShare; +pub type PublicDecryptionContext = PublicDecryptionContextSimple; const NAMESPACE_TDEC: &str = "tdec"; @@ -78,13 +76,13 @@ impl KeystoreEntry for TdecKeyEntry { /// `TdecKeyStore` keeps only the validator's private decryption scalar and the /// corresponding public key. It does not store /// [`gear_tdec::PrivateDecryptionContextSimple`]; callers should keep or obtain -/// [`TdecPublicDecryptionContext`] separately and pass it to [`Self::create_share`]. +/// [`PublicDecryptionContext`] separately and pass it to [`Self::create_share`]. /// /// Typical usage: /// /// 1. Import the local validator's `validator_decryption_key` with /// [`Self::import_decryption_key`]. -/// 2. Receive or load a [`TdecPublicDecryptionContext`] containing +/// 2. Receive or load a [`PublicDecryptionContext`] containing /// `validator_public_key` and `blinded_key_share`. /// 3. Call [`Self::create_share`] with the public context, ciphertext header, /// and AAD. The store finds the matching local private scalar by public key @@ -205,10 +203,10 @@ impl TdecKeyStore { /// `gear-tdec`. pub fn create_share( &self, - public_context: &TdecPublicDecryptionContext, - ciphertext_header: &TdecCiphertextHeader, + public_context: &PublicDecryptionContext, + ciphertext_header: &CiphertextHeader, aad: &[u8], - ) -> Result { + ) -> Result { self.create_share_with_blinded_key( &public_context.validator_public_key, &public_context.blinded_key_share, @@ -224,10 +222,10 @@ impl TdecKeyStore { pub fn create_share_with_blinded_key( &self, public_key: &TdecPublicKey, - blinded_key_share: &TdecBlindedKeyShare, - ciphertext_header: &TdecCiphertextHeader, + blinded_key_share: &BlindedKeyShare, + ciphertext_header: &CiphertextHeader, aad: &[u8], - ) -> Result { + ) -> Result { let keypair = self.keypair(public_key)?; blinded_key_share .create_decryption_share_simple(ciphertext_header, aad, &keypair) @@ -336,7 +334,7 @@ mod tests { let public_context = context.public_decryption_contexts[context.index].clone(); let ciphertext = gear_tdec::encrypt_raw::(b"hello", b"aad", &dealer.public_key, &mut rng).unwrap(); - let header = ciphertext.header().unwrap(); + let header = ciphertext.header(); let store = TdecKeyStore::memory(); store .import_decryption_key(context.validator_decryption_key) From 5468b9ac49cf445d9d2ea35a265cbe03aae1d60e Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 19 Jun 2026 15:57:24 +0300 Subject: [PATCH 14/41] chore: remove voting extension, emit event with decryption shares while processing mb proposal --- Cargo.lock | 1 - ethexe/common/Cargo.toml | 1 - ethexe/common/src/malachite.rs | 21 ++-- ethexe/malachite/core/src/app.rs | 59 ++-------- ethexe/malachite/core/src/externalities.rs | 16 --- ethexe/malachite/service/src/externalities.rs | 107 +++++++----------- ethexe/malachite/service/src/lib.rs | 15 ++- ethexe/malachite/service/src/service.rs | 4 + .../service/tests/restart_resilience.rs | 5 + ethexe/service/src/lib.rs | 4 + ethexe/service/src/tests/utils/env.rs | 1 + 11 files changed, 89 insertions(+), 145 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e2aa033108..15600489bb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5616,7 +5616,6 @@ dependencies = [ "alloy-primitives", "anyhow", "ark-ec 0.5.0", - "ark-ff 0.5.0", "ark-serialize 0.5.0", "auto_impl", "derive_more 2.1.1", diff --git a/ethexe/common/Cargo.toml b/ethexe/common/Cargo.toml index 053c8811b45..2ff6dfa0ede 100644 --- a/ethexe/common/Cargo.toml +++ b/ethexe/common/Cargo.toml @@ -31,7 +31,6 @@ k256 = { version = "0.13.4", features = ["ecdsa"], default-features = false } nonempty.workspace = true ark-ec.workspace = true -ark-ff.workspace = true # optional dependencies serde = { workspace = true, optional = true } diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index 15ef533b221..ea79ad44863 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -68,7 +68,7 @@ pub enum Operation { /// User-submitted shielded transaction from mempool. #[cfg(feature = "shielded")] - Shielded(SignedShieldedTransaction) = 6, + Shielded(SignedShieldedTransaction) = 6, // encrypted transactions } impl Operation { @@ -82,16 +82,7 @@ impl Operation { // Mirrors the `#[repr(u32)]` discriminants below and the `Decode` // arms. These three must agree; `operation_encoding_is_frozen` pins // the bytes so a divergence can't slip through. - match self { - Self::AdvanceTillEthereumBlock { .. } => 0, - Self::ProgressTasks => 1, - Self::ProcessQueues { .. } => 2, - Self::Injected(_) => 3, - Self::ProcessQueuesV2 { .. } => 4, - Self::ProcessQueuesV3 { .. } => 5, - #[cfg(feature = "shielded")] - Self::Shielded(_) => 6, - } + unsafe { (self as *const Operation).cast::().read() } } /// Returns `Some` if `Self` contains shielded transaction. @@ -102,6 +93,14 @@ impl Operation { _ => None, } } + + #[cfg(feature = "shielded")] + pub fn into_shielded(self) -> Option { + match self { + Self::Shielded(tx) => Some(tx), + _ => None, + } + } } // Custom encoder/decoder so the discriminant is always a fixed-width `u32` diff --git a/ethexe/malachite/core/src/app.rs b/ethexe/malachite/core/src/app.rs index 02f29406386..1c55f6acc5e 100644 --- a/ethexe/malachite/core/src/app.rs +++ b/ethexe/malachite/core/src/app.rs @@ -171,34 +171,13 @@ where } // Vote extensions. - AppMsg::ExtendVote { - value_id, reply, .. - } => { - let extension = self - .process_extend_vote(value_id) - .await - .unwrap_or_else(|e| { - error!(?e, %value_id, "ExtendVote: process failed"); - None - }); - if reply.send(extension).is_err() { + AppMsg::ExtendVote { reply, .. } => { + if reply.send(self.process_extend_vote()).is_err() { error!("ExtendVote: failed to send reply"); } } - AppMsg::VerifyVoteExtension { - value_id, - reply, - extension, - .. - } => { - let result = self - .process_verify_vote_extension(value_id, extension) - .await - .unwrap_or_else(|e| { - error!(?e, %value_id, "VerifyVoteExtension: process failed"); - Err(VoteExtensionError::InvalidVoteExtension) - }); - if reply.send(result).is_err() { + AppMsg::VerifyVoteExtension { reply, .. } => { + if reply.send(self.process_verify_vote_extension()).is_err() { error!("VerifyVoteExtension: failed to send reply"); } } @@ -426,34 +405,12 @@ where Ok(locally) } - async fn process_extend_vote(&self, value_id: ValueId) -> Result> { - let Some(block) = self.block_by_value_id(value_id)? else { - return Ok(None); - }; - self.externalities - .extend_vote(block.hash(), block) - .await - .context("extend vote") + fn process_extend_vote(&self) -> Option { + None } - async fn process_verify_vote_extension( - &self, - value_id: ValueId, - extension: Bytes, - ) -> Result> { - let Some(block) = self.block_by_value_id(value_id)? else { - return Ok(Err(VoteExtensionError::InvalidVoteExtension)); - }; - let is_valid = self - .externalities - .verify_vote_extension(block.hash(), block, extension) - .await - .context("verify vote extension")?; - Ok(if is_valid { - Ok(()) - } else { - Err(VoteExtensionError::InvalidVoteExtension) - }) + fn process_verify_vote_extension(&self) -> Result<(), VoteExtensionError> { + Ok(()) } fn block_by_value_id(&self, value_id: ValueId) -> Result> { diff --git a/ethexe/malachite/core/src/externalities.rs b/ethexe/malachite/core/src/externalities.rs index 658ea5e8966..4d02ef323cc 100644 --- a/ethexe/malachite/core/src/externalities.rs +++ b/ethexe/malachite/core/src/externalities.rs @@ -61,22 +61,6 @@ pub trait Externalities: Send + Sync + 'static { extensions: Vec<(Address, Bytes)>, ) -> Result<()>; - /// Build an optional opaque vote extension for the block this node is about - /// to precommit. - async fn extend_vote(&self, _mb_hash: H256, _block: Block) -> Result> { - Ok(None) - } - - /// Application-side validation for an opaque vote extension. - async fn verify_vote_extension( - &self, - _mb_hash: H256, - _block: Block, - _extension: Bytes, - ) -> Result { - Ok(false) - } - /// Build a fresh block payload whose parent has hash /// `parent_mb_hash`. Called only when this node has been elected /// proposer. The new block's height is derivable from `parent_mb_hash` diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 16e7e8f79eb..e2e3ff01b87 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -39,10 +39,12 @@ //! back via the same key the consensus layer hands in. use crate::{ - CommitCertificate, MalachiteEvent, Mempool, quarantine, + CommitCertificate, MalachiteEvent, Mempool, + externalities::utils::operation_to_transaction, + quarantine, tx_validity::{TxValidity, TxValidityChecker, eb_touched_programs}, }; -use anyhow::{Result, anyhow}; +use anyhow::{Context, Result, anyhow}; use async_trait::async_trait; use bytes::Bytes; use ethexe_common::{ @@ -50,7 +52,10 @@ use ethexe_common::{ db::{ CompactMb, GlobalsStorageRO, GlobalsStorageRW, InjectedStorageRO, MbStorageRO, MbStorageRW, }, - injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, ShieldedFields, Transaction}, + injected::{ + MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, ShieldedFields, SignedShieldedTransaction, + Transaction, + }, malachite::{Operation, Operations, VotingDecryptionShare, VotingExtension}, }; use ethexe_db::Database; @@ -58,10 +63,10 @@ use ethexe_malachite_core::{ Address as MalachiteAddress, Block, BlockPayload, Externalities, MAX_BLOCK_PAYLOAD_BYTES, }; use gprimitives::H256; -use gsigner::tdec::TdecKeyStore; +use gsigner::{PublicDecryptionContext, tdec::TdecKeyStore}; use parity_scale_codec::{DecodeAll, Encode}; use std::{ - collections::VecDeque, + collections::{BTreeSet, HashMap, VecDeque}, sync::{Arc, Mutex, RwLock}, }; use tokio::sync::{Notify, mpsc}; @@ -74,6 +79,7 @@ pub(crate) struct EthexeExternalities { /// Ethexe database. pub(crate) db: Database, /// + pub(crate) tdec_pub_ctx: Option, pub(crate) tdec_store: TdecKeyStore, /// Validator's local transaction pool. pub(crate) mempool: Arc, @@ -134,7 +140,7 @@ impl Externalities for EthexeExternalities { // dispatch in `ethexe_malachite_core::app`), so a too-old node stalls // on such a block rather than advancing past it or crashing. Only a // failure in `process_mb_finalized` is treated as fatal. - let payload = Operations::decode_all(&mut mb.payload.as_ref()) + let operations = Operations::decode_all(&mut mb.payload.as_ref()) .map_err(|e| anyhow!("decoding Operations from block payload bytes: {e}"))?; let parent = mb.parent_hash; @@ -147,7 +153,7 @@ impl Externalities for EthexeExternalities { } else { self.db.mb_meta(parent).last_advanced_eb }; - let last_advanced = payload + let last_advanced = operations .iter() .rev() .find_map(|tx| match tx { @@ -159,7 +165,7 @@ impl Externalities for EthexeExternalities { // CAS-store operations first so the contract — "if // CompactMb exists, operations are reachable" — holds // unconditionally. - let operations_hash = self.db.set_operations(payload.clone()); + let operations_hash = self.db.set_operations(operations.clone()); self.db.set_mb_compact_block( mb_hash, CompactMb { @@ -179,6 +185,31 @@ impl Externalities for EthexeExternalities { }, last_advanced, ); + + let Some(decryption_context) = self.tdec_pub_ctx.as_ref() else { + return Ok(()); + }; + + let shares = operations + .iter() + .filter_map(|op| op.as_shielded().map(|tx| tx.data())) + .filter_map(|tx| { + self.tdec_store + .create_share(decryption_context, &tx.ciphertext.header(), tx.aad.as_ref()) + .map(|share| VotingDecryptionShare { + tx_hash: tx.to_hash(), + share, + }) + .ok() + }) + .collect::>(); + + if !shares.is_empty() { + // let event = MalachiteEvent:: + let v = self + .event_tx + .send(Ok(MalachiteEvent::DecryptionShares { mb_hash, shares })); + } Ok(()) } @@ -245,62 +276,6 @@ impl Externalities for EthexeExternalities { Ok(()) } - async fn extend_vote(&self, _mb_hash: H256, mb: Block) -> Result> { - let payload = Operations::decode_all(&mut mb.payload.as_ref()) - .map_err(|e| anyhow!("decoding Operations for voting extension: {e}"))?; - - let decryption_shares = payload - .iter() - .filter_map(|op| op.as_shielded().map(|tx| tx.data())) - .filter_map(|tx| { - let _r = &self.tdec_store; - // self.tdec_store - // .create_share(public_context, &tx.ciphertext.header(), tx.aad.as_ref()) - // .map(|share| VotingDecryptionShare { - // tx_hash: tx.to_hash(), - // share, - // }) - // .ok() - None - }) - .collect::>(); - - Ok(decryption_shares.is_empty().then(|| { - let extension = VotingExtension { decryption_shares }; - Bytes::from(extension.encode()) - })) - } - - async fn verify_vote_extension( - &self, - _mb_hash: H256, - _mb: Block, - extension: Bytes, - ) -> Result { - let decryption_shares = match VotingExtension::decode_all(&mut extension.as_ref()) { - Ok(voting_extension) => voting_extension.decryption_shares, - Err(e) => { - warn!(error = %e, "verify_vote_extension: undecodable extension"); - return Ok(false); - } - }; - - for VotingDecryptionShare { tx_hash, share } in decryption_shares { - let Some(shielded_tx) = self.db.shielded_transaction(tx_hash) else { - trace!(%tx_hash, "validator provide decryption share for not existed transaction"); - return Ok(false); - }; - let ciphertext = &shielded_tx.data().ciphertext; - let _shares = [share]; - // let is_correct = gear_tdec::verify_decryption_shares_simple::< - // gear_tdec::bls12_381::E, - // ShieldedFields, - // >(pub_contexts, &ciphertext, &shares); - } - - Ok(true) - } - async fn build_block_above(&self, parent_mb_hash: H256) -> Result { // `parent_hash` is the consensus envelope hash of the parent // (zero for genesis). Use it directly to seed the producer's @@ -926,6 +901,7 @@ mod tests { let (event_tx, event_rx) = mpsc::unbounded_channel(); let ext = EthexeExternalities { db, + tdec_pub_ctx: None, tdec_store: TdecKeyStore::memory(), mempool: Arc::new(EmptyMempool), chain_head: Arc::new(RwLock::new(None)), @@ -1343,6 +1319,7 @@ mod tests { let (event_tx, mut event_rx) = mpsc::unbounded_channel(); let ext = EthexeExternalities { db: db.clone(), + tdec_pub_ctx: None, tdec_store: TdecKeyStore::memory(), mempool: Arc::clone(&tracker) as Arc, chain_head: Arc::new(RwLock::new(None)), @@ -1410,6 +1387,7 @@ mod tests { let (event_tx, event_rx) = mpsc::unbounded_channel(); let ext = EthexeExternalities { db, + tdec_pub_ctx: None, tdec_store: TdecKeyStore::memory(), mempool: mempool as Arc, chain_head: Arc::new(RwLock::new(None)), @@ -2162,6 +2140,7 @@ mod tests { let (event_tx, _event_rx) = mpsc::unbounded_channel(); let ext = EthexeExternalities { db: db.clone(), + tdec_pub_ctx: None, tdec_store: TdecKeyStore::memory(), mempool: Arc::new(EmptyMempool), chain_head: Arc::new(RwLock::new(Some(head))), diff --git a/ethexe/malachite/service/src/lib.rs b/ethexe/malachite/service/src/lib.rs index adac9bab075..55979dc6558 100644 --- a/ethexe/malachite/service/src/lib.rs +++ b/ethexe/malachite/service/src/lib.rs @@ -61,7 +61,7 @@ pub use crate::{ service::MalachiteService, tx_validity::{MIN_EXECUTABLE_BALANCE_FOR_INJECTED_MESSAGES, TxValidity, TxValidityChecker}, }; -use ethexe_common::injected::PurgedTransaction; +use ethexe_common::{injected::PurgedTransaction, malachite::VotingDecryptionShare}; pub use ethexe_common::{ injected::Transaction, malachite::{Operation, Operations}, @@ -97,6 +97,12 @@ pub enum MalachiteEvent { eb_hash: H256, transactions: Vec, }, + + /// Decryption shares for shielded transaction in a concrete malachite block. + DecryptionShares { + mb_hash: H256, + shares: Vec, + }, } impl std::fmt::Display for MalachiteEvent { @@ -126,6 +132,13 @@ impl std::fmt::Display for MalachiteEvent { transactions.len() ) } + Self::DecryptionShares { mb_hash, shares } => { + write!( + f, + "DecryptionShares(mb_hash: {mb_hash}, shares_len: {})", + shares.len() + ) + } } } } diff --git a/ethexe/malachite/service/src/service.rs b/ethexe/malachite/service/src/service.rs index 411d38d30f9..552050c6b18 100644 --- a/ethexe/malachite/service/src/service.rs +++ b/ethexe/malachite/service/src/service.rs @@ -97,6 +97,7 @@ impl MalachiteService { db: Database, signer: Signer, validator_pub_key: Option, + validator_tdec_ctx: Option, mempool: Arc, ) -> Result { tracing::info!( @@ -156,8 +157,11 @@ impl MalachiteService { let externalities = Arc::new(EthexeExternalities { db, + // TODO: FIXME (temporary solution) + tdec_pub_ctx: validator_tdec_ctx, tdec_store: gsigner::TdecKeyStore::memory(), + mempool: Arc::clone(&mempool), chain_head: Arc::clone(&chain_head), chain_head_notify: Arc::clone(&chain_head_notify), diff --git a/ethexe/malachite/service/tests/restart_resilience.rs b/ethexe/malachite/service/tests/restart_resilience.rs index 5436f7c1943..509057aeb12 100644 --- a/ethexe/malachite/service/tests/restart_resilience.rs +++ b/ethexe/malachite/service/tests/restart_resilience.rs @@ -188,6 +188,9 @@ async fn collect_until_finalized( Ok(Some(Ok(MalachiteEvent::PurgedTransactions { .. }))) => { // ignore } + Ok(Some(Ok(MalachiteEvent::DecryptionShares { .. }))) => { + // ignore + } Ok(Some(Err(e))) => panic!("service error: {e}"), Ok(None) | Err(_) => break, } @@ -217,6 +220,7 @@ async fn single_validator_finalizes_and_recovers_after_restart() { db.clone(), signer.clone(), Some(pub_key), + None, Arc::new(EmptyMempool), ) .await @@ -259,6 +263,7 @@ async fn single_validator_finalizes_and_recovers_after_restart() { db.clone(), signer, Some(pub_key), + None, Arc::new(EmptyMempool), ) .await diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index dff2ac0989f..a830a0ff812 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -534,6 +534,7 @@ impl Service { db.clone(), signer.clone(), validator_pub_key, + None, std::sync::Arc::new(InjectedTxMempool::new(db.clone())), ) .await @@ -1046,6 +1047,9 @@ impl Service { } }); } + MalachiteEvent::DecryptionShares { mb_hash, shares } => { + todo!("handle this malachite event variant") + } }, Event::Prometheus(event) => match event { PrometheusEvent::CollectMetrics { libp2p_metrics } => { diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index 267fb33d3de..87322373dd8 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -1202,6 +1202,7 @@ impl Node { self.db.clone(), self.signer.clone(), self.validator_config.as_ref().map(|c| c.public_key), + None, mempool, ) .await From 80b9d2bdff724f67856e0ac67148c2c1827e4066 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 22 Jun 2026 15:00:58 +0300 Subject: [PATCH 15/41] chore: backbone for gossip distributed decryption shares --- ethexe/common/src/db.rs | 6 +- ethexe/common/src/malachite.rs | 52 ++++++-- ethexe/malachite/core/src/app.rs | 13 -- ethexe/malachite/service/src/externalities.rs | 123 ++++++++++-------- ethexe/malachite/service/src/lib.rs | 4 +- ethexe/malachite/service/src/service.rs | 17 ++- ethexe/network/src/gossipsub.rs | 15 ++- ethexe/network/src/lib.rs | 16 ++- ethexe/network/src/validator/topic.rs | 58 +++++++++ ethexe/service/src/lib.rs | 27 +++- ethexe/service/src/tests/mod.rs | 3 +- ethexe/service/src/tests/utils/events.rs | 3 + protocol/gsigner/src/lib.rs | 17 +-- 13 files changed, 254 insertions(+), 100 deletions(-) diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index 817ef5d6df1..d4ab1b08e75 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -6,12 +6,12 @@ #[cfg(feature = "shielded")] use crate::injected::{ShieldedTransaction, SignedShieldedTransaction, SignedTxReceipt}; use crate::{ + Address, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, ProtocolTimelines, Schedule, + SimpleBlockData, ValidatorsVec, events::BlockEvent, gear::StateTransition, injected::{InjectedTransaction, Promise, SignedInjectedTransaction}, malachite::Operations, - Address, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, ProtocolTimelines, Schedule, - SimpleBlockData, ValidatorsVec, }; use alloc::{ collections::{BTreeSet, VecDeque}, @@ -275,7 +275,7 @@ mod tests { use super::*; // use crate::malachite::Operations; use indoc::formatdoc; - use scale_info::{meta_type, PortableRegistry, Registry}; + use scale_info::{PortableRegistry, Registry, meta_type}; use sha3::{Digest, Sha3_256}; #[test] diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index ea79ad44863..8025102ef41 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -29,13 +29,18 @@ use crate::injected::SignedInjectedTransaction; #[cfg(feature = "shielded")] -use crate::{HashOf, injected::ShieldedTransaction}; +use crate::{HashOf, ToDigest, injected::ShieldedTransaction}; use alloc::vec::Vec; use derive_more::{Deref, DerefMut, IntoIterator}; use gprimitives::H256; use parity_scale_codec::{Decode, Encode}; #[cfg(feature = "shielded")] -use {crate::injected::SignedShieldedTransaction, gear_tdec::bls12_381::DecryptionShareSimple}; +use { + crate::injected::SignedShieldedTransaction, + gear_tdec::bls12_381::DecryptionShareSimple, + gsigner::{PublicDecryptionContext, SignedMessage}, + sha3::Keccak256, +}; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; @@ -170,16 +175,18 @@ impl Operations { } } -/// Opaque application data attached to a Malachite precommit vote. -/// -/// The consensus layer transports this as bytes. Ethexe decodes the bytes into -/// this type at the application boundary, so the generic Malachite service does -/// not need to know about shielded transactions or threshold-decryption types. #[cfg(feature = "shielded")] -#[derive(Clone, Debug, Default, PartialEq, Eq, Encode, Decode)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct VotingExtension { - pub decryption_shares: Vec, +#[derive(Debug, Clone)] +pub struct MalachiteTdecContext { + /// Minimal number of decryption shares required to decrypt transaction. + pub threshold: u8, + /// Current validator's public decryption context. + /// Private data stored in [TdecKeyStore]. + /// + /// [TdecKeyStore]: gsigner::tdec::TdecKeyStore + pub my_context: PublicDecryptionContext, + /// Public contexts of the remaining validators involved in decryption. + pub others_contexts: Vec, } /// One validator's decryption-share payload for one shielded transaction. @@ -189,12 +196,33 @@ pub struct VotingExtension { #[cfg(feature = "shielded")] #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct VotingDecryptionShare { +pub struct ShieldedTxDecryptionShare { /// Transaction hash decryption share belongs to. pub tx_hash: HashOf, pub share: DecryptionShareSimple, } +#[cfg(feature = "shielded")] +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct BlockDecryptionData { + /// Malachite block hash the decryption shares belong to. + pub mb_hash: H256, + /// Decryption shares for [`ShieldedTransaction`]s in the Malachite block. + pub shares: Vec, +} + +#[cfg(feature = "shielded")] +impl ToDigest for BlockDecryptionData { + fn update_hasher(&self, hasher: &mut Keccak256) { + // TODO: + } +} + +/// Validator-signed decryption shares for one Malachite block. +#[cfg(feature = "shielded")] +pub type SignedBlockDecryptionShares = SignedMessage; + #[cfg(test)] mod tests { use super::*; diff --git a/ethexe/malachite/core/src/app.rs b/ethexe/malachite/core/src/app.rs index 1c55f6acc5e..8c7a133824b 100644 --- a/ethexe/malachite/core/src/app.rs +++ b/ethexe/malachite/core/src/app.rs @@ -413,19 +413,6 @@ where Ok(()) } - fn block_by_value_id(&self, value_id: ValueId) -> Result> { - let Some(proposal) = self - .state - .store - .get_undecided_proposal_by_value_id(&value_id)? - else { - return Ok(None); - }; - let block = Block::decode(&mut &proposal.value.block_bytes[..]) - .map_err(|e| anyhow!("decoding Block for vote extension: {e}"))?; - Ok(Some(block)) - } - // TODO: #5475 add per-peer token-bucket rate limit before `ingest_proposal_part` // (CPU/bandwidth bound; complements the memory bound from #5473). // TODO: #5480 gate `from` against a validator-peer-id allowlist so random diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index e2e3ff01b87..e1debccd4d8 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -39,38 +39,32 @@ //! back via the same key the consensus layer hands in. use crate::{ - CommitCertificate, MalachiteEvent, Mempool, - externalities::utils::operation_to_transaction, - quarantine, + CommitCertificate, MalachiteEvent, Mempool, quarantine, tx_validity::{TxValidity, TxValidityChecker, eb_touched_programs}, }; -use anyhow::{Context, Result, anyhow}; +use anyhow::{Result, anyhow, bail}; use async_trait::async_trait; use bytes::Bytes; use ethexe_common::{ MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, - db::{ - CompactMb, GlobalsStorageRO, GlobalsStorageRW, InjectedStorageRO, MbStorageRO, MbStorageRW, - }, - injected::{ - MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, ShieldedFields, SignedShieldedTransaction, - Transaction, - }, - malachite::{Operation, Operations, VotingDecryptionShare, VotingExtension}, + db::{CompactMb, GlobalsStorageRO, GlobalsStorageRW, MbStorageRO, MbStorageRW}, + injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, Transaction}, + malachite::{MalachiteTdecContext, Operation, Operations, ShieldedTxDecryptionShare}, }; use ethexe_db::Database; use ethexe_malachite_core::{ Address as MalachiteAddress, Block, BlockPayload, Externalities, MAX_BLOCK_PAYLOAD_BYTES, }; +use gear_tdec::bls12_381::SharedSecret; use gprimitives::H256; -use gsigner::{PublicDecryptionContext, tdec::TdecKeyStore}; +use gsigner::tdec::TdecKeyStore; use parity_scale_codec::{DecodeAll, Encode}; use std::{ - collections::{BTreeSet, HashMap, VecDeque}, + collections::{HashSet, VecDeque}, sync::{Arc, Mutex, RwLock}, }; use tokio::sync::{Notify, mpsc}; -use tracing::{debug, error, info, trace, warn}; +use tracing::{debug, error, info, warn}; /// Inputs the externalities need to satisfy the [`ethexe_malachite_core::Externalities`] /// contract. Constructed by [`crate::MalachiteService::new`] and @@ -78,11 +72,12 @@ use tracing::{debug, error, info, trace, warn}; pub(crate) struct EthexeExternalities { /// Ethexe database. pub(crate) db: Database, - /// - pub(crate) tdec_pub_ctx: Option, - pub(crate) tdec_store: TdecKeyStore, /// Validator's local transaction pool. pub(crate) mempool: Arc, + /// ... + pub(crate) tdec_ctx: Option, + /// ... + pub(crate) tdec_store: TdecKeyStore, /// Latest Ethereum chain head observed via the outer /// [`crate::MalachiteService::receive_new_chain_head`]. The /// producer reads this from inside [`Self::build_block_above`]; @@ -95,6 +90,8 @@ pub(crate) struct EthexeExternalities { /// fresh chain head arrives. Combines with the mempool's /// [`Mempool::wait_for_new_tx`] notify into a single select. pub(crate) chain_head_notify: Arc, + /// ... + pub(crate) decryption_share_notify: Arc, /// Outbound event channel — drained by /// [`crate::MalachiteService::poll_next`]. We wrap each emit in /// [`Self::try_emit_or_queue`] so that events whose @@ -186,9 +183,10 @@ impl Externalities for EthexeExternalities { last_advanced, ); - let Some(decryption_context) = self.tdec_pub_ctx.as_ref() else { + let Some(context) = self.tdec_ctx.as_ref() else { return Ok(()); }; + let decryption_context = &context.my_context; let shares = operations .iter() @@ -196,7 +194,7 @@ impl Externalities for EthexeExternalities { .filter_map(|tx| { self.tdec_store .create_share(decryption_context, &tx.ciphertext.header(), tx.aad.as_ref()) - .map(|share| VotingDecryptionShare { + .map(|share| ShieldedTxDecryptionShare { tx_hash: tx.to_hash(), share, }) @@ -205,8 +203,8 @@ impl Externalities for EthexeExternalities { .collect::>(); if !shares.is_empty() { - // let event = MalachiteEvent:: - let v = self + // Channel receiver is dropped only during shutdown. + let _ = self .event_tx .send(Ok(MalachiteEvent::DecryptionShares { mb_hash, shares })); } @@ -217,7 +215,7 @@ impl Externalities for EthexeExternalities { &self, mb_hash: H256, cert: ethexe_malachite_core::CommitCertificate, - extensions: Vec<(MalachiteAddress, Bytes)>, + _extensions: Vec<(MalachiteAddress, Bytes)>, ) -> Result<()> { let compact = self.db.mb_compact_block(mb_hash).ok_or_else(|| { anyhow!( @@ -254,13 +252,6 @@ impl Externalities for EthexeExternalities { mb_hash, signatures: cert.signatures, }; - let voting_extensions = utils::decode_voting_extensions(extensions)?; - if !voting_extensions.is_empty() { - info!( - validators = voting_extensions.len(), - "process_mb_finalized: received voting extensions", - ); - } // Same prerequisite as the matching BlockProposal — by the // time `process_mb_finalized` runs, `process_mb_proposal` has // already populated `mb_meta(block_hash).last_advanced_eb`. @@ -286,6 +277,7 @@ impl Externalities for EthexeExternalities { self.db.mb_meta(parent_mb_hash).last_advanced_eb }; + // let shares = self.wait_for_proposable_content(prev_advanced_eb_hash) let (advance, transactions) = self.wait_for_proposable_content(parent_advanced).await; info!( @@ -767,6 +759,46 @@ impl EthexeExternalities { } } + /// ... + async fn wait_for_shielded_tx_decryption_key( + &self, + parent_mb_hash: H256, + ) -> Result> { + let Some(ctx) = self.tdec_ctx.as_ref() else { + bail!("block produces has no decryption context") + }; + + let Some(compact) = self.db.mb_compact_block(parent_mb_hash) else { + bail!("compact block not found for block with hash={parent_mb_hash}") + }; + + let Some(operations) = self.db.operations(compact.operations_hash) else { + bail!( + "operations not found for block with hash={parent_mb_hash}, op_hash={}", + compact.operations_hash + ) + }; + + // Set of shielded transactions hashes that are waiting to be decrypt. + let shielded = operations + .iter() + .filter_map(|op| op.as_shielded().map(|tx| tx.data().to_hash())) + .collect::>(); + + // No shielded transactions in previous block, do not need to wait for decryption shares. + if shielded.is_empty() { + return Ok(None); + } + + loop { + // Waiting when new decryption shares will be received. + // self.decryption_share_notify.notified().await; + break; + } + + Ok(None) + } + // Candidate EB must be anchored in the quarantine and a strict descendant of the previously advanced EB. fn find_eb_candidate_for_advancing(&self, prev_advanced_eb_hash: H256) -> Option { let head = (*self.chain_head.read().expect("chain_head poisoned"))?; @@ -805,15 +837,11 @@ impl EthexeExternalities { } mod utils { - use anyhow::{Result, anyhow}; - use bytes::Bytes; use ethexe_common::{ injected::{Transaction, TransactionRef}, - malachite::{Operation, VotingExtension}, + malachite::Operation, }; - use ethexe_malachite_core::Address as MalachiteAddress; use gprimitives::H256; - use parity_scale_codec::DecodeAll; /// Optimization for reducing `clone` operation for potentially large transactions. pub(crate) fn operation_to_transaction(operation: &Operation) -> Option> { @@ -837,19 +865,6 @@ mod utils { Transaction::Shielded(_) => todo!("Shielded transaction block inclusion"), } } - - pub(crate) fn decode_voting_extensions( - extensions: Vec<(MalachiteAddress, Bytes)>, - ) -> Result> { - extensions - .into_iter() - .map(|(address, bytes)| { - VotingExtension::decode_all(&mut bytes.as_ref()) - .map(|extension| (address, extension)) - .map_err(|e| anyhow!("decoding voting extension from {address}: {e}")) - }) - .collect() - } } #[cfg(test)] @@ -901,11 +916,12 @@ mod tests { let (event_tx, event_rx) = mpsc::unbounded_channel(); let ext = EthexeExternalities { db, - tdec_pub_ctx: None, + tdec_ctx: None, tdec_store: TdecKeyStore::memory(), mempool: Arc::new(EmptyMempool), chain_head: Arc::new(RwLock::new(None)), chain_head_notify: Arc::new(Notify::new()), + decryption_share_notify: Arc::new(Notify::new()), event_tx, pending_events: Mutex::new(VecDeque::new()), gas_allowance: 1_000_000, @@ -1319,7 +1335,8 @@ mod tests { let (event_tx, mut event_rx) = mpsc::unbounded_channel(); let ext = EthexeExternalities { db: db.clone(), - tdec_pub_ctx: None, + tdec_ctx: None, + decryption_share_notify: Arc::new(Notify::new()), tdec_store: TdecKeyStore::memory(), mempool: Arc::clone(&tracker) as Arc, chain_head: Arc::new(RwLock::new(None)), @@ -1387,7 +1404,8 @@ mod tests { let (event_tx, event_rx) = mpsc::unbounded_channel(); let ext = EthexeExternalities { db, - tdec_pub_ctx: None, + tdec_ctx: None, + decryption_share_notify: Arc::new(Notify::new()), tdec_store: TdecKeyStore::memory(), mempool: mempool as Arc, chain_head: Arc::new(RwLock::new(None)), @@ -2140,7 +2158,8 @@ mod tests { let (event_tx, _event_rx) = mpsc::unbounded_channel(); let ext = EthexeExternalities { db: db.clone(), - tdec_pub_ctx: None, + tdec_ctx: None, + decryption_share_notify: Arc::new(Notify::new()), tdec_store: TdecKeyStore::memory(), mempool: Arc::new(EmptyMempool), chain_head: Arc::new(RwLock::new(Some(head))), diff --git a/ethexe/malachite/service/src/lib.rs b/ethexe/malachite/service/src/lib.rs index 55979dc6558..501b2eabed7 100644 --- a/ethexe/malachite/service/src/lib.rs +++ b/ethexe/malachite/service/src/lib.rs @@ -61,7 +61,7 @@ pub use crate::{ service::MalachiteService, tx_validity::{MIN_EXECUTABLE_BALANCE_FOR_INJECTED_MESSAGES, TxValidity, TxValidityChecker}, }; -use ethexe_common::{injected::PurgedTransaction, malachite::VotingDecryptionShare}; +use ethexe_common::{injected::PurgedTransaction, malachite::ShieldedTxDecryptionShare}; pub use ethexe_common::{ injected::Transaction, malachite::{Operation, Operations}, @@ -101,7 +101,7 @@ pub enum MalachiteEvent { /// Decryption shares for shielded transaction in a concrete malachite block. DecryptionShares { mb_hash: H256, - shares: Vec, + shares: Vec, }, } diff --git a/ethexe/malachite/service/src/service.rs b/ethexe/malachite/service/src/service.rs index 552050c6b18..8a1b9f98acf 100644 --- a/ethexe/malachite/service/src/service.rs +++ b/ethexe/malachite/service/src/service.rs @@ -28,6 +28,7 @@ use ethexe_common::{ Address, SimpleBlockData, db::{ConfigStorageRO, OnChainStorageRO}, injected::Transaction, + malachite::{MalachiteTdecContext, SignedBlockDecryptionShares}, }; use ethexe_db::Database; use futures::{Stream, stream::FusedStream}; @@ -44,6 +45,7 @@ pub struct MalachiteService { events_rx: mpsc::UnboundedReceiver>, chain_head: Arc>>, chain_head_notify: Arc, + decryption_share_notify: Arc, mempool: Arc, /// Shared with the inner engine — held here so /// [`Self::receive_new_chain_head`] can release pending events @@ -97,7 +99,7 @@ impl MalachiteService { db: Database, signer: Signer, validator_pub_key: Option, - validator_tdec_ctx: Option, + validator_tdec_ctx: Option, mempool: Arc, ) -> Result { tracing::info!( @@ -153,18 +155,20 @@ impl MalachiteService { let chain_head = Arc::new(RwLock::new(None)); let chain_head_notify = Arc::new(Notify::new()); + let decryption_share_notify = Arc::new(Notify::new()); let (events_tx, events_rx) = mpsc::unbounded_channel(); let externalities = Arc::new(EthexeExternalities { db, // TODO: FIXME (temporary solution) - tdec_pub_ctx: validator_tdec_ctx, + tdec_ctx: validator_tdec_ctx, tdec_store: gsigner::TdecKeyStore::memory(), mempool: Arc::clone(&mempool), chain_head: Arc::clone(&chain_head), chain_head_notify: Arc::clone(&chain_head_notify), + decryption_share_notify: Arc::clone(&decryption_share_notify), event_tx: events_tx, pending_events: std::sync::Mutex::new(std::collections::VecDeque::new()), gas_allowance: config.gas_allowance, @@ -188,6 +192,7 @@ impl MalachiteService { events_rx, chain_head, chain_head_notify, + decryption_share_notify, mempool, externalities, validator_pool, @@ -271,6 +276,14 @@ impl MalachiteService { self.externalities.drain_pending_events(); } + /// Handle signed decryption shares for [ShieldedTransaction]. + /// + /// [ShieldedTransaction]: ethexe_common::injected::ShieldedTransaction + pub fn receive_decryption_shares(&self, _signed_shares: SignedBlockDecryptionShares) { + self.decryption_share_notify.notify_one(); + todo!() + } + /// Push the on-chain validators for `head`'s era into the engine, /// if the era moved. Skips on missing DB data or unknown pub keys /// (wait-and-retry: the next `BlockSynced` re-evaluates). diff --git a/ethexe/network/src/gossipsub.rs b/ethexe/network/src/gossipsub.rs index 72b79e177d7..f6865ed66e7 100644 --- a/ethexe/network/src/gossipsub.rs +++ b/ethexe/network/src/gossipsub.rs @@ -8,7 +8,10 @@ use crate::{ peer_score, }; use anyhow::anyhow; -use ethexe_common::{Address, injected::SignedCompactTxReceipt, network::SignedValidatorMessage}; +use ethexe_common::{ + Address, injected::SignedCompactTxReceipt, malachite::SignedBlockDecryptionShares, + network::SignedValidatorMessage, +}; use libp2p::{ core::{Endpoint, transport::PortUse}, gossipsub, @@ -32,6 +35,7 @@ pub enum Message { // TODO: rename to `Validators` Commitments(SignedValidatorMessage), TxReceipt(SignedCompactTxReceipt), + DecryptionShares(SignedBlockDecryptionShares), } impl Message { @@ -39,6 +43,7 @@ impl Message { match self { Message::Commitments(_) => behaviour.commitments_topic.hash(), Message::TxReceipt(_) => behaviour.tx_receipts_topic.hash(), + Message::DecryptionShares(_) => behaviour.decryption_shares_topic.hash(), } } @@ -46,6 +51,7 @@ impl Message { match self { Message::Commitments(message) => message.encode(), Message::TxReceipt(message) => message.encode(), + Message::DecryptionShares(message) => message.encode(), } } } @@ -98,6 +104,7 @@ pub(crate) struct Behaviour { message_queue: VecDeque, commitments_topic: IdentTopic, tx_receipts_topic: IdentTopic, + decryption_shares_topic: IdentTopic, metrics: Arc, } @@ -111,6 +118,7 @@ impl Behaviour { ) -> anyhow::Result { let commitments_topic = Self::topic_with_router("commitments", router_address); let tx_receipts_topic = Self::topic_with_router("receipts", router_address); + let decryption_shares_topic = Self::topic_with_router("decryption_shares", router_address); let inner = ConfigBuilder::default() // dedup messages @@ -135,6 +143,7 @@ impl Behaviour { inner.subscribe(&commitments_topic)?; inner.subscribe(&tx_receipts_topic)?; + inner.subscribe(&decryption_shares_topic)?; Ok(Self { inner, @@ -142,6 +151,7 @@ impl Behaviour { message_queue: VecDeque::new(), commitments_topic, tx_receipts_topic, + decryption_shares_topic, metrics, }) } @@ -176,6 +186,9 @@ impl Behaviour { SignedValidatorMessage::decode(&mut &data[..]).map(Message::Commitments) } else if topic == self.tx_receipts_topic.hash() { SignedCompactTxReceipt::decode(&mut &data[..]).map(Message::TxReceipt) + } else if topic == self.decryption_shares_topic.hash() { + SignedBlockDecryptionShares::decode(&mut &data[..]) + .map(Message::DecryptionShares) } else { unreachable!("topic we never subscribed to: {topic:?}"); }; diff --git a/ethexe/network/src/lib.rs b/ethexe/network/src/lib.rs index 612e3e7d0b1..ad31033866d 100644 --- a/ethexe/network/src/lib.rs +++ b/ethexe/network/src/lib.rs @@ -8,7 +8,7 @@ //! //! - peer management and connection caps; //! - Kademlia-backed validator discovery; -//! - gossipsub topics for validator messages and public promises; +//! - gossipsub topics for validator messages, public promises, and decryption shares; //! - request/response database synchronization; //! - private injected-transaction delivery to validators; //! - peer scoring and temporary peer blocking. @@ -45,6 +45,7 @@ use ethexe_common::{ db::ConfigStorageRO, ecdsa::PublicKey, injected::{SignedCompactTxReceipt, Transaction}, + malachite::SignedBlockDecryptionShares, network::{SignedValidatorMessage, VerifiedValidatorMessage}, }; use ethexe_db::Database; @@ -92,6 +93,8 @@ pub enum NetworkEvent { ValidatorMessage(VerifiedValidatorMessage), /// A public promise observed on the promise gossipsub topic. TxReceiptMessage(SignedCompactTxReceipt), + /// Validator-signed decryption shares for a Malachite block. + DecryptionShares(SignedBlockDecryptionShares), /// Validator discovery learned or refreshed the network identity of the /// given validator address. ValidatorIdentityUpdated(Address), @@ -543,6 +546,12 @@ impl NetworkService { self.validator_topic.verify_receipt(source, receipt); (acceptance, receipt.map(NetworkEvent::TxReceiptMessage)) } + gossipsub::Message::DecryptionShares(message) => { + let (acceptance, message) = self + .validator_topic + .verify_decryption_message(source, message); + (acceptance, message.map(NetworkEvent::DecryptionShares)) + } }) } gossipsub::Event::PublishFailure { @@ -646,6 +655,11 @@ impl NetworkService { pub fn publish_tx_receipt(&mut self, receipt: SignedCompactTxReceipt) { self.swarm.behaviour_mut().gossipsub.publish(receipt) } + + /// Publish validator-signed decryption shares for a Malachite block. + pub fn publish_decryption_shares(&mut self, message: SignedBlockDecryptionShares) { + self.swarm.behaviour_mut().gossipsub.publish(message) + } } impl Drop for NetworkService { diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index 26b20f589ac..7d2b4fc3668 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -11,6 +11,7 @@ use crate::{ use ethexe_common::{ Address, HashOf, injected::{InjectedTransaction, SignedCompactTxReceipt}, + malachite::SignedBlockDecryptionShares, network::VerifiedValidatorMessage, }; use lru::LruCache; @@ -303,6 +304,26 @@ impl ValidatorTopic { } } + /// Admit a signed decryption-share message from a known validator. + /// + /// Block relevance and share correctness are intentionally left to the + /// future decryption-share handler. + pub fn verify_decryption_message( + &self, + source: PeerId, + message: SignedBlockDecryptionShares, + ) -> (MessageAcceptance, Option) { + if self.snapshot.contains(message.address()) { + (MessageAcceptance::Accept, Some(message)) + } else { + log::trace!( + "ignore decryption shares from unknown validator {} via {source}", + message.address() + ); + (MessageAcceptance::Ignore, None) + } + } + /// Retrieve the next verified message that is ready for further processing. pub(crate) fn next_message(&mut self) -> Option { self.verified_messages.pop_front() @@ -317,9 +338,11 @@ mod tests { consensus::BatchCommitmentValidationRequest, ecdsa::PublicKey, injected::{Promise, Receipt}, + malachite::BlockDecryptionData, mock::Mock, network::{SignedValidatorMessage, ValidatorMessage}, }; + use gprimitives::H256; use gsigner::secp256k1::{Secp256k1SignerExt, Signer}; use nonempty::{NonEmpty, nonempty}; @@ -377,6 +400,22 @@ mod tests { .into() } + fn signed_decryption_message( + signer: &Signer, + public_key: PublicKey, + ) -> SignedBlockDecryptionShares { + signer + .signed_message( + public_key, + BlockDecryptionData { + mb_hash: H256::random(), + shares: Vec::new(), + }, + None, + ) + .unwrap() + } + /// Buckets a message era can fall into relative to the snapshot era. #[derive(Debug, Clone, Copy)] enum EraRelation { @@ -771,4 +810,23 @@ mod tests { assert_matches!(acceptance, MessageAcceptance::Accept); assert_eq!(returned_receipt, Some(receipt)); } + + #[test] + fn verify_decryption_message_checks_validator_membership() { + let (pubkey, signer) = signer_with_pubkey(); + let message = signed_decryption_message(&signer, pubkey); + let peer_id = PeerId::random(); + + let unknown_topic = new_topic(nonempty![Address::default()]); + let (acceptance, returned) = + unknown_topic.verify_decryption_message(peer_id, message.clone()); + assert_matches!(acceptance, MessageAcceptance::Ignore); + assert_eq!(returned, None); + + let validator_topic = new_topic(nonempty![message.address()]); + let (acceptance, returned) = + validator_topic.verify_decryption_message(peer_id, message.clone()); + assert_matches!(acceptance, MessageAcceptance::Accept); + assert_eq!(returned, Some(message)); + } } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index a830a0ff812..0a0b42a06c8 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -48,6 +48,7 @@ use ethexe_common::{ db::{GlobalsStorageRW, MbStorageRO, OnChainStorageRO}, gear::CodeState, injected::{CompactPromise, InjectedTransactionAcceptance, Receipt}, + malachite::BlockDecryptionData, network::VerifiedValidatorMessage, }; use ethexe_compute::{ComputeEvent, ComputeService}; @@ -875,6 +876,12 @@ impl Service { rpc.receive_tx_receipt(receipt); } } + NetworkEvent::DecryptionShares(message) => { + // Just route shares to malachite service. + if let Some(malachite) = malachite.as_mut() { + malachite.receive_decryption_shares(message); + } + } NetworkEvent::ValidatorIdentityUpdated(_) | NetworkEvent::PeerBlocked(_) | NetworkEvent::PeerConnected(_) => {} @@ -1048,7 +1055,25 @@ impl Service { }); } MalachiteEvent::DecryptionShares { mb_hash, shares } => { - todo!("handle this malachite event variant") + let Some(pub_key) = validator_pub_key else { + // Validator key not found, can not sign shares. + continue; + }; + + let data = BlockDecryptionData { mb_hash, shares }; + match signer.signed_message(pub_key, data, None) { + Ok(message) => { + if let Some(network) = network.as_mut() { + network.publish_decryption_shares(message); + } + } + Err(err) => { + tracing::error!( + %mb_hash, + "failed to sign decryption shares: {err}" + ); + } + } } }, Event::Prometheus(event) => match event { diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index ab1b3b32d14..fa58c4bcedc 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -23,7 +23,8 @@ use ethexe_common::{ }, gear::BatchCommitment, injected::{ - InjectedTransaction, InjectedTransactionAcceptance, Receipt, TransactionHash, TransactionPurgedReason + InjectedTransaction, InjectedTransactionAcceptance, Receipt, TransactionHash, + TransactionPurgedReason, }, mock::*, }; diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index 8ef6add251c..4073f49c356 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -14,6 +14,7 @@ use ethexe_common::{ injected::{ InjectedTransaction, InjectedTransactionAcceptance, SignedCompactTxReceipt, Transaction, }, + malachite::SignedBlockDecryptionShares, network::VerifiedValidatorMessage, }; use ethexe_compute::ComputeEvent; @@ -77,6 +78,7 @@ impl TestingNetworkInjectedEvent { pub enum TestingNetworkEvent { ValidatorMessage(VerifiedValidatorMessage), TxReceiptMessage(SignedCompactTxReceipt), + DecryptionShares(SignedBlockDecryptionShares), ValidatorIdentityUpdated(Address), InjectedTransaction(TestingNetworkInjectedEvent), PeerBlocked(PeerId), @@ -88,6 +90,7 @@ impl TestingNetworkEvent { match event { NetworkEvent::ValidatorMessage(message) => Self::ValidatorMessage(message.clone()), NetworkEvent::TxReceiptMessage(message) => Self::TxReceiptMessage(message.clone()), + NetworkEvent::DecryptionShares(message) => Self::DecryptionShares(message.clone()), NetworkEvent::ValidatorIdentityUpdated(address) => { Self::ValidatorIdentityUpdated(*address) } diff --git a/protocol/gsigner/src/lib.rs b/protocol/gsigner/src/lib.rs index 141a54ad68d..c2e86652ff7 100644 --- a/protocol/gsigner/src/lib.rs +++ b/protocol/gsigner/src/lib.rs @@ -97,20 +97,13 @@ pub use storage::{FilesystemBackend, MemoryBackend, StorageBackend, StorageError feature = "serde", feature = "tdec" ))] -mod tdec_exports { - pub use crate::tdec::{ +pub use { + crate::tdec::{ BlindedKeyShare, PublicDecryptionContext, TdecDecryptionKey, TdecKeyEntry, TdecKeyStore, TdecKeypair, TdecPublicKey, - }; - pub use gear_tdec::bls12_381::{CiphertextHeader, DecryptionShareSimple as DecryptionShare}; -} -#[cfg(all( - feature = "std", - feature = "keyring", - feature = "serde", - feature = "tdec" -))] -pub use tdec_exports::*; + }, + gear_tdec::bls12_381::{CiphertextHeader, DecryptionShareSimple as DecryptionShare}, +}; #[cfg(feature = "secp256k1")] pub use schemes::secp256k1::{ From e753df9adf27842057f77f530bb749ef4023ce72 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 22 Jun 2026 15:40:40 +0300 Subject: [PATCH 16/41] chore: ai generated backbone for decryption shares store --- .../service/src/decryption_shares.rs | 223 +++++++++++++++++ ethexe/malachite/service/src/externalities.rs | 234 +++++++++++++++--- ethexe/malachite/service/src/lib.rs | 1 + ethexe/malachite/service/src/service.rs | 14 +- 4 files changed, 424 insertions(+), 48 deletions(-) create mode 100644 ethexe/malachite/service/src/decryption_shares.rs diff --git a/ethexe/malachite/service/src/decryption_shares.rs b/ethexe/malachite/service/src/decryption_shares.rs new file mode 100644 index 00000000000..fd44940d84d --- /dev/null +++ b/ethexe/malachite/service/src/decryption_shares.rs @@ -0,0 +1,223 @@ +// Copyright (C) Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! In-memory collection of threshold-decryption shares. + +use ethexe_common::{HashOf, injected::ShieldedTransaction}; +use gprimitives::H256; +use gsigner::DecryptionShare; +use std::{collections::HashMap, sync::Mutex}; +use tokio::sync::Notify; + +type ShieldedTxHash = HashOf; + +/// Result of inserting one verified decryption share. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum InsertOutcome { + Inserted, + Duplicate, + Equivocation, + UnknownBlock, + UnknownTransaction, +} + +/// Decryption shares grouped by MB, shielded transaction, and TDEC participant. +/// +/// The participant index addresses the corresponding entry in the local +/// [`ethexe_common::malachite::MalachiteTdecContext`]. Shares are verified +/// before reaching this store. +pub(crate) struct DecryptionSharesStore { + inner: Mutex>, + changed: Notify, +} + +type BlockShares = HashMap>; + +impl DecryptionSharesStore { + /// Constructs new empty decryption shares store. + pub(crate) fn new() -> Self { + Self { + inner: Mutex::new(HashMap::new()), + changed: Notify::new(), + } + } + + /// Register the shielded transactions belonging to an assembled MB. + pub(crate) fn register_block( + &self, + mb_hash: H256, + tx_hashes: impl IntoIterator, + ) { + let transactions = tx_hashes + .into_iter() + .map(|tx_hash| (tx_hash, HashMap::new())) + .collect(); + self.inner + .lock() + .expect("decryption shares poisoned") + .entry(mb_hash) + .or_insert(transactions); + } + + /// Insert a share whose transaction membership and cryptographic proof + /// have already been checked. + pub(crate) fn insert( + &self, + mb_hash: H256, + tx_hash: ShieldedTxHash, + participant: usize, + share: DecryptionShare, + ) -> InsertOutcome { + let mut blocks = self.inner.lock().expect("decryption shares poisoned"); + let Some(block) = blocks.get_mut(&mb_hash) else { + return InsertOutcome::UnknownBlock; + }; + let Some(shares) = block.get_mut(&tx_hash) else { + return InsertOutcome::UnknownTransaction; + }; + + let outcome = match shares.get(&participant) { + Some(existing) if existing == &share => InsertOutcome::Duplicate, + Some(_) => InsertOutcome::Equivocation, + None => { + shares.insert(participant, share); + InsertOutcome::Inserted + } + }; + drop(blocks); + + if outcome == InsertOutcome::Inserted { + self.changed.notify_one(); + } + outcome + } + + /// Return verified shares ordered by participant index. + pub(crate) fn shares( + &self, + mb_hash: H256, + tx_hash: ShieldedTxHash, + ) -> Vec<(usize, DecryptionShare)> { + let blocks = self.inner.lock().expect("decryption shares poisoned"); + let Some(shares) = blocks.get(&mb_hash).and_then(|block| block.get(&tx_hash)) else { + return Vec::new(); + }; + + let mut shares = shares + .iter() + .map(|(participant, share)| (*participant, share.clone())) + .collect::>(); + shares.sort_unstable_by_key(|(participant, _)| *participant); + shares + } + + /// Keep decryption shares only for the finalized MB. + /// Other shares are no longer useful. + pub(crate) fn retain_block(&self, mb_hash: H256) { + let mut this = self.inner.lock().expect("decryption shares poisoned"); + this.retain(|stored_hash, _| *stored_hash == mb_hash); + } + + pub(crate) fn notified(&self) -> impl Future + '_ { + self.changed.notified() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use gear_tdec::{bls12_381::E, rand_utils::Rng}; + + fn shares() -> (DecryptionShare, DecryptionShare) { + let mut rng = gear_tdec::rand_utils::test_rng(); + let dealer = gear_tdec::deal::(3, 2, &mut rng); + let plaintext = rng.r#gen::<[u8; 32]>(); + let ciphertext = + gear_tdec::encrypt_raw::(&plaintext, b"aad", &dealer.public_key, &mut rng) + .expect("encryption succeeds"); + let header = ciphertext.header(); + ( + dealer.private_contexts[0] + .create_share(&header, b"aad") + .expect("share creation succeeds"), + dealer.private_contexts[1] + .create_share(&header, b"aad") + .expect("share creation succeeds"), + ) + } + + fn random_tx_hash() -> ShieldedTxHash { + unsafe { HashOf::new(H256::random()) } + } + + #[tokio::test] + async fn insertion_is_idempotent_and_notifies() { + let store = DecryptionSharesStore::new(); + let mb_hash = H256::random(); + let tx_hash = random_tx_hash(); + let (share, _) = shares(); + store.register_block(mb_hash, [tx_hash]); + + assert_eq!( + store.insert(mb_hash, tx_hash, 0, share.clone()), + InsertOutcome::Inserted + ); + tokio::time::timeout(std::time::Duration::from_millis(10), store.notified()) + .await + .expect("insert notification is retained"); + assert_eq!( + store.insert(mb_hash, tx_hash, 0, share), + InsertOutcome::Duplicate + ); + assert_eq!(store.shares(mb_hash, tx_hash).len(), 1); + } + + #[test] + fn rejects_unknown_entries_and_equivocation() { + let store = DecryptionSharesStore::new(); + let mb_hash = H256::random(); + let tx_hash = random_tx_hash(); + let other_tx_hash = random_tx_hash(); + let (share, conflicting_share) = shares(); + + assert_eq!( + store.insert(mb_hash, tx_hash, 0, share.clone()), + InsertOutcome::UnknownBlock + ); + store.register_block(mb_hash, [tx_hash]); + assert_eq!( + store.insert(mb_hash, other_tx_hash, 0, share.clone()), + InsertOutcome::UnknownTransaction + ); + assert_eq!( + store.insert(mb_hash, tx_hash, 0, share), + InsertOutcome::Inserted + ); + assert_eq!( + store.insert(mb_hash, tx_hash, 0, conflicting_share), + InsertOutcome::Equivocation + ); + } + + #[test] + fn finalization_prunes_sibling_blocks() { + let store = DecryptionSharesStore::new(); + let finalized = H256::random(); + let sibling = H256::random(); + let tx_hash = random_tx_hash(); + let (share, _) = shares(); + store.register_block(finalized, [tx_hash]); + store.register_block(sibling, [tx_hash]); + assert_eq!( + store.insert(sibling, tx_hash, 0, share.clone()), + InsertOutcome::Inserted + ); + + store.retain_block(finalized); + + assert_eq!( + store.insert(sibling, tx_hash, 0, share), + InsertOutcome::UnknownBlock + ); + } +} diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index e1debccd4d8..a7812f5dd55 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -39,28 +39,35 @@ //! back via the same key the consensus layer hands in. use crate::{ - CommitCertificate, MalachiteEvent, Mempool, quarantine, + CommitCertificate, MalachiteEvent, Mempool, + decryption_shares::{DecryptionSharesStore, InsertOutcome}, + quarantine, tx_validity::{TxValidity, TxValidityChecker, eb_touched_programs}, }; use anyhow::{Result, anyhow, bail}; use async_trait::async_trait; use bytes::Bytes; use ethexe_common::{ - MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, + HashOf, MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, db::{CompactMb, GlobalsStorageRO, GlobalsStorageRW, MbStorageRO, MbStorageRW}, - injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, Transaction}, - malachite::{MalachiteTdecContext, Operation, Operations, ShieldedTxDecryptionShare}, + injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, ShieldedTransaction, Transaction}, + malachite::{ + MalachiteTdecContext, Operation, Operations, ShieldedTxDecryptionShare, + SignedBlockDecryptionShares, + }, }; use ethexe_db::Database; use ethexe_malachite_core::{ Address as MalachiteAddress, Block, BlockPayload, Externalities, MAX_BLOCK_PAYLOAD_BYTES, }; -use gear_tdec::bls12_381::SharedSecret; +use gear_tdec::bls12_381::{ + DecryptionShareSimple, SharedSecret, prepare_combine_simple, share_combine_simple, +}; use gprimitives::H256; use gsigner::tdec::TdecKeyStore; use parity_scale_codec::{DecodeAll, Encode}; use std::{ - collections::{HashSet, VecDeque}, + collections::{HashMap, VecDeque}, sync::{Arc, Mutex, RwLock}, }; use tokio::sync::{Notify, mpsc}; @@ -90,8 +97,8 @@ pub(crate) struct EthexeExternalities { /// fresh chain head arrives. Combines with the mempool's /// [`Mempool::wait_for_new_tx`] notify into a single select. pub(crate) chain_head_notify: Arc, - /// ... - pub(crate) decryption_share_notify: Arc, + /// Verified threshold-decryption shares received from validator gossip. + pub(crate) decryption_shares: Arc, /// Outbound event channel — drained by /// [`crate::MalachiteService::poll_next`]. We wrap each emit in /// [`Self::try_emit_or_queue`] so that events whose @@ -175,6 +182,13 @@ impl Externalities for EthexeExternalities { meta.last_advanced_eb = last_advanced; }); + let shielded_transactions = operations + .iter() + .filter_map(|op| op.as_shielded().map(|signed| signed.data())) + .collect::>(); + self.decryption_shares + .register_block(mb_hash, shielded_transactions.iter().map(|tx| tx.to_hash())); + self.try_emit_or_queue( MalachiteEvent::BlockProposal { height: mb.height, @@ -188,19 +202,25 @@ impl Externalities for EthexeExternalities { }; let decryption_context = &context.my_context; - let shares = operations - .iter() - .filter_map(|op| op.as_shielded().map(|tx| tx.data())) - .filter_map(|tx| { - self.tdec_store - .create_share(decryption_context, &tx.ciphertext.header(), tx.aad.as_ref()) - .map(|share| ShieldedTxDecryptionShare { - tx_hash: tx.to_hash(), - share, - }) - .ok() - }) - .collect::>(); + let mut shares = Vec::with_capacity(shielded_transactions.len()); + for tx in shielded_transactions { + let Ok(share) = self.tdec_store.create_share( + decryption_context, + &tx.ciphertext.header(), + tx.aad.as_ref(), + ) else { + continue; + }; + let tx_hash = tx.to_hash(); + let outcome = self + .decryption_shares + .insert(mb_hash, tx_hash, 0, share.clone()); + debug_assert!(matches!( + outcome, + InsertOutcome::Inserted | InsertOutcome::Duplicate + )); + shares.push(ShieldedTxDecryptionShare { tx_hash, share }); + } if !shares.is_empty() { // Channel receiver is dropped only during shutdown. @@ -264,6 +284,7 @@ impl Externalities for EthexeExternalities { }, last_advanced, ); + self.decryption_shares.retain_block(mb_hash); Ok(()) } @@ -277,7 +298,17 @@ impl Externalities for EthexeExternalities { self.db.mb_meta(parent_mb_hash).last_advanced_eb }; - // let shares = self.wait_for_proposable_content(prev_advanced_eb_hash) + let decryption_keys = self + .wait_for_shielded_tx_decryption_keys(parent_mb_hash) + .await?; + if let Some(keys) = decryption_keys.as_ref() { + debug!( + %parent_mb_hash, + transactions = keys.len(), + "build_block_above: reconstructed shielded transaction keys", + ); + } + let (advance, transactions) = self.wait_for_proposable_content(parent_advanced).await; info!( @@ -759,15 +790,15 @@ impl EthexeExternalities { } } - /// ... - async fn wait_for_shielded_tx_decryption_key( + /// Wait until every shielded transaction in the parent has enough verified + /// shares, then reconstruct one shared secret per transaction. + async fn wait_for_shielded_tx_decryption_keys( &self, parent_mb_hash: H256, - ) -> Result> { - let Some(ctx) = self.tdec_ctx.as_ref() else { - bail!("block produces has no decryption context") - }; - + ) -> Result, SharedSecret>>> { + if parent_mb_hash.is_zero() { + return Ok(None); + } let Some(compact) = self.db.mb_compact_block(parent_mb_hash) else { bail!("compact block not found for block with hash={parent_mb_hash}") }; @@ -779,24 +810,147 @@ impl EthexeExternalities { ) }; - // Set of shielded transactions hashes that are waiting to be decrypt. let shielded = operations .iter() - .filter_map(|op| op.as_shielded().map(|tx| tx.data().to_hash())) - .collect::>(); + .filter_map(|op| op.as_shielded().map(|tx| tx.data())) + .collect::>(); // No shielded transactions in previous block, do not need to wait for decryption shares. if shielded.is_empty() { return Ok(None); } + let Some(ctx) = self.tdec_ctx.as_ref() else { + bail!("block producer has no threshold-decryption context") + }; + let contexts = std::iter::once(&ctx.my_context) + .chain(ctx.others_contexts.iter()) + .collect::>(); + let threshold = usize::from(ctx.threshold); + if threshold == 0 || threshold > contexts.len() { + bail!( + "invalid threshold-decryption context: threshold={}, participants={}", + threshold, + contexts.len() + ); + } + loop { - // Waiting when new decryption shares will be received. - // self.decryption_share_notify.notified().await; - break; + let mut keys = HashMap::with_capacity(shielded.len()); + let mut complete = true; + + for tx in &shielded { + let tx_hash = tx.to_hash(); + let shares = self.decryption_shares.shares(parent_mb_hash, tx_hash); + if shares.len() < threshold { + complete = false; + break; + } + + let selected = shares.into_iter().take(threshold).collect::>(); + let domains = selected + .iter() + .map(|(participant, _)| contexts[*participant].domain) + .collect::>(); + let shares = selected + .into_iter() + .map(|(_, share)| share) + .collect::>(); + let coefficients = prepare_combine_simple::(&domains); + keys.insert( + tx_hash, + share_combine_simple::(&shares, &coefficients), + ); + } + + if complete { + return Ok(Some(keys)); + } + + self.decryption_shares.notified().await; } + } + + pub(crate) fn receive_decryption_shares(&self, signed: SignedBlockDecryptionShares) { + let Some(context) = self.tdec_ctx.as_ref() else { + debug!("ignoring decryption shares without local TDEC context"); + return; + }; + + let sender = signed.address(); + let data = signed.data(); + let Some(compact) = self.db.mb_compact_block(data.mb_hash) else { + debug!(%sender, mb_hash = %data.mb_hash, "ignoring shares for unknown MB"); + return; + }; + let Some(operations) = self.db.operations(compact.operations_hash) else { + warn!( + %sender, + mb_hash = %data.mb_hash, + operations_hash = %compact.operations_hash, + "ignoring decryption shares: MB operations are missing", + ); + return; + }; + + let transactions = operations + .iter() + .filter_map(|op| op.as_shielded().map(|signed| signed.data())) + .map(|tx| (tx.to_hash(), tx)) + .collect::>(); + let contexts = std::iter::once(&context.my_context) + .chain(context.others_contexts.iter()) + .collect::>(); - Ok(None) + for message_share in &data.shares { + let Some(transaction) = transactions.get(&message_share.tx_hash) else { + debug!( + %sender, + mb_hash = %data.mb_hash, + tx_hash = %message_share.tx_hash.inner(), + "ignoring decryption share for transaction outside MB", + ); + continue; + }; + let participant = contexts.iter().position(|context| { + message_share.share.verify( + &context.blinded_key_share.blinded_key_share, + &context.validator_public_key.encryption_key, + &transaction.ciphertext, + ) + }); + let Some(participant) = participant else { + debug!( + %sender, + mb_hash = %data.mb_hash, + tx_hash = %message_share.tx_hash.inner(), + "ignoring invalid decryption share", + ); + continue; + }; + + match self.decryption_shares.insert( + data.mb_hash, + message_share.tx_hash, + participant, + message_share.share.clone(), + ) { + InsertOutcome::Inserted | InsertOutcome::Duplicate => {} + InsertOutcome::Equivocation => warn!( + %sender, + mb_hash = %data.mb_hash, + tx_hash = %message_share.tx_hash.inner(), + participant, + "conflicting valid decryption share from the same participant", + ), + InsertOutcome::UnknownBlock | InsertOutcome::UnknownTransaction => debug!( + %sender, + mb_hash = %data.mb_hash, + tx_hash = %message_share.tx_hash.inner(), + "decryption-share storage rejected unknown MB or transaction", + ), + } + } } // Candidate EB must be anchored in the quarantine and a strict descendant of the previously advanced EB. @@ -921,7 +1075,7 @@ mod tests { mempool: Arc::new(EmptyMempool), chain_head: Arc::new(RwLock::new(None)), chain_head_notify: Arc::new(Notify::new()), - decryption_share_notify: Arc::new(Notify::new()), + decryption_shares: Arc::new(DecryptionSharesStore::new()), event_tx, pending_events: Mutex::new(VecDeque::new()), gas_allowance: 1_000_000, @@ -1336,7 +1490,7 @@ mod tests { let ext = EthexeExternalities { db: db.clone(), tdec_ctx: None, - decryption_share_notify: Arc::new(Notify::new()), + decryption_shares: Arc::new(DecryptionSharesStore::new()), tdec_store: TdecKeyStore::memory(), mempool: Arc::clone(&tracker) as Arc, chain_head: Arc::new(RwLock::new(None)), @@ -1405,7 +1559,7 @@ mod tests { let ext = EthexeExternalities { db, tdec_ctx: None, - decryption_share_notify: Arc::new(Notify::new()), + decryption_shares: Arc::new(DecryptionSharesStore::new()), tdec_store: TdecKeyStore::memory(), mempool: mempool as Arc, chain_head: Arc::new(RwLock::new(None)), @@ -2159,7 +2313,7 @@ mod tests { let ext = EthexeExternalities { db: db.clone(), tdec_ctx: None, - decryption_share_notify: Arc::new(Notify::new()), + decryption_shares: Arc::new(DecryptionSharesStore::new()), tdec_store: TdecKeyStore::memory(), mempool: Arc::new(EmptyMempool), chain_head: Arc::new(RwLock::new(Some(head))), diff --git a/ethexe/malachite/service/src/lib.rs b/ethexe/malachite/service/src/lib.rs index 501b2eabed7..ae9b6d5e4e7 100644 --- a/ethexe/malachite/service/src/lib.rs +++ b/ethexe/malachite/service/src/lib.rs @@ -49,6 +49,7 @@ //! RocksDB locks and sockets release. mod config; +mod decryption_shares; mod externalities; mod mempool; mod quarantine; diff --git a/ethexe/malachite/service/src/service.rs b/ethexe/malachite/service/src/service.rs index 8a1b9f98acf..2ec7c97d7e6 100644 --- a/ethexe/malachite/service/src/service.rs +++ b/ethexe/malachite/service/src/service.rs @@ -37,7 +37,8 @@ use gsigner::{Signer, schemes::secp256k1::Secp256k1}; use tokio::sync::{Notify, mpsc}; use crate::{ - MalachiteConfig, MalachiteEvent, Mempool, ValidatorEntry, externalities::EthexeExternalities, + MalachiteConfig, MalachiteEvent, Mempool, ValidatorEntry, + decryption_shares::DecryptionSharesStore, externalities::EthexeExternalities, }; /// Public consensus service. @@ -45,7 +46,6 @@ pub struct MalachiteService { events_rx: mpsc::UnboundedReceiver>, chain_head: Arc>>, chain_head_notify: Arc, - decryption_share_notify: Arc, mempool: Arc, /// Shared with the inner engine — held here so /// [`Self::receive_new_chain_head`] can release pending events @@ -155,7 +155,7 @@ impl MalachiteService { let chain_head = Arc::new(RwLock::new(None)); let chain_head_notify = Arc::new(Notify::new()); - let decryption_share_notify = Arc::new(Notify::new()); + let decryption_shares = Arc::new(DecryptionSharesStore::new()); let (events_tx, events_rx) = mpsc::unbounded_channel(); let externalities = Arc::new(EthexeExternalities { @@ -168,7 +168,7 @@ impl MalachiteService { mempool: Arc::clone(&mempool), chain_head: Arc::clone(&chain_head), chain_head_notify: Arc::clone(&chain_head_notify), - decryption_share_notify: Arc::clone(&decryption_share_notify), + decryption_shares, event_tx: events_tx, pending_events: std::sync::Mutex::new(std::collections::VecDeque::new()), gas_allowance: config.gas_allowance, @@ -192,7 +192,6 @@ impl MalachiteService { events_rx, chain_head, chain_head_notify, - decryption_share_notify, mempool, externalities, validator_pool, @@ -279,9 +278,8 @@ impl MalachiteService { /// Handle signed decryption shares for [ShieldedTransaction]. /// /// [ShieldedTransaction]: ethexe_common::injected::ShieldedTransaction - pub fn receive_decryption_shares(&self, _signed_shares: SignedBlockDecryptionShares) { - self.decryption_share_notify.notify_one(); - todo!() + pub fn receive_decryption_shares(&self, signed_shares: SignedBlockDecryptionShares) { + self.externalities.receive_decryption_shares(signed_shares); } /// Push the on-chain validators for `head`'s era into the engine, From a6dfb68ad306fe1ed690c40f3956b17c5b5caa05 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 22 Jun 2026 17:05:54 +0300 Subject: [PATCH 17/41] chore: store in malachite context HashMap: validator_address -> its PublicDecryptionContext --- ethexe/common/src/malachite.rs | 22 ++-- .../service/src/decryption_shares.rs | 97 +++++++++++----- ethexe/malachite/service/src/externalities.rs | 108 +++++++++--------- 3 files changed, 137 insertions(+), 90 deletions(-) diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index 8025102ef41..5c30a346fe6 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -27,7 +27,10 @@ //! `ethexe-malachite`) so `ethexe-processor` can accept them without //! depending on the consensus layer. -use crate::injected::SignedInjectedTransaction; +#[cfg(all(feature = "shielded", feature = "std"))] +use std::num::NonZeroUsize; + +use crate::{Address, injected::SignedInjectedTransaction}; #[cfg(feature = "shielded")] use crate::{HashOf, ToDigest, injected::ShieldedTransaction}; use alloc::vec::Vec; @@ -38,9 +41,11 @@ use parity_scale_codec::{Decode, Encode}; use { crate::injected::SignedShieldedTransaction, gear_tdec::bls12_381::DecryptionShareSimple, - gsigner::{PublicDecryptionContext, SignedMessage}, - sha3::Keccak256, + gsigner::SignedMessage, + sha3::{Digest as _, Keccak256}, }; +#[cfg(all(feature = "shielded", feature = "std"))] +use {gsigner::PublicDecryptionContext, std::collections::HashMap}; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; @@ -175,18 +180,19 @@ impl Operations { } } -#[cfg(feature = "shielded")] +#[cfg(all(feature = "shielded", feature = "std"))] #[derive(Debug, Clone)] pub struct MalachiteTdecContext { /// Minimal number of decryption shares required to decrypt transaction. - pub threshold: u8, + pub threshold: NonZeroUsize, /// Current validator's public decryption context. /// Private data stored in [TdecKeyStore]. /// /// [TdecKeyStore]: gsigner::tdec::TdecKeyStore pub my_context: PublicDecryptionContext, - /// Public contexts of the remaining validators involved in decryption. - pub others_contexts: Vec, + /// Public decryption context of every validator involved in decryption, + /// including the current validator. + pub contexts: HashMap, } /// One validator's decryption-share payload for one shielded transaction. @@ -215,7 +221,7 @@ pub struct BlockDecryptionData { #[cfg(feature = "shielded")] impl ToDigest for BlockDecryptionData { fn update_hasher(&self, hasher: &mut Keccak256) { - // TODO: + hasher.update(self.encode()); } } diff --git a/ethexe/malachite/service/src/decryption_shares.rs b/ethexe/malachite/service/src/decryption_shares.rs index fd44940d84d..45b4707e426 100644 --- a/ethexe/malachite/service/src/decryption_shares.rs +++ b/ethexe/malachite/service/src/decryption_shares.rs @@ -3,7 +3,7 @@ //! In-memory collection of threshold-decryption shares. -use ethexe_common::{HashOf, injected::ShieldedTransaction}; +use ethexe_common::{Address, HashOf, injected::ShieldedTransaction}; use gprimitives::H256; use gsigner::DecryptionShare; use std::{collections::HashMap, sync::Mutex}; @@ -21,17 +21,15 @@ pub(crate) enum InsertOutcome { UnknownTransaction, } -/// Decryption shares grouped by MB, shielded transaction, and TDEC participant. +/// Decryption shares grouped by MB, shielded transaction, and validator. /// -/// The participant index addresses the corresponding entry in the local -/// [`ethexe_common::malachite::MalachiteTdecContext`]. Shares are verified -/// before reaching this store. +/// Shares are verified before reaching this store. pub(crate) struct DecryptionSharesStore { inner: Mutex>, changed: Notify, } -type BlockShares = HashMap>; +type BlockShares = HashMap>; impl DecryptionSharesStore { /// Constructs new empty decryption shares store. @@ -65,7 +63,7 @@ impl DecryptionSharesStore { &self, mb_hash: H256, tx_hash: ShieldedTxHash, - participant: usize, + validator: Address, share: DecryptionShare, ) -> InsertOutcome { let mut blocks = self.inner.lock().expect("decryption shares poisoned"); @@ -76,11 +74,11 @@ impl DecryptionSharesStore { return InsertOutcome::UnknownTransaction; }; - let outcome = match shares.get(&participant) { + let outcome = match shares.get(&validator) { Some(existing) if existing == &share => InsertOutcome::Duplicate, Some(_) => InsertOutcome::Equivocation, None => { - shares.insert(participant, share); + shares.insert(validator, share); InsertOutcome::Inserted } }; @@ -92,23 +90,36 @@ impl DecryptionSharesStore { outcome } - /// Return verified shares ordered by participant index. - pub(crate) fn shares( + /// Return exactly `threshold` verified shares ordered by validator address. + /// + /// Returns `None` until enough distinct validators have provided a share. + pub(crate) fn threshold_shares( &self, mb_hash: H256, tx_hash: ShieldedTxHash, - ) -> Vec<(usize, DecryptionShare)> { + threshold: usize, + ) -> Option> { let blocks = self.inner.lock().expect("decryption shares poisoned"); - let Some(shares) = blocks.get(&mb_hash).and_then(|block| block.get(&tx_hash)) else { - return Vec::new(); - }; + let shares = blocks.get(&mb_hash)?.get(&tx_hash)?; + if shares.len() < threshold { + return None; + } - let mut shares = shares - .iter() - .map(|(participant, share)| (*participant, share.clone())) - .collect::>(); - shares.sort_unstable_by_key(|(participant, _)| *participant); - shares + let mut validators = shares.keys().copied().collect::>(); + validators.sort_unstable(); + Some( + validators + .into_iter() + .take(threshold) + .map(|validator| { + let share = shares + .get(&validator) + .expect("validator was collected from this map") + .clone(); + (validator, share) + }) + .collect(), + ) } /// Keep decryption shares only for the finalized MB. @@ -150,6 +161,10 @@ mod tests { unsafe { HashOf::new(H256::random()) } } + fn validator(byte: u8) -> Address { + [byte; 20].into() + } + #[tokio::test] async fn insertion_is_idempotent_and_notifies() { let store = DecryptionSharesStore::new(); @@ -159,17 +174,39 @@ mod tests { store.register_block(mb_hash, [tx_hash]); assert_eq!( - store.insert(mb_hash, tx_hash, 0, share.clone()), + store.insert(mb_hash, tx_hash, validator(1), share.clone()), InsertOutcome::Inserted ); tokio::time::timeout(std::time::Duration::from_millis(10), store.notified()) .await .expect("insert notification is retained"); assert_eq!( - store.insert(mb_hash, tx_hash, 0, share), + store.insert(mb_hash, tx_hash, validator(1), share), InsertOutcome::Duplicate ); - assert_eq!(store.shares(mb_hash, tx_hash).len(), 1); + assert_eq!( + store.threshold_shares(mb_hash, tx_hash, 1).unwrap().len(), + 1 + ); + } + + #[test] + fn threshold_query_is_deterministic_and_limited() { + let store = DecryptionSharesStore::new(); + let mb_hash = H256::random(); + let tx_hash = random_tx_hash(); + let (first_share, second_share) = shares(); + store.register_block(mb_hash, [tx_hash]); + store.insert(mb_hash, tx_hash, validator(2), second_share); + + assert!(store.threshold_shares(mb_hash, tx_hash, 2).is_none()); + + store.insert(mb_hash, tx_hash, validator(1), first_share); + let shares = store + .threshold_shares(mb_hash, tx_hash, 1) + .expect("threshold reached"); + assert_eq!(shares.len(), 1); + assert_eq!(shares[0].0, validator(1)); } #[test] @@ -181,20 +218,20 @@ mod tests { let (share, conflicting_share) = shares(); assert_eq!( - store.insert(mb_hash, tx_hash, 0, share.clone()), + store.insert(mb_hash, tx_hash, validator(1), share.clone()), InsertOutcome::UnknownBlock ); store.register_block(mb_hash, [tx_hash]); assert_eq!( - store.insert(mb_hash, other_tx_hash, 0, share.clone()), + store.insert(mb_hash, other_tx_hash, validator(1), share.clone()), InsertOutcome::UnknownTransaction ); assert_eq!( - store.insert(mb_hash, tx_hash, 0, share), + store.insert(mb_hash, tx_hash, validator(1), share), InsertOutcome::Inserted ); assert_eq!( - store.insert(mb_hash, tx_hash, 0, conflicting_share), + store.insert(mb_hash, tx_hash, validator(1), conflicting_share), InsertOutcome::Equivocation ); } @@ -209,14 +246,14 @@ mod tests { store.register_block(finalized, [tx_hash]); store.register_block(sibling, [tx_hash]); assert_eq!( - store.insert(sibling, tx_hash, 0, share.clone()), + store.insert(sibling, tx_hash, validator(1), share.clone()), InsertOutcome::Inserted ); store.retain_block(finalized); assert_eq!( - store.insert(sibling, tx_hash, 0, share), + store.insert(sibling, tx_hash, validator(1), share), InsertOutcome::UnknownBlock ); } diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index a7812f5dd55..1d19eff80e2 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -67,7 +67,7 @@ use gprimitives::H256; use gsigner::tdec::TdecKeyStore; use parity_scale_codec::{DecodeAll, Encode}; use std::{ - collections::{HashMap, VecDeque}, + collections::{HashMap, HashSet, VecDeque}, sync::{Arc, Mutex, RwLock}, }; use tokio::sync::{Notify, mpsc}; @@ -201,6 +201,13 @@ impl Externalities for EthexeExternalities { return Ok(()); }; let decryption_context = &context.my_context; + let Some(local_validator) = context.contexts.iter().find_map(|(address, participant)| { + (participant.validator_public_key == decryption_context.validator_public_key) + .then_some(*address) + }) else { + warn!("local TDEC context is absent from validator contexts"); + return Ok(()); + }; let mut shares = Vec::with_capacity(shielded_transactions.len()); for tx in shielded_transactions { @@ -212,9 +219,9 @@ impl Externalities for EthexeExternalities { continue; }; let tx_hash = tx.to_hash(); - let outcome = self - .decryption_shares - .insert(mb_hash, tx_hash, 0, share.clone()); + let outcome = + self.decryption_shares + .insert(mb_hash, tx_hash, local_validator, share.clone()); debug_assert!(matches!( outcome, InsertOutcome::Inserted | InsertOutcome::Duplicate @@ -796,6 +803,10 @@ impl EthexeExternalities { &self, parent_mb_hash: H256, ) -> Result, SharedSecret>>> { + let Some(ctx) = self.tdec_ctx.as_ref() else { + bail!("block producer has no threshold-decryption context") + }; + if parent_mb_hash.is_zero() { return Ok(None); } @@ -810,47 +821,42 @@ impl EthexeExternalities { ) }; - let shielded = operations + let mut pending = operations .iter() - .filter_map(|op| op.as_shielded().map(|tx| tx.data())) - .collect::>(); + .filter_map(|op| op.as_shielded().map(|tx| tx.data().to_hash())) + .collect::>(); // No shielded transactions in previous block, do not need to wait for decryption shares. - if shielded.is_empty() { + if pending.is_empty() { return Ok(None); } - let Some(ctx) = self.tdec_ctx.as_ref() else { - bail!("block producer has no threshold-decryption context") - }; - let contexts = std::iter::once(&ctx.my_context) - .chain(ctx.others_contexts.iter()) - .collect::>(); - let threshold = usize::from(ctx.threshold); - if threshold == 0 || threshold > contexts.len() { + let threshold = ctx.threshold.get(); + if threshold > ctx.contexts.len() { bail!( - "invalid threshold-decryption context: threshold={}, participants={}", - threshold, - contexts.len() + "invalid threshold-decryption context: threshold={threshold}, participants={}", + ctx.contexts.len() ); } - loop { - let mut keys = HashMap::with_capacity(shielded.len()); - let mut complete = true; - - for tx in &shielded { - let tx_hash = tx.to_hash(); - let shares = self.decryption_shares.shares(parent_mb_hash, tx_hash); - if shares.len() < threshold { - complete = false; - break; - } + let mut keys = HashMap::with_capacity(pending.len()); + while !pending.is_empty() { + pending.retain(|tx_hash| { + let Some(selected) = + self.decryption_shares + .threshold_shares(parent_mb_hash, *tx_hash, threshold) + else { + return true; + }; - let selected = shares.into_iter().take(threshold).collect::>(); let domains = selected .iter() - .map(|(participant, _)| contexts[*participant].domain) + .map(|(validator, _)| { + ctx.contexts + .get(validator) + .expect("stored share has a validator context") + .domain + }) .collect::>(); let shares = selected .into_iter() @@ -858,17 +864,18 @@ impl EthexeExternalities { .collect::>(); let coefficients = prepare_combine_simple::(&domains); keys.insert( - tx_hash, + *tx_hash, share_combine_simple::(&shares, &coefficients), ); - } + false + }); - if complete { - return Ok(Some(keys)); + if !pending.is_empty() { + self.decryption_shares.notified().await; } - - self.decryption_shares.notified().await; } + + Ok(Some(keys)) } pub(crate) fn receive_decryption_shares(&self, signed: SignedBlockDecryptionShares) { @@ -893,14 +900,15 @@ impl EthexeExternalities { return; }; + let Some(participant_context) = context.contexts.get(&sender) else { + debug!(%sender, "ignoring decryption shares from unknown TDEC participant"); + return; + }; let transactions = operations .iter() .filter_map(|op| op.as_shielded().map(|signed| signed.data())) .map(|tx| (tx.to_hash(), tx)) .collect::>(); - let contexts = std::iter::once(&context.my_context) - .chain(context.others_contexts.iter()) - .collect::>(); for message_share in &data.shares { let Some(transaction) = transactions.get(&message_share.tx_hash) else { @@ -912,14 +920,11 @@ impl EthexeExternalities { ); continue; }; - let participant = contexts.iter().position(|context| { - message_share.share.verify( - &context.blinded_key_share.blinded_key_share, - &context.validator_public_key.encryption_key, - &transaction.ciphertext, - ) - }); - let Some(participant) = participant else { + if !message_share.share.verify( + &participant_context.blinded_key_share.blinded_key_share, + &participant_context.validator_public_key.encryption_key, + &transaction.ciphertext, + ) { debug!( %sender, mb_hash = %data.mb_hash, @@ -927,12 +932,12 @@ impl EthexeExternalities { "ignoring invalid decryption share", ); continue; - }; + } match self.decryption_shares.insert( data.mb_hash, message_share.tx_hash, - participant, + sender, message_share.share.clone(), ) { InsertOutcome::Inserted | InsertOutcome::Duplicate => {} @@ -940,7 +945,6 @@ impl EthexeExternalities { %sender, mb_hash = %data.mb_hash, tx_hash = %message_share.tx_hash.inner(), - participant, "conflicting valid decryption share from the same participant", ), InsertOutcome::UnknownBlock | InsertOutcome::UnknownTransaction => debug!( From 5bc795b9ce586b5dc540a6d75adb996213a7efd6 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 22 Jun 2026 18:32:24 +0300 Subject: [PATCH 18/41] chore: add DecryptionKeys variant to Operation enum --- Cargo.lock | 21 ++++++------ ethexe/common/src/malachite.rs | 34 ++++++++++--------- ethexe/compute/src/compute.rs | 3 ++ ethexe/malachite/service/src/externalities.rs | 19 ++++------- 4 files changed, 39 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15600489bb6..78313891e15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5544,7 +5544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6231,7 +6231,7 @@ dependencies = [ [[package]] name = "ferveo-gear-common" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#a8cabb84093e9c2f26e604834146d35a06a90b18" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#4ec5f2ee169ce14af865fd1fb34d79b5e72881c5" dependencies = [ "ark-ec 0.5.0", "ark-serialize 0.5.0", @@ -6239,6 +6239,7 @@ dependencies = [ "bincode", "const-hex", "generic-array 0.14.7", + "parity-scale-codec", "rand 0.8.5", "serde", "thiserror 1.0.69", @@ -6247,7 +6248,7 @@ dependencies = [ [[package]] name = "ferveo-gear-tdec" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#a8cabb84093e9c2f26e604834146d35a06a90b18" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#4ec5f2ee169ce14af865fd1fb34d79b5e72881c5" dependencies = [ "ark-bls12-381 0.5.0", "ark-ec 0.5.0", @@ -9668,7 +9669,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -15628,7 +15629,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -15641,7 +15642,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -15744,7 +15745,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]] @@ -15765,7 +15766,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 1.0.5", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -18876,7 +18877,7 @@ dependencies = [ [[package]] name = "subproductdomain-gear" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#a8cabb84093e9c2f26e604834146d35a06a90b18" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#4ec5f2ee169ce14af865fd1fb34d79b5e72881c5" dependencies = [ "anyhow", "ark-ec 0.5.0", @@ -19352,7 +19353,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index 5c30a346fe6..178da1685c8 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -31,17 +31,18 @@ use std::num::NonZeroUsize; use crate::{Address, injected::SignedInjectedTransaction}; -#[cfg(feature = "shielded")] -use crate::{HashOf, ToDigest, injected::ShieldedTransaction}; use alloc::vec::Vec; use derive_more::{Deref, DerefMut, IntoIterator}; use gprimitives::H256; use parity_scale_codec::{Decode, Encode}; #[cfg(feature = "shielded")] use { - crate::injected::SignedShieldedTransaction, - gear_tdec::bls12_381::DecryptionShareSimple, - gsigner::SignedMessage, + crate::{ + HashOf, ToDigest, + injected::{ShieldedTransaction, SignedShieldedTransaction}, + }, + gear_tdec::bls12_381::SharedSecret, + gsigner::{DecryptionShare, SignedMessage}, sha3::{Digest as _, Keccak256}, }; #[cfg(all(feature = "shielded", feature = "std"))] @@ -78,7 +79,10 @@ pub enum Operation { /// User-submitted shielded transaction from mempool. #[cfg(feature = "shielded")] - Shielded(SignedShieldedTransaction) = 6, // encrypted transactions + Shielded(SignedShieldedTransaction) = 6, + + #[cfg(feature = "shielded")] + DecryptionKeys(Vec<(HashOf, SharedSecret)>) = 7, } impl Operation { @@ -103,14 +107,6 @@ impl Operation { _ => None, } } - - #[cfg(feature = "shielded")] - pub fn into_shielded(self) -> Option { - match self { - Self::Shielded(tx) => Some(tx), - _ => None, - } - } } // Custom encoder/decoder so the discriminant is always a fixed-width `u32` @@ -143,6 +139,10 @@ impl Decode for Operation { 6 => Ok(Operation::Shielded(SignedShieldedTransaction::decode( input, )?)), + #[cfg(feature = "shielded")] + 7 => Ok(Operation::DecryptionKeys( as Decode>::decode( + input, + )?)), _ => Err(parity_scale_codec::Error::from("invalid operation tag")), } } @@ -160,6 +160,8 @@ impl Encode for Operation { Operation::ProcessQueuesV3 { gas_allowance } => gas_allowance.encode_to(dest), #[cfg(feature = "shielded")] Operation::Shielded(shielded_tx) => shielded_tx.encode_to(dest), + #[cfg(feature = "shielded")] + Operation::DecryptionKeys(keys) => keys.encode_to(dest), } } } @@ -196,7 +198,7 @@ pub struct MalachiteTdecContext { } /// One validator's decryption-share payload for one shielded transaction. -/// Holds [`DecryptionShareSimple`] over [`ShieldedTransaction`]. +/// Holds [`DecryptionShare`] over [`ShieldedTransaction`]. /// /// [ShieldedTransaction]: crate::injected::ShieldedTransaction #[cfg(feature = "shielded")] @@ -205,7 +207,7 @@ pub struct MalachiteTdecContext { pub struct ShieldedTxDecryptionShare { /// Transaction hash decryption share belongs to. pub tx_hash: HashOf, - pub share: DecryptionShareSimple, + pub share: DecryptionShare, } #[cfg(feature = "shielded")] diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index a93ab7d0b33..68c8074ff35 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -285,6 +285,9 @@ fn build_executable_data( } current_anchor = block_hash; } + Operation::DecryptionKeys(keys) => { + todo!() + } Operation::Injected(signed) => { let verified = signed.into_verified(); injected_transactions.push(verified); diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 1d19eff80e2..47fbd2f1a95 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -308,14 +308,6 @@ impl Externalities for EthexeExternalities { let decryption_keys = self .wait_for_shielded_tx_decryption_keys(parent_mb_hash) .await?; - if let Some(keys) = decryption_keys.as_ref() { - debug!( - %parent_mb_hash, - transactions = keys.len(), - "build_block_above: reconstructed shielded transaction keys", - ); - } - let (advance, transactions) = self.wait_for_proposable_content(parent_advanced).await; info!( @@ -434,6 +426,9 @@ impl Externalities for EthexeExternalities { if let Some(block_hash) = advance { operations.push(Operation::AdvanceTillEthereumBlock { block_hash }); } + if let Some(keys) = decryption_keys { + operations.push(Operation::DecryptionKeys(keys.into_iter().collect())); + } for tx in capped { operations.push(utils::transaction_to_operation(tx)); } @@ -803,10 +798,6 @@ impl EthexeExternalities { &self, parent_mb_hash: H256, ) -> Result, SharedSecret>>> { - let Some(ctx) = self.tdec_ctx.as_ref() else { - bail!("block producer has no threshold-decryption context") - }; - if parent_mb_hash.is_zero() { return Ok(None); } @@ -831,6 +822,10 @@ impl EthexeExternalities { return Ok(None); } + let Some(ctx) = self.tdec_ctx.as_ref() else { + bail!("block producer has no threshold-decryption context") + }; + let threshold = ctx.threshold.get(); if threshold > ctx.contexts.len() { bail!( From 6c1859bc641cc29fe6715231a1e9a1dd0999b7dd Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 22 Jun 2026 21:03:26 +0300 Subject: [PATCH 19/41] chore: initial handling shielded transactions in compute service --- ethexe/common/src/injected.rs | 1 + ethexe/common/src/malachite.rs | 9 ++-- ethexe/compute/src/compute.rs | 52 +++++++++++++++++-- ethexe/malachite/service/src/externalities.rs | 8 +-- .../src/schemes/secp256k1/signature.rs | 6 +++ 5 files changed, 64 insertions(+), 12 deletions(-) diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index 8968736ae57..7de2a20f0c7 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -398,6 +398,7 @@ pub enum TransactionPurgedReason { /// The transaction references a block that is not known locally. #[display("transaction reference block is unknown")] UnknownReferenceBlock = 2, + /// /// The transaction has a non-zero value, which is not supported yet. /// diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index 178da1685c8..7c59438da25 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -44,6 +44,7 @@ use { gear_tdec::bls12_381::SharedSecret, gsigner::{DecryptionShare, SignedMessage}, sha3::{Digest as _, Keccak256}, + std::collections::BTreeMap, }; #[cfg(all(feature = "shielded", feature = "std"))] use {gsigner::PublicDecryptionContext, std::collections::HashMap}; @@ -82,7 +83,7 @@ pub enum Operation { Shielded(SignedShieldedTransaction) = 6, #[cfg(feature = "shielded")] - DecryptionKeys(Vec<(HashOf, SharedSecret)>) = 7, + DecryptionKeys(BTreeMap, SharedSecret>) = 7, } impl Operation { @@ -140,9 +141,9 @@ impl Decode for Operation { input, )?)), #[cfg(feature = "shielded")] - 7 => Ok(Operation::DecryptionKeys( as Decode>::decode( - input, - )?)), + 7 => Ok(Operation::DecryptionKeys( + as Decode>::decode(input)?, + )), _ => Err(parity_scale_codec::Error::from("invalid operation tag")), } } diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 68c8074ff35..1d9660e12bf 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -14,7 +14,7 @@ use ethexe_common::{ PromiseEmissionMode, PromisePolicy, db::{CodesStorageRW, CompactMb, ConfigStorageRO, MbStorageRO, MbStorageRW, OnChainStorageRO}, events::BlockRequestEvent, - injected::Promise, + injected::{Promise, SignedShieldedTransaction}, malachite::{Operation, Operations}, }; use ethexe_db::Database; @@ -224,7 +224,7 @@ pub fn prepare_executable_for_mb( .. } = compact_mb; - let mb_payload = db + let operations = db .operations(operations_hash) .ok_or(ComputeError::MbPayloadNotFound { mb_hash, @@ -245,7 +245,8 @@ pub fn prepare_executable_for_mb( build_executable_data( db, - mb_payload, + parent, + operations, program_states, schedule, initial_advanced_block, @@ -259,6 +260,7 @@ pub fn prepare_executable_for_mb( /// genesis block from [`ConfigStorageRO::config`]. fn build_executable_data( db: &Database, + parent_mb: H256, operations: Operations, program_states: ethexe_common::ProgramStates, schedule: ethexe_common::Schedule, @@ -286,7 +288,21 @@ fn build_executable_data( current_anchor = block_hash; } Operation::DecryptionKeys(keys) => { - todo!() + let transactions = collect_parent_mb_shielded_txs(db, parent_mb)?; + + for tx in transactions { + match keys.get(&tx.data().to_hash()) { + Some(shared_secret) => { + match tx.into_verified().try_map(|tx| tx.unshield(shared_secret)) { + Ok(injected_tx) => injected_transactions.push(injected_tx), + Err(_err) => { + todo!("emit event about invalid transaction decryption") + } + } + } + None => {} + } + } } Operation::Injected(signed) => { let verified = signed.into_verified(); @@ -348,6 +364,34 @@ fn build_executable_data( }) } +fn collect_parent_mb_shielded_txs( + db: &Database, + parent_mb: H256, +) -> Result> { + if parent_mb.is_zero() { + return Ok(Default::default()); + } + + let parent_compact = db + .mb_compact_block(parent_mb) + .ok_or_else(|| ComputeError::MbCompactNotFound(parent_mb))?; + + let operations = db + .operations(parent_compact.operations_hash) + .ok_or_else(|| ComputeError::MbPayloadNotFound { + mb_hash: parent_mb, + payload_hash: parent_compact.operations_hash, + })?; + + Ok(operations + .into_iter() + .filter_map(|op| match op { + Operation::Shielded(tx) => Some(tx), + _ => None, + }) + .collect()) +} + /// EBs in `(last_advanced, target]`, oldest-first; capped at 1024. fn collect_advance_chain(db: &Database, target: H256, last_advanced: H256) -> Result> { const MAX_ADVANCE_STEPS: usize = 1024; diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 47fbd2f1a95..91ecdf0cd52 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -67,7 +67,7 @@ use gprimitives::H256; use gsigner::tdec::TdecKeyStore; use parity_scale_codec::{DecodeAll, Encode}; use std::{ - collections::{HashMap, HashSet, VecDeque}, + collections::{BTreeMap, HashMap, HashSet, VecDeque}, sync::{Arc, Mutex, RwLock}, }; use tokio::sync::{Notify, mpsc}; @@ -427,7 +427,7 @@ impl Externalities for EthexeExternalities { operations.push(Operation::AdvanceTillEthereumBlock { block_hash }); } if let Some(keys) = decryption_keys { - operations.push(Operation::DecryptionKeys(keys.into_iter().collect())); + operations.push(Operation::DecryptionKeys(keys)); } for tx in capped { operations.push(utils::transaction_to_operation(tx)); @@ -797,7 +797,7 @@ impl EthexeExternalities { async fn wait_for_shielded_tx_decryption_keys( &self, parent_mb_hash: H256, - ) -> Result, SharedSecret>>> { + ) -> Result, SharedSecret>>> { if parent_mb_hash.is_zero() { return Ok(None); } @@ -834,7 +834,7 @@ impl EthexeExternalities { ); } - let mut keys = HashMap::with_capacity(pending.len()); + let mut keys = BTreeMap::new(); while !pending.is_empty() { pending.retain(|tx_hash| { let Some(selected) = diff --git a/protocol/gsigner/src/schemes/secp256k1/signature.rs b/protocol/gsigner/src/schemes/secp256k1/signature.rs index 14d717e0752..3116cdfdb5a 100644 --- a/protocol/gsigner/src/schemes/secp256k1/signature.rs +++ b/protocol/gsigner/src/schemes/secp256k1/signature.rs @@ -389,6 +389,12 @@ impl VerifiedData { VerifiedData { data, public_key } } + pub fn try_map(self, f: impl FnOnce(T) -> Result) -> Result, E> { + let Self { data, public_key } = self; + let data = f(data)?; + Ok(VerifiedData { data, public_key }) + } + pub fn data(&self) -> &T { &self.data } From 13414729623cfb19457638f831b4524e9523a5e0 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 23 Jun 2026 12:12:29 +0300 Subject: [PATCH 20/41] test(service): shielded_tx_fungible_token --- Cargo.lock | 2 + ethexe/common/Cargo.toml | 2 +- ethexe/common/src/injected.rs | 3 +- ethexe/rpc/Cargo.toml | 1 + ethexe/rpc/src/apis/injected/server.rs | 6 ++ ethexe/rpc/src/apis/injected/trait.rs | 4 + ethexe/service/Cargo.toml | 1 + ethexe/service/src/tests/mod.rs | 110 ++++++++++++++++++++++++- 8 files changed, 126 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 78313891e15..68bcaefb426 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5976,6 +5976,7 @@ dependencies = [ "ethexe-db", "ethexe-processor", "ethexe-runtime-common", + "ferveo-gear-tdec", "futures", "gear-core", "gear-workspace-hack", @@ -6095,6 +6096,7 @@ dependencies = [ "log", "ntest", "parity-scale-codec", + "rand 0.8.5", "tempfile", "tokio", "tracing", diff --git a/ethexe/common/Cargo.toml b/ethexe/common/Cargo.toml index 2ff6dfa0ede..b9d30046e42 100644 --- a/ethexe/common/Cargo.toml +++ b/ethexe/common/Cargo.toml @@ -64,5 +64,5 @@ std = [ "gsigner/keyring", "shielded" ] -shielded = ["dep:gear-tdec", "dep:ark-serialize"] +shielded = ["gsigner/tdec", "dep:gear-tdec", "dep:ark-serialize"] mock = ["std", "itertools/use_std", "tap", "proptest"] diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index 7de2a20f0c7..5f8022a43b2 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -41,6 +41,7 @@ pub const MAX_INJECTED_TX_SALT_SIZE: usize = 32; /// always admissible. pub const MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB: usize = 127 * 1024; +// TODO: rename this type to just `TransactionAcceptance` #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] pub enum InjectedTransactionAcceptance { @@ -298,7 +299,7 @@ impl ToDigest for Receipt

{ #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::From, derive_more::Deref)] #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", serde(transparent))] -pub struct SignedTxReceipt(SignedMessage>); +pub struct SignedTxReceipt(pub SignedMessage>); /// Signed [Receipt] with a [CompactPromise] generic. /// It is used as a lightweight transfer type diff --git a/ethexe/rpc/Cargo.toml b/ethexe/rpc/Cargo.toml index 1d645724df2..ef6e1d2b32d 100644 --- a/ethexe/rpc/Cargo.toml +++ b/ethexe/rpc/Cargo.toml @@ -35,6 +35,7 @@ gear-workspace-hack.workspace = true thiserror.workspace = true scopeguard.workspace = true moka = {workspace = true, features = ["sync"]} +gear-tdec = {workspace = true, features = ["bls12_381"]} [dev-dependencies] jsonrpsee = { workspace = true, features = ["client"] } diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index 7754fe7d693..b237a1494fd 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -16,6 +16,7 @@ use ethexe_common::{ }, }; use ethexe_db::Database; +use gear_tdec::bls12_381::DkgPublicKey; use jsonrpsee::{ core::{RpcResult, SubscriptionResult, async_trait}, server::PendingSubscriptionSink, @@ -37,6 +38,11 @@ pub struct InjectedApi { // TODO: Issue #5387 #[async_trait] impl InjectedServer for InjectedApi { + async fn shielding_key(&self) -> RpcResult> { + // TODO: Implement me + Ok(None) + } + async fn send_transaction( &self, transaction: Transaction, diff --git a/ethexe/rpc/src/apis/injected/trait.rs b/ethexe/rpc/src/apis/injected/trait.rs index 0186340b7eb..1c28ff139d8 100644 --- a/ethexe/rpc/src/apis/injected/trait.rs +++ b/ethexe/rpc/src/apis/injected/trait.rs @@ -8,6 +8,7 @@ use ethexe_common::{ SignedTxReceipt, Transaction, }, }; +use gear_tdec::bls12_381::DkgPublicKey; use jsonrpsee::proc_macros::rpc; #[cfg_attr( @@ -23,6 +24,9 @@ use jsonrpsee::proc_macros::rpc; rpc(client, namespace = "injected") )] pub trait Injected { + #[method(name = "getShieldingKey")] + async fn shielding_key(&self) -> jsonrpsee::core::RpcResult>; + /// Just sends an injected transaction. #[method(name = "sendTransaction")] async fn send_transaction( diff --git a/ethexe/service/Cargo.toml b/ethexe/service/Cargo.toml index 00eb0b3c584..4a336db5492 100644 --- a/ethexe/service/Cargo.toml +++ b/ethexe/service/Cargo.toml @@ -72,6 +72,7 @@ jsonrpsee = { workspace = true, features = ["client"] } async-broadcast.workspace = true wat.workspace = true tempfile.workspace = true +rand.workspace = true demo-ping = { workspace = true, features = ["debug", "ethexe"] } demo-value-sender-ethexe = { workspace = true, features = ["debug", "ethexe"] } diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 5a362a2a69d..714ee8f1a82 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -2862,7 +2862,7 @@ async fn program_subscribe_best_state() { .unwrap(); let mut tx_subscription = rpc_client - .send_transaction_and_watch(rpc_tx) + .send_transaction_and_watch(rpc_tx.into()) .await .expect("successfully subscribe for injected transaction promise"); @@ -3056,6 +3056,114 @@ async fn injected_tx_fungible_token_over_network() { stop_nodes([alice_node, bob_node]).await; } +#[tokio::test] +#[ntest::timeout(60_000)] +async fn shielded_tx_fungible_token() { + init_logger(); + + let env_config = TestEnvConfig { + network: EnvNetworkConfig::Enabled, + ..Default::default() + }; + let mut env = TestEnv::new(env_config).await.unwrap(); + + let pubkey = env.validators[0].public_key; + let mut node = env + .new_node( + NodeConfig::default() + .service_rpc(8090) + .validator(env.validators[0]), + ) + .await; + node.start_service().await; + 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 { + name: "USD Tether".to_string(), + symbol: "USDT".to_string(), + decimals: 10, + initial_capacity: None, + }; + + // 2. Uploading code and creating program + let res = env + .upload_code(demo_fungible_token::WASM_BINARY) + .await + .unwrap() + .wait_for() + .await + .unwrap(); + + let code_id = res.code_id; + let res = env + .create_program(code_id, 500_000_000_000_000) + .await + .unwrap() + .wait_for() + .await + .unwrap(); + + let usdt_actor_id = res.program_id; + + // 3. Initialize program + let init_reply = env + .send_message(usdt_actor_id, &token_config.encode()) + .await + .unwrap() + .wait_for() + .await + .unwrap(); + + assert_eq!(init_reply.program_id, usdt_actor_id); + assert_eq!(init_reply.value, 0); + assert_eq!( + init_reply.code, + ReplyCode::Success(SuccessReplyReason::Auto) + ); + assert!( + init_reply.payload.is_empty(), + "Expect empty payload, because of initializing Fungible Token returns nothing" + ); + + tracing::info!("✅ Fungible token successfully initialized"); + + let shielding_key = rpc_client.shielding_key().await.unwrap().unwrap(); + + let amount: u128 = 5_000_000_000; + let mint_action = demo_fungible_token::FTAction::Mint(amount); + + let mint_tx = InjectedTransaction { + destination: usdt_actor_id, + payload: mint_action.encode().try_into().unwrap(), + value: 0, + reference_block: node.db.globals().latest_prepared_eb_hash, + salt: vec![1].try_into().unwrap(), + }; + let shielded = mint_tx + .shield(&shielding_key, &mut rand::thread_rng()) + .unwrap(); + let signed_shielded_tx = env.signer.signed_message(pubkey, shielded, None).unwrap(); + let mut subscription = rpc_client + .send_transaction_and_watch(signed_shielded_tx.into()) + .await + .unwrap(); + + let receipt = subscription.next().await.unwrap().unwrap(); + let promise = receipt.0.into_data().unwrap_promise(); + + let expected_event = demo_fungible_token::FTEvent::Transfer { + from: ActorId::new([0u8; 32]), + to: pubkey.to_address().into(), + amount, + }; + + assert_eq!(promise.reply.payload, expected_event.encode()); +} + #[tokio::test] #[ntest::timeout(120_000)] async fn whole_network_restore() { From 3989e548c7d7e67320a6f437d81754cfc0d13246 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 23 Jun 2026 15:30:05 +0300 Subject: [PATCH 21/41] chore: unshielded output from finalizing block --- ethexe/common/src/db.rs | 3 + ethexe/common/src/injected.rs | 2 + ethexe/common/src/malachite.rs | 8 +++ ethexe/compute/src/compute.rs | 55 +++---------------- ethexe/db/src/database.rs | 25 ++++++++- ethexe/malachite/service/src/externalities.rs | 54 +++++++++++++++++- ethexe/malachite/service/src/lib.rs | 27 ++++++++- .../service/tests/restart_resilience.rs | 3 + ethexe/service/src/lib.rs | 7 +++ .../gsigner/src/schemes/secp256k1/keys.rs | 3 +- .../src/schemes/secp256k1/signature.rs | 2 +- 11 files changed, 133 insertions(+), 56 deletions(-) diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index d4ab1b08e75..7474af1f846 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -22,6 +22,7 @@ use gear_core::{ ids::{ActorId, CodeId}, }; use gprimitives::H256; +use gsigner::VerifiedData; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; @@ -178,6 +179,7 @@ pub trait MbStorageRO { fn mb_outcome(&self, mb_hash: H256) -> Option>; fn mb_schedule(&self, mb_hash: H256) -> Option; fn mb_meta(&self, mb_hash: H256) -> MbMeta; + fn mb_unshielded_txs(&self, mb_hash: H256) -> Vec>; } #[auto_impl::auto_impl(&)] @@ -189,6 +191,7 @@ pub trait MbStorageRW: MbStorageRO { fn set_mb_program_states(&self, mb_hash: H256, program_states: ProgramStates); fn set_mb_outcome(&self, mb_hash: H256, outcome: Vec); fn set_mb_schedule(&self, mb_hash: H256, schedule: Schedule); + fn set_mb_unshielded_txs(&self, mb_hash: H256, txs: Vec>); fn mutate_mb_meta(&self, mb_hash: H256, f: impl FnOnce(&mut MbMeta)); } diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index 5f8022a43b2..668a206ccdd 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -400,6 +400,8 @@ pub enum TransactionPurgedReason { #[display("transaction reference block is unknown")] UnknownReferenceBlock = 2, /// + #[display("failed to decryption shielded transaction")] + DecryptionFailed = 3, /// The transaction has a non-zero value, which is not supported yet. /// diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index 7c59438da25..f5b5a393f1e 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -108,6 +108,14 @@ impl Operation { _ => None, } } + + #[cfg(feature = "shielded")] + pub fn into_shielded(self) -> Option { + match self { + Self::Shielded(tx) => Some(tx), + _ => None, + } + } } // Custom encoder/decoder so the discriminant is always a fixed-width `u32` diff --git a/ethexe/compute/src/compute.rs b/ethexe/compute/src/compute.rs index 1d9660e12bf..f9f29623236 100644 --- a/ethexe/compute/src/compute.rs +++ b/ethexe/compute/src/compute.rs @@ -14,7 +14,7 @@ use ethexe_common::{ PromiseEmissionMode, PromisePolicy, db::{CodesStorageRW, CompactMb, ConfigStorageRO, MbStorageRO, MbStorageRW, OnChainStorageRO}, events::BlockRequestEvent, - injected::{Promise, SignedShieldedTransaction}, + injected::Promise, malachite::{Operation, Operations}, }; use ethexe_db::Database; @@ -245,7 +245,7 @@ pub fn prepare_executable_for_mb( build_executable_data( db, - parent, + mb_hash, operations, program_states, schedule, @@ -260,14 +260,15 @@ pub fn prepare_executable_for_mb( /// genesis block from [`ConfigStorageRO::config`]. fn build_executable_data( db: &Database, - parent_mb: H256, + mb_hash: H256, operations: Operations, program_states: ethexe_common::ProgramStates, schedule: ethexe_common::Schedule, initial_advanced_block: H256, ) -> Result { let mut events: Vec = Vec::new(); - let mut injected_transactions = Vec::new(); + // Initialize injected transactions with already unshielded txs. + let mut injected_transactions = db.mb_unshielded_txs(mb_hash); let mut gas_allowance: Option = None; let mut current_anchor = initial_advanced_block; let mut mailbox_validity = ethexe_common::MAILBOX_VALIDITY_VERSION_2; @@ -287,22 +288,8 @@ fn build_executable_data( } current_anchor = block_hash; } - Operation::DecryptionKeys(keys) => { - let transactions = collect_parent_mb_shielded_txs(db, parent_mb)?; - - for tx in transactions { - match keys.get(&tx.data().to_hash()) { - Some(shared_secret) => { - match tx.into_verified().try_map(|tx| tx.unshield(shared_secret)) { - Ok(injected_tx) => injected_transactions.push(injected_tx), - Err(_err) => { - todo!("emit event about invalid transaction decryption") - } - } - } - None => {} - } - } + Operation::DecryptionKeys(_) => { + // ignored } Operation::Injected(signed) => { let verified = signed.into_verified(); @@ -364,34 +351,6 @@ fn build_executable_data( }) } -fn collect_parent_mb_shielded_txs( - db: &Database, - parent_mb: H256, -) -> Result> { - if parent_mb.is_zero() { - return Ok(Default::default()); - } - - let parent_compact = db - .mb_compact_block(parent_mb) - .ok_or_else(|| ComputeError::MbCompactNotFound(parent_mb))?; - - let operations = db - .operations(parent_compact.operations_hash) - .ok_or_else(|| ComputeError::MbPayloadNotFound { - mb_hash: parent_mb, - payload_hash: parent_compact.operations_hash, - })?; - - Ok(operations - .into_iter() - .filter_map(|op| match op { - Operation::Shielded(tx) => Some(tx), - _ => None, - }) - .collect()) -} - /// EBs in `(last_advanced, target]`, oldest-first; capped at 1024. fn collect_advance_chain(db: &Database, target: H256, last_advanced: H256) -> Result> { const MAX_ADVANCE_STEPS: usize = 1024; diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index 34633a34085..9c837b73cfe 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -10,7 +10,7 @@ use crate::{ use anyhow::{Context, Result}; use delegate::delegate; use ethexe_common::{ - BlockHeader, CodeBlobInfo, HashOf, ProgramStates, Schedule, ValidatorsVec, + BlockHeader, CodeBlobInfo, HashOf, ProgramStates, Schedule, ValidatorsVec, VerifiedData, db::{ BlockMeta, BlockMetaStorageRO, BlockMetaStorageRW, CodesStorageRO, CodesStorageRW, CompactMb, ConfigStorageRO, DBConfig, DBGlobals, GlobalsStorageRO, GlobalsStorageRW, @@ -71,6 +71,8 @@ enum Key { Promise(HashOf) = 26, TxReceipt(HashOf) = 27, ShieldedTransaction(HashOf) = 28, + + MbUnshieldedTxs(H256) = 29, } impl Key { @@ -98,7 +100,8 @@ impl Key { | Self::MbOutcome(hash) | Self::MbSchedule(hash) | Self::MbMeta(hash) - | Self::MbCompactBlock(hash) => bytes.extend(hash.as_ref()), + | Self::MbCompactBlock(hash) + | Self::MbUnshieldedTxs(hash) => bytes.extend(hash.as_ref()), Self::InjectedTransaction(hash) | Self::Promise(hash) | Self::TxReceipt(hash) => { bytes.extend(hash.as_ref()) @@ -410,6 +413,16 @@ impl MbStorageRO for RawDatabase { }) } + fn mb_unshielded_txs(&self, mb_hash: H256) -> Vec> { + self.kv + .get(&Key::MbUnshieldedTxs(mb_hash).to_bytes()) + .map(|data| { + Vec::<_>::decode(&mut data.as_slice()) + .expect("Failed to decode data into `Vec>`") + }) + .unwrap_or_default() + } + fn mb_meta(&self, mb_hash: H256) -> MbMeta { self.kv .get(&Key::MbMeta(mb_hash).to_bytes()) @@ -451,6 +464,12 @@ impl MbStorageRW for RawDatabase { .put(&Key::MbSchedule(mb_hash).to_bytes(), schedule.encode()); } + fn set_mb_unshielded_txs(&self, mb_hash: H256, txs: Vec>) { + tracing::trace!(mb_hash = %mb_hash, "Set MB unshielded transactions"); + self.kv + .put(&Key::MbUnshieldedTxs(mb_hash).to_bytes(), txs.encode()); + } + fn mutate_mb_meta(&self, mb_hash: H256, f: impl FnOnce(&mut MbMeta)) { tracing::trace!(mb_hash = %mb_hash, "Mutate MB meta"); let mut meta = self.mb_meta(mb_hash); @@ -999,6 +1018,7 @@ impl MbStorageRO for Database { fn mb_program_states(&self, mb_hash: H256) -> Option; fn mb_outcome(&self, mb_hash: H256) -> Option>; fn mb_schedule(&self, mb_hash: H256) -> Option; + fn mb_unshielded_txs(&self, mb_hash: H256) -> Vec>; fn mb_meta(&self, mb_hash: H256) -> MbMeta; }); } @@ -1010,6 +1030,7 @@ impl MbStorageRW for Database { fn set_mb_program_states(&self, mb_hash: H256, program_states: ProgramStates); fn set_mb_outcome(&self, mb_hash: H256, outcome: Vec); fn set_mb_schedule(&self, mb_hash: H256, schedule: Schedule); + fn set_mb_unshielded_txs(&self, mb_hash: H256, txs: Vec>); fn mutate_mb_meta(&self, mb_hash: H256, f: impl FnOnce(&mut MbMeta)); }); } diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 91ecdf0cd52..6bc196354df 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -50,7 +50,10 @@ use bytes::Bytes; use ethexe_common::{ HashOf, MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, db::{CompactMb, GlobalsStorageRO, GlobalsStorageRW, MbStorageRO, MbStorageRW}, - injected::{MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, ShieldedTransaction, Transaction}, + injected::{ + MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, PurgedTransaction, ShieldedTransaction, Transaction, + TransactionHash, TransactionPurgedReason, + }, malachite::{ MalachiteTdecContext, Operation, Operations, ShieldedTxDecryptionShare, SignedBlockDecryptionShares, @@ -250,7 +253,7 @@ impl Externalities for EthexeExternalities { (process_mb_proposal must run first)" ) })?; - let payload = self.db.operations(compact.operations_hash).ok_or_else(|| { + let operations = self.db.operations(compact.operations_hash).ok_or_else(|| { anyhow!( "mark_finalized: operations blob {} missing for block {mb_hash}", compact.operations_hash @@ -260,13 +263,14 @@ impl Externalities for EthexeExternalities { // Flush the committed injected txs from the mempool and add // their hashes to the seen-set so a re-gossip can't slip them // back in before they age out. - let transactions = payload + let transactions = operations .iter() .filter_map(utils::operation_to_transaction) .collect::>(); if !transactions.is_empty() { self.mempool.forget(&transactions).await; } + drop(transactions); // Advance the canonical pointer downstream consumers // (compute, batch commitment) walk to find the last @@ -291,7 +295,51 @@ impl Externalities for EthexeExternalities { }, last_advanced, ); + + // Retain shares belonging to another block self.decryption_shares.retain_block(mb_hash); + + let Some(decryption_keys) = operations.iter().find_map(|op| match op { + Operation::DecryptionKeys(keys) => Some(keys.clone()), + _ => None, + }) else { + // No need to find shielded transaction, because decryption keys wasn't provided. + return Ok(()); + }; + + let mut not_unshielded = Vec::new(); + let mut unshielded = Vec::new(); + let mut unshielded_hash_mapping = Vec::new(); + + for tx in operations.into_iter().filter_map(|op| op.into_shielded()) { + let tx_hash = tx.data().to_hash(); + match decryption_keys.get(&tx_hash) { + Some(shared_key) => { + match tx.into_verified().try_map(|tx| tx.unshield(shared_key)) { + Ok(injected_tx) => { + unshielded_hash_mapping.push((tx_hash, injected_tx.data().to_hash())); + unshielded.push(injected_tx); + } + Err(_err) => { + not_unshielded.push(PurgedTransaction { + tx_hash: TransactionHash::Right(tx_hash), + reason: TransactionPurgedReason::DecryptionFailed, + }); + } + } + } + None => { + // unreachable case, because in `validate_block_above` we check, that all decryption keys was provided + } + } + } + let _ = self.event_tx.send(Ok(MalachiteEvent::UnshieldingOutput { + mb_hash, + unshielded_hash_mapping, + not_unshielded, + })); + self.db.set_mb_unshielded_txs(mb_hash, unshielded); + Ok(()) } diff --git a/ethexe/malachite/service/src/lib.rs b/ethexe/malachite/service/src/lib.rs index ae9b6d5e4e7..560cca54671 100644 --- a/ethexe/malachite/service/src/lib.rs +++ b/ethexe/malachite/service/src/lib.rs @@ -62,11 +62,15 @@ pub use crate::{ service::MalachiteService, tx_validity::{MIN_EXECUTABLE_BALANCE_FOR_INJECTED_MESSAGES, TxValidity, TxValidityChecker}, }; -use ethexe_common::{injected::PurgedTransaction, malachite::ShieldedTxDecryptionShare}; pub use ethexe_common::{ + HashOf, injected::Transaction, malachite::{Operation, Operations}, }; +use ethexe_common::{ + injected::{InjectedTransaction, PurgedTransaction, ShieldedTransaction}, + malachite::ShieldedTxDecryptionShare, +}; pub use ethexe_malachite_core::{ Multiaddr, PeerId, derive_libp2p_secret, libp2p_peer_id as malachite_libp2p_peer_id, }; @@ -99,6 +103,15 @@ pub enum MalachiteEvent { transactions: Vec, }, + /// Output of unshielding transactions in MB. + UnshieldingOutput { + mb_hash: H256, + /// Mapping from shielded transaction hashes to unshielded transaction hashes. + unshielded_hash_mapping: Vec<(HashOf, HashOf)>, + /// Transactions that could not be unshielded. + not_unshielded: Vec, + }, + /// Decryption shares for shielded transaction in a concrete malachite block. DecryptionShares { mb_hash: H256, @@ -133,6 +146,18 @@ impl std::fmt::Display for MalachiteEvent { transactions.len() ) } + Self::UnshieldingOutput { + mb_hash, + unshielded_hash_mapping, + not_unshielded, + } => { + write!( + f, + "UnshieldingOutput(mb_hash: {mb_hash}, unshielded_len: {}, not_unshielded_len: {})", + unshielded_hash_mapping.len(), + not_unshielded.len() + ) + } Self::DecryptionShares { mb_hash, shares } => { write!( f, diff --git a/ethexe/malachite/service/tests/restart_resilience.rs b/ethexe/malachite/service/tests/restart_resilience.rs index 509057aeb12..d7c95f0e8ca 100644 --- a/ethexe/malachite/service/tests/restart_resilience.rs +++ b/ethexe/malachite/service/tests/restart_resilience.rs @@ -191,6 +191,9 @@ async fn collect_until_finalized( Ok(Some(Ok(MalachiteEvent::DecryptionShares { .. }))) => { // ignore } + Ok(Some(Ok(MalachiteEvent::UnshieldingOutput { .. }))) => { + // ignore + } Ok(Some(Err(e))) => panic!("service error: {e}"), Ok(None) | Err(_) => break, } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 5cc9d4264d0..17fb323bcb4 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -1079,6 +1079,13 @@ impl Service { } } } + MalachiteEvent::UnshieldingOutput { + mb_hash, + unshielded_hash_mapping, + not_unshielded, + } => { + // TODO: handle `unshielded_hash_mapping` and `not_unshielded` in RPC + } }, Event::Prometheus(event) => match event { PrometheusEvent::CollectMetrics { libp2p_metrics } => { diff --git a/protocol/gsigner/src/schemes/secp256k1/keys.rs b/protocol/gsigner/src/schemes/secp256k1/keys.rs index 5e360d58b4b..6d96224c905 100644 --- a/protocol/gsigner/src/schemes/secp256k1/keys.rs +++ b/protocol/gsigner/src/schemes/secp256k1/keys.rs @@ -11,6 +11,7 @@ use alloc::{format, vec::Vec}; use core::{fmt, str::FromStr}; use derive_more::{From, Into}; use k256::ecdsa::VerifyingKey; +use parity_scale_codec::{Decode, Encode}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use sp_core::{ @@ -115,7 +116,7 @@ impl<'de> Deserialize<'de> for PrivateKey { } /// secp256k1 public key backed by `sp_core::ecdsa::Public` (compressed form). -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, From, Into)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, From, Into, Encode, Decode)] pub struct PublicKey(SpPublic); impl PublicKey { diff --git a/protocol/gsigner/src/schemes/secp256k1/signature.rs b/protocol/gsigner/src/schemes/secp256k1/signature.rs index 3116cdfdb5a..780bbcb2a4d 100644 --- a/protocol/gsigner/src/schemes/secp256k1/signature.rs +++ b/protocol/gsigner/src/schemes/secp256k1/signature.rs @@ -375,7 +375,7 @@ where } /// A signature verified data structure with the data and recovered public key. -#[derive(Clone, PartialEq, Eq, Debug, Display, Hash)] +#[derive(Clone, PartialEq, Eq, Debug, Display, Hash, Encode, Decode)] #[display("ValidatedData({data}, {public_key})")] pub struct VerifiedData { data: T, From 14c404a1932773d016c94ccbf8e6e7463ccf1b77 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 23 Jun 2026 16:04:16 +0300 Subject: [PATCH 22/41] chore: adapt RPC to work with TransactionHash --- ethexe/common/src/hash.rs | 10 +- ethexe/common/src/injected.rs | 21 +- ethexe/db/src/database.rs | 6 +- ethexe/malachite/service/src/externalities.rs | 12 +- ethexe/malachite/service/src/mempool.rs | 9 +- ethexe/network/src/injected.rs | 12 +- ethexe/network/src/validator/topic.rs | 6 +- .../rpc/src/apis/injected/promise_manager.rs | 303 +++++++++++++++--- ethexe/rpc/src/apis/injected/server.rs | 11 +- ethexe/rpc/src/apis/injected/spawner.rs | 4 +- ethexe/rpc/src/lib.rs | 12 +- ethexe/service/src/lib.rs | 31 +- ethexe/service/src/tests/mod.rs | 5 +- ethexe/service/src/tests/utils/events.rs | 6 +- 14 files changed, 359 insertions(+), 89 deletions(-) diff --git a/ethexe/common/src/hash.rs b/ethexe/common/src/hash.rs index 24fbd094099..baa314f83c3 100644 --- a/ethexe/common/src/hash.rs +++ b/ethexe/common/src/hash.rs @@ -203,7 +203,7 @@ impl From> for MaybeHashOf { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, derive_more::Display)] +#[derive(Debug, PartialEq, Eq, Hash, Encode, Decode, derive_more::Display)] #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] pub enum EitherHashOf { #[display("Left({_0})")] @@ -212,6 +212,14 @@ pub enum EitherHashOf { Right(HashOf), } +impl Clone for EitherHashOf { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for EitherHashOf {} + impl EitherHashOf { pub fn inner(&self) -> H256 { match self { diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index 668a206ccdd..e43bb3c7bda 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -264,15 +264,10 @@ pub enum Receipt

{ #[cfg(feature = "shielded")] impl Receipt

{ - pub fn tx_hash(&self) -> HashOf { + pub fn tx_hash(&self) -> TransactionHash { match self { - Self::Promise(promise) => promise.tx_hash(), - Self::Purged(purged) => { - let TransactionHash::Left(tx_hash) = purged.tx_hash else { - todo!() - }; - tx_hash - } + Self::Promise(promise) => TransactionHash::Left(promise.tx_hash()), + Self::Purged(purged) => purged.tx_hash, } } } @@ -399,8 +394,8 @@ pub enum TransactionPurgedReason { /// The transaction references a block that is not known locally. #[display("transaction reference block is unknown")] UnknownReferenceBlock = 2, - /// - #[display("failed to decryption shielded transaction")] + /// The shielded transaction could not be decrypted. + #[display("failed to decrypt shielded transaction")] DecryptionFailed = 3, /// The transaction has a non-zero value, which is not supported yet. @@ -572,10 +567,10 @@ pub enum TransactionRef<'op> { #[cfg(feature = "shielded")] impl<'t> TransactionRef<'t> { - pub fn hash(&self) -> HashOf { + pub fn hash(&self) -> TransactionHash { match self { - Self::Injected(tx) => tx.data().to_hash(), - Self::Shielded(_) => todo!("Shielded transaction hash"), + Self::Injected(tx) => TransactionHash::Left(tx.data().to_hash()), + Self::Shielded(tx) => TransactionHash::Right(tx.data().to_hash()), } } diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index 9c837b73cfe..87b5f75da9e 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -21,7 +21,7 @@ use ethexe_common::{ gear::StateTransition, injected::{ InjectedTransaction, Promise, ShieldedTransaction, SignedInjectedTransaction, - SignedShieldedTransaction, SignedTxReceipt, + SignedShieldedTransaction, SignedTxReceipt, TransactionHash, }, malachite::Operations, }; @@ -748,7 +748,9 @@ impl InjectedStorageRW for RawDatabase { } fn set_receipt(&self, receipt: &SignedTxReceipt) { - let tx_hash = receipt.data().tx_hash(); + let TransactionHash::Left(tx_hash) = receipt.data().tx_hash() else { + panic!("only injected transaction receipts can be stored"); + }; tracing::trace!(?receipt, "Set receipt for injected transaction"); self.kv diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 6bc196354df..e6dd3243151 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -1074,11 +1074,9 @@ mod tests { use crate::{MalachiteEvent, mempool::EmptyMempool}; use anyhow::Context; use ethexe_common::{ - BlockHeader, HashOf, + BlockHeader, db::{BlockMetaStorageRW, OnChainStorageRW}, - injected::{ - InjectedTransaction, PurgedTransaction, SignedInjectedTransaction, TransactionRef, - }, + injected::{PurgedTransaction, SignedInjectedTransaction, TransactionRef}, }; fn to_payload(bytes: Vec) -> BlockPayload { @@ -1464,7 +1462,7 @@ mod tests { /// can assert which txs reached the mempool eviction path. #[derive(Default)] struct ForgetTracker { - seen: tokio::sync::Mutex>>, + seen: tokio::sync::Mutex>, } #[async_trait::async_trait] @@ -1582,8 +1580,8 @@ mod tests { 2, "exactly two injected txs should be forgotten" ); - assert!(seen_hashes.contains(&tx_a.data().to_hash())); - assert!(seen_hashes.contains(&tx_b.data().to_hash())); + assert!(seen_hashes.contains(&TransactionHash::Left(tx_a.data().to_hash()))); + assert!(seen_hashes.contains(&TransactionHash::Left(tx_b.data().to_hash()))); } // ------------------------------------------------------------------ diff --git a/ethexe/malachite/service/src/mempool.rs b/ethexe/malachite/service/src/mempool.rs index 71545a8411e..ba942a26a7d 100644 --- a/ethexe/malachite/service/src/mempool.rs +++ b/ethexe/malachite/service/src/mempool.rs @@ -807,14 +807,7 @@ mod tests { let head = chain[2]; let fetched = futures::executor::block_on(pool.fetch(head)); assert_eq!(fetched.len(), 1); - assert_eq!( - fetched[0] - .as_injected() - .expect("injected transaction") - .data() - .to_hash(), - tx_hash - ); + assert_eq!(fetched[0].as_ref().hash(), tx_hash); } #[test] diff --git a/ethexe/network/src/injected.rs b/ethexe/network/src/injected.rs index 5372983b4b6..cb0f9fba9eb 100644 --- a/ethexe/network/src/injected.rs +++ b/ethexe/network/src/injected.rs @@ -7,8 +7,8 @@ use crate::{ validator::discovery::ValidatorIdentities, }; use ethexe_common::{ - Address, HashOf, - injected::{InjectedTransaction, InjectedTransactionAcceptance, Transaction}, + Address, + injected::{InjectedTransactionAcceptance, Transaction, TransactionHash}, }; use futures::{FutureExt, StreamExt, future::BoxFuture, stream::FuturesUnordered}; use libp2p::{ @@ -82,7 +82,7 @@ pub enum Event { }, /// We got a response from a validator we sent transaction to OutboundAcceptance { - transaction_hash: HashOf, + transaction_hash: TransactionHash, acceptance: InjectedTransactionAcceptance, }, } @@ -108,7 +108,7 @@ impl Event { fn unwrap_injected_transaction_acceptance( self, - ) -> (HashOf, InjectedTransactionAcceptance) { + ) -> (TransactionHash, InjectedTransactionAcceptance) { match self { Event::OutboundAcceptance { transaction_hash, @@ -134,9 +134,9 @@ type PendingResponseFuture = BoxFuture<'static, (ResponseChannel, pub(crate) struct Behaviour { inner: InnerBehaviour, - pending_requests: HashMap>, + pending_requests: HashMap, pending_responses: FuturesUnordered, - transaction_cache: LruCache, LruCache>, + transaction_cache: LruCache>, metrics: Metrics, } diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index 7d2b4fc3668..e38a94a5189 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -9,8 +9,8 @@ use crate::{ validator::list::ValidatorListSnapshot, }; use ethexe_common::{ - Address, HashOf, - injected::{InjectedTransaction, SignedCompactTxReceipt}, + Address, + injected::{SignedCompactTxReceipt, TransactionHash}, malachite::SignedBlockDecryptionShares, network::VerifiedValidatorMessage, }; @@ -85,7 +85,7 @@ enum VerifyTxReceiptError { #[display("unknown validator: address={address}, tx_hash={tx_hash}")] UnknownValidator { address: Address, - tx_hash: HashOf, + tx_hash: TransactionHash, }, } diff --git a/ethexe/rpc/src/apis/injected/promise_manager.rs b/ethexe/rpc/src/apis/injected/promise_manager.rs index a2dcbeedb71..3e639f83959 100644 --- a/ethexe/rpc/src/apis/injected/promise_manager.rs +++ b/ethexe/rpc/src/apis/injected/promise_manager.rs @@ -2,33 +2,58 @@ // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 use anyhow::Result; -use dashmap::{DashMap, mapref::entry::Entry}; use ethexe_common::{ Address, HashOf, db::{ ConfigStorageRO, GlobalsStorageRO, InjectedStorageRO, InjectedStorageRW, OnChainStorageRO, }, injected::{ - InjectedTransaction, Promise, SignedCompactTxReceipt, SignedTxReceipt, - TryFillPromiseResult, UnfilledPromiseReceipt, UpgradedReceipt, + InjectedTransaction, Promise, ShieldedTransaction, SignedCompactTxReceipt, SignedTxReceipt, + TransactionHash, TryFillPromiseResult, UnfilledPromiseReceipt, UpgradedReceipt, }, }; use ethexe_db::Database; -use std::sync::Arc; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; use tokio::sync::oneshot; use tracing::{trace, warn}; -// TODO: #5385. -type PromiseSubscribers = - Arc, oneshot::Sender>>; type PendingReceiptsCache = moka::sync::Cache, UnfilledPromiseReceipt>; +#[derive(Debug)] +struct Subscriber { + registration_hash: TransactionHash, + sender: oneshot::Sender, +} + +/// Stores receipt subscribers under both their current routing hash and original registration hash. +/// +/// A shielded transaction is initially routed by [`TransactionHash::Right`]. After unshielding, +/// its subscriber is moved to the corresponding [`TransactionHash::Left`] entry while retaining +/// the original shielded registration hash for cancellation. +#[derive(Debug, Default)] +struct PromiseSubscribers { + /// Subscribers grouped by the hash under which an incoming receipt will currently arrive. + /// + /// Multiple subscribers can share an injected hash after a shielded subscriber is migrated + /// to a hash that already has a directly registered injected subscriber. + subscribers_by_receipt_hash: HashMap>, + /// Maps each original registration hash to its current receipt hash in + /// [`Self::subscribers_by_receipt_hash`]. + /// + /// For an unmigrated subscriber both hashes are identical. For a migrated shielded + /// subscriber this maps its shielded hash to the resulting injected hash. + receipt_hash_by_registration_hash: HashMap, +} + /// The manager for promise subscribers. #[derive(Debug, Clone)] pub struct PromiseSubscriptionManager { db: Database, - /// Active subscribers for injected transaction receipt ([SignedTxReceipt]). - subscribers: PromiseSubscribers, + /// Active subscribers for transaction receipts ([SignedTxReceipt]). + subscribers: Arc>, /// Cached [UnfilledPromiseReceipt] waiting for local [Promise] computation. pending_receipts: PendingReceiptsCache, } @@ -36,7 +61,7 @@ pub struct PromiseSubscriptionManager { #[derive(Debug, Clone, thiserror::Error)] pub enum RegisterSubscriberError { #[error("Subscriber for this transaction already exists, tx_hash={0}")] - AlreadyRegistered(HashOf), + AlreadyRegistered(TransactionHash), } type TimeoutReceiver = tokio::time::Timeout>; @@ -47,7 +72,7 @@ type TimeoutReceiver = tokio::time::Timeout>; /// Important: to avoid infinite waiting we wrap [oneshot::Receiver] into [tokio::time::timeout]. pub struct PendingSubscriber { /// Tx hash waiting promise for. - tx_hash: HashOf, + tx_hash: TransactionHash, /// Wrapped tx receipt [oneshot::Receiver]. receiver: TimeoutReceiver, } @@ -55,7 +80,7 @@ pub struct PendingSubscriber { impl PendingSubscriber { pub fn new( db: &Database, - tx_hash: HashOf, + tx_hash: TransactionHash, receiver: oneshot::Receiver, ) -> Self { let timeout_duration = utils::receipt_waiting_timeout(db); @@ -63,7 +88,7 @@ impl PendingSubscriber { Self { tx_hash, receiver } } - pub fn into_parts(self) -> (HashOf, TimeoutReceiver) { + pub fn into_parts(self) -> (TransactionHash, TimeoutReceiver) { (self.tx_hash, self.receiver) } } @@ -73,30 +98,106 @@ impl PromiseSubscriptionManager { Self { pending_receipts: utils::build_pending_receipts_cache(&db), db, - subscribers: PromiseSubscribers::default(), + subscribers: Arc::default(), } } // TODO: Issue #5402 pub fn try_register_subscriber( &self, - tx_hash: HashOf, + tx_hash: TransactionHash, ) -> 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)) - } + let mut subscribers = self.subscribers.lock().expect("subscribers lock poisoned"); + if subscribers + .receipt_hash_by_registration_hash + .contains_key(&tx_hash) + { + return Err(RegisterSubscriberError::AlreadyRegistered(tx_hash)); } + + let (sender, receiver) = oneshot::channel(); + subscribers + .subscribers_by_receipt_hash + .entry(tx_hash) + .or_default() + .push(Subscriber { + registration_hash: tx_hash, + sender, + }); + subscribers + .receipt_hash_by_registration_hash + .insert(tx_hash, tx_hash); + + Ok(PendingSubscriber::new(&self.db, tx_hash, receiver)) } pub fn cancel_registration( &self, - tx_hash: HashOf, + tx_hash: TransactionHash, ) -> Option> { - self.subscribers.remove(&tx_hash).map(|(_, v)| v) + let mut subscribers = self.subscribers.lock().expect("subscribers lock poisoned"); + let receipt_hash = subscribers + .receipt_hash_by_registration_hash + .remove(&tx_hash)?; + let receipt_subscribers = subscribers + .subscribers_by_receipt_hash + .get_mut(&receipt_hash) + .expect("registered subscriber must exist"); + let position = receipt_subscribers + .iter() + .position(|subscriber| subscriber.registration_hash == tx_hash) + .expect("registered subscriber must exist under its receipt hash"); + let subscriber = receipt_subscribers.swap_remove(position); + if receipt_subscribers.is_empty() { + subscribers + .subscribers_by_receipt_hash + .remove(&receipt_hash); + } + Some(subscriber.sender) + } + + pub fn on_unshielded_transactions( + &self, + hash_mapping: Vec<(HashOf, HashOf)>, + ) { + let moved_to = { + let mut subscribers = self.subscribers.lock().expect("subscribers lock poisoned"); + let mut moved_to = Vec::new(); + + for (shielded_hash, injected_hash) in hash_mapping { + let registration_hash = TransactionHash::Right(shielded_hash); + let receipt_hash = TransactionHash::Left(injected_hash); + if subscribers + .receipt_hash_by_registration_hash + .get(®istration_hash) + != Some(®istration_hash) + { + continue; + } + + let moved = subscribers + .subscribers_by_receipt_hash + .remove(®istration_hash) + .expect("registered shielded subscriber must exist"); + subscribers + .subscribers_by_receipt_hash + .entry(receipt_hash) + .or_default() + .extend(moved); + subscribers + .receipt_hash_by_registration_hash + .insert(registration_hash, receipt_hash); + moved_to.push(injected_hash); + } + + moved_to + }; + + for injected_hash in moved_to { + if let Some(receipt) = self.db.receipt(injected_hash) { + self.dispatch_receipt(receipt); + } + } } // TODO: Issue #5403 @@ -160,11 +261,7 @@ impl PromiseSubscriptionManager { self.signer_is_known_validator(receipt.address(), receipt.data().tx_hash()) } - fn signer_is_known_validator( - &self, - address: Address, - tx_hash: HashOf, - ) -> bool { + fn signer_is_known_validator(&self, address: Address, tx_hash: TransactionHash) -> bool { let timestamp = self.db.globals().latest_synced_eb.header.timestamp; let timelines = self.db.config().timelines; @@ -207,21 +304,43 @@ impl PromiseSubscriptionManager { } fn dispatch_receipt(&self, receipt: SignedTxReceipt) { - if let Some((_, sender)) = self.subscribers.remove(&receipt.data().tx_hash()) - && let Err(unsent_receipt) = sender.send(receipt) - { - trace!("failed to send receipt to subscriber, receipt={unsent_receipt:?}"); + let senders = { + let mut subscribers = self.subscribers.lock().expect("subscribers lock poisoned"); + subscribers + .subscribers_by_receipt_hash + .remove(&receipt.data().tx_hash()) + .unwrap_or_default() + .into_iter() + .map(|subscriber| { + subscribers + .receipt_hash_by_registration_hash + .remove(&subscriber.registration_hash); + subscriber.sender + }) + .collect::>() + }; + + for sender in senders { + if let Err(unsent_receipt) = sender.send(receipt.clone()) { + trace!("failed to send receipt to subscriber, receipt={unsent_receipt:?}"); + } } } fn store_and_dispatch_receipt(&self, receipt: SignedTxReceipt) { - self.db.set_receipt(&receipt); + if matches!(receipt.data().tx_hash(), TransactionHash::Left(_)) { + self.db.set_receipt(&receipt); + } self.dispatch_receipt(receipt); } #[cfg(test)] pub fn subscribers_count(&self) -> usize { - self.subscribers.len() + self.subscribers + .lock() + .expect("subscribers lock poisoned") + .receipt_hash_by_registration_hash + .len() } } @@ -267,7 +386,7 @@ mod tests { Address, SignedMessage, ValidatorsVec, db::{GlobalsStorageRO, OnChainStorageRW, SetGlobals}, ecdsa::PrivateKey, - injected::{InjectedTransaction, Receipt}, + injected::{InjectedTransaction, PurgedTransaction, Receipt, TransactionPurgedReason}, mock::Mock, }; use gear_core::{message::ReplyCode, rpc::ReplyInfo}; @@ -303,7 +422,7 @@ mod tests { manager: &PromiseSubscriptionManager, tx_hash: HashOf, ) -> std::pin::Pin>> { - let pending = match manager.try_register_subscriber(tx_hash) { + let pending = match manager.try_register_subscriber(TransactionHash::Left(tx_hash)) { Ok(pending) => pending, Err(err) => panic!("first registration must succeed: {err}"), }; @@ -381,14 +500,120 @@ mod tests { async fn duplicate_subscriber_rejected() { let manager = PromiseSubscriptionManager::new(Database::memory()); let (promise, _) = make_promise(); - let _first = manager.try_register_subscriber(promise.tx_hash).ok(); + let tx_hash = TransactionHash::Left(promise.tx_hash); + let _first = manager.try_register_subscriber(tx_hash).ok(); let err = manager - .try_register_subscriber(promise.tx_hash) + .try_register_subscriber(tx_hash) .err() .expect("second registration must fail"); assert!(matches!(err, RegisterSubscriberError::AlreadyRegistered(_))); } + #[tokio::test] + async fn shielded_subscriber_migrates_to_injected_hash() { + let db = Database::memory(); + let manager = PromiseSubscriptionManager::new(db.clone()); + let (promise, private_key) = make_promise(); + let mut injected_receiver = register(&manager, promise.tx_hash); + let shielded_hash = HashOf::::random(); + let registration_hash = TransactionHash::Right(shielded_hash); + let pending = manager + .try_register_subscriber(registration_hash) + .expect("first registration must succeed"); + let (_, receiver) = pending.into_parts(); + let mut receiver = Box::pin(receiver.into_inner()); + + manager.on_unshielded_transactions(vec![(shielded_hash, promise.tx_hash)]); + assert_eq!(manager.subscribers_count(), 2); + + manager.on_computed_promise(promise.clone()); + let receipt = + SignedMessage::create(private_key, Receipt::Promise(promise.to_compact())).unwrap(); + set_current_validators(&db, vec![receipt.address()]); + manager.on_tx_receipt(receipt.into()); + + let expected = Receipt::Promise(promise); + assert_eq!(receiver.as_mut().await.unwrap().data(), &expected); + assert_eq!(injected_receiver.as_mut().await.unwrap().data(), &expected); + assert_eq!(manager.subscribers_count(), 0); + } + + #[tokio::test] + async fn migration_dispatches_receipt_that_arrived_under_injected_hash_first() { + let db = Database::memory(); + let manager = PromiseSubscriptionManager::new(db.clone()); + let (promise, private_key) = make_promise(); + let shielded_hash = HashOf::::random(); + let pending = manager + .try_register_subscriber(TransactionHash::Right(shielded_hash)) + .expect("first registration must succeed"); + let (_, receiver) = pending.into_parts(); + let mut receiver = Box::pin(receiver.into_inner()); + + manager.on_computed_promise(promise.clone()); + let receipt = + SignedMessage::create(private_key, Receipt::Promise(promise.to_compact())).unwrap(); + set_current_validators(&db, vec![receipt.address()]); + manager.on_tx_receipt(receipt.into()); + assert_eq!(manager.subscribers_count(), 1); + assert!(db.receipt(promise.tx_hash).is_some()); + + manager.on_unshielded_transactions(vec![(shielded_hash, promise.tx_hash)]); + + assert_eq!( + receiver.as_mut().await.unwrap().data(), + &Receipt::Promise(promise) + ); + assert_eq!(manager.subscribers_count(), 0); + } + + #[tokio::test] + async fn shielded_purge_receipt_dispatches_without_database_storage() { + let db = Database::memory(); + let manager = PromiseSubscriptionManager::new(db.clone()); + let shielded_hash = HashOf::::random(); + let registration_hash = TransactionHash::Right(shielded_hash); + let pending = manager + .try_register_subscriber(registration_hash) + .expect("first registration must succeed"); + let (_, receiver) = pending.into_parts(); + let mut receiver = Box::pin(receiver.into_inner()); + let purged = PurgedTransaction { + tx_hash: registration_hash, + reason: TransactionPurgedReason::DecryptionFailed, + }; + let receipt = SignedMessage::create( + PrivateKey::random(), + Receipt::::Purged(purged.clone()), + ) + .unwrap(); + set_current_validators(&db, vec![receipt.address()]); + + manager.on_tx_receipt(receipt.into()); + + assert_eq!( + receiver.as_mut().await.unwrap().data(), + &Receipt::Purged(purged) + ); + assert_eq!(manager.subscribers_count(), 0); + } + + #[tokio::test] + async fn migrated_shielded_subscriber_can_be_cancelled_by_original_hash() { + let manager = PromiseSubscriptionManager::new(Database::memory()); + let shielded_hash = HashOf::::random(); + let injected_hash = HashOf::::random(); + let registration_hash = TransactionHash::Right(shielded_hash); + let _pending = manager + .try_register_subscriber(registration_hash) + .expect("first registration must succeed"); + + manager.on_unshielded_transactions(vec![(shielded_hash, injected_hash)]); + + assert!(manager.cancel_registration(registration_hash).is_some()); + assert_eq!(manager.subscribers_count(), 0); + } + /// A compact promise whose signature does not match the body that /// arrives later is parked rather than delivering a malformed /// [`SignedTxReceipt`]. diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index b237a1494fd..2c434bab22b 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -11,8 +11,8 @@ use ethexe_common::{ HashOf, db::InjectedStorageRO, injected::{ - InjectedTransaction, InjectedTransactionAcceptance, SignedInjectedTransaction, - SignedTxReceipt, Transaction, + InjectedTransaction, InjectedTransactionAcceptance, ShieldedTransaction, + SignedInjectedTransaction, SignedTxReceipt, Transaction, }, }; use ethexe_db::Database; @@ -140,6 +140,13 @@ impl InjectedApi { Ok(()) } + pub fn on_unshielded_transactions( + &self, + hash_mapping: Vec<(HashOf, HashOf)>, + ) { + self.manager.on_unshielded_transactions(hash_mapping); + } + async fn get_transaction_receipt( &self, tx_hash: HashOf, diff --git a/ethexe/rpc/src/apis/injected/spawner.rs b/ethexe/rpc/src/apis/injected/spawner.rs index 34ebe957f45..3c578353a7f 100644 --- a/ethexe/rpc/src/apis/injected/spawner.rs +++ b/ethexe/rpc/src/apis/injected/spawner.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 use super::promise_manager::PendingSubscriber; -use ethexe_common::{HashOf, injected::InjectedTransaction}; +use ethexe_common::injected::TransactionHash; use jsonrpsee::{SubscriptionMessage, SubscriptionSink}; use tracing::{error, trace, warn}; @@ -14,7 +14,7 @@ pub fn spawn_pending_subscriber( subscriber: PendingSubscriber, on_finish: F, ) where - F: FnOnce(HashOf) + std::marker::Send + 'static, + F: FnOnce(TransactionHash) + std::marker::Send + 'static, { let (tx_hash, receiver) = subscriber.into_parts(); diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index 486140c6d5a..7de11775934 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -48,8 +48,11 @@ use apis::{ InfoServer, InjectedApi, InjectedServer, ProgramApi, ProgramServer, }; #[cfg(feature = "server")] +use ethexe_common::HashOf; +#[cfg(feature = "server")] use ethexe_common::injected::{ - InjectedTransactionAcceptance, Promise, SignedCompactTxReceipt, Transaction, + InjectedTransaction, InjectedTransactionAcceptance, Promise, ShieldedTransaction, + SignedCompactTxReceipt, Transaction, }; #[cfg(feature = "server")] use ethexe_db::Database; @@ -230,6 +233,13 @@ impl RpcService { self.injected_api.on_tx_receipt(receipt); } + pub fn receive_unshielded_transactions( + &self, + hash_mapping: Vec<(HashOf, HashOf)>, + ) { + self.injected_api.on_unshielded_transactions(hash_mapping); + } + pub fn receive_mb_computed(&self, mb_hash: H256) { self.best_state.notify(mb_hash); } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 17fb323bcb4..d41b69f4771 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -1084,7 +1084,36 @@ impl Service { unshielded_hash_mapping, not_unshielded, } => { - // TODO: handle `unshielded_hash_mapping` and `not_unshielded` in RPC + let Some(rpc) = rpc.as_ref() else { + tracing::trace!( + %mb_hash, + "can not handle unshielding output without RPC service" + ); + continue; + }; + + rpc.receive_unshielded_transactions(unshielded_hash_mapping); + + let Some(pub_key) = validator_pub_key else { + tracing::trace!( + %mb_hash, + "validator public key not found, can not sign failed unshielding receipts" + ); + continue; + }; + + not_unshielded.into_iter().for_each(|purged_tx| { + let receipt = Receipt::::Purged(purged_tx); + match signer.signed_message(pub_key, receipt, None) { + Ok(signed_receipt) => rpc.receive_tx_receipt(signed_receipt.into()), + Err(err) => { + tracing::error!( + %mb_hash, + "failed to sign unshielding receipt: {err}" + ); + } + } + }); } }, Event::Prometheus(event) => match event { diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 714ee8f1a82..83ecc9e5101 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -2667,7 +2667,10 @@ async fn injected_tx_fungible_token() { .await .expect("subscription produce value") .expect("no errors for correct injected transaction"); - assert_eq!(subscription_receipt.data().tx_hash(), mint_tx.to_hash()); + assert_eq!( + subscription_receipt.data().tx_hash(), + TransactionHash::Left(mint_tx.to_hash()) + ); let subscription_promise = subscription_receipt.data().clone().unwrap_promise(); assert_eq!(subscription_promise.reply.value, 0); assert_eq!( diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index 4073f49c356..a72706a23ac 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -8,11 +8,11 @@ use alloy::providers::{RootProvider, ext::AnvilApi}; use async_broadcast::{Receiver, RecvError, Sender}; use ethexe_blob_loader::BlobLoaderEvent; use ethexe_common::{ - Address, HashOf, SimpleBlockData, + Address, SimpleBlockData, db::*, events::BlockEvent, injected::{ - InjectedTransaction, InjectedTransactionAcceptance, SignedCompactTxReceipt, Transaction, + InjectedTransactionAcceptance, SignedCompactTxReceipt, Transaction, TransactionHash, }, malachite::SignedBlockDecryptionShares, network::VerifiedValidatorMessage, @@ -48,7 +48,7 @@ pub enum TestingNetworkInjectedEvent { transaction: Transaction, }, OutboundAcceptance { - transaction_hash: HashOf, + transaction_hash: TransactionHash, acceptance: InjectedTransactionAcceptance, }, } From 03ec325287aebefa74d89ad458440b0cb9ab84d4 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 23 Jun 2026 16:23:36 +0300 Subject: [PATCH 23/41] chore(service-test-env): add tdec setup for testing nodes --- Cargo.lock | 1 + ethexe/malachite/service/src/lib.rs | 4 +- ethexe/malachite/service/src/service.rs | 21 +++- ethexe/service/Cargo.toml | 2 + ethexe/service/src/tests/mod.rs | 1 + ethexe/service/src/tests/utils/env.rs | 143 +++++++++++++++++++++++- 6 files changed, 161 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3549c22410..49b0ab7dac4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6085,6 +6085,7 @@ dependencies = [ "ethexe-rpc", "ethexe-runtime-common", "ethexe-service-utils", + "ferveo-gear-tdec", "futures", "gear-core", "gear-core-errors", diff --git a/ethexe/malachite/service/src/lib.rs b/ethexe/malachite/service/src/lib.rs index 560cca54671..7ed2bb5af4b 100644 --- a/ethexe/malachite/service/src/lib.rs +++ b/ethexe/malachite/service/src/lib.rs @@ -35,7 +35,7 @@ //! ## Caller Invariants //! //! - Construct with `MalachiteService::new(config, db, signer, validator_pub_key, -//! mempool)`. A `Some` key starts a `Validator` and must appear in +//! validator_tdec_setup, mempool)`. A `Some` key starts a `Validator` and must appear in //! `config.validators`; `None` starts a gossip/sync-only `FullNode`. `new` //! returns `Err` if `config.validators` is empty or the local key is absent. //! - `BlockProposal` is always emitted before the matching `BlockFinalized` for a @@ -59,7 +59,7 @@ mod tx_validity; pub use crate::{ config::{MalachiteConfig, ValidatorEntry}, mempool::{DEFAULT_POOL_CAPACITY, InjectedTxMempool, Mempool, TxInsertionStatus}, - service::MalachiteService, + service::{MalachiteService, ValidatorTdecSetup}, tx_validity::{MIN_EXECUTABLE_BALANCE_FOR_INJECTED_MESSAGES, TxValidity, TxValidityChecker}, }; pub use ethexe_common::{ diff --git a/ethexe/malachite/service/src/service.rs b/ethexe/malachite/service/src/service.rs index 2ec7c97d7e6..e607e651ecc 100644 --- a/ethexe/malachite/service/src/service.rs +++ b/ethexe/malachite/service/src/service.rs @@ -33,7 +33,7 @@ use ethexe_common::{ use ethexe_db::Database; use futures::{Stream, stream::FusedStream}; use gprimitives::H256; -use gsigner::{Signer, schemes::secp256k1::Secp256k1}; +use gsigner::{Signer, TdecKeyStore, schemes::secp256k1::Secp256k1}; use tokio::sync::{Notify, mpsc}; use crate::{ @@ -41,6 +41,15 @@ use crate::{ decryption_shares::DecryptionSharesStore, externalities::EthexeExternalities, }; +/// Public threshold-decryption context and local private-key storage for one validator. +#[derive(Clone, Debug)] +pub struct ValidatorTdecSetup { + /// Public contexts used to create and verify validator decryption shares. + pub context: MalachiteTdecContext, + /// Store containing this validator's private threshold-decryption key. + pub key_store: TdecKeyStore, +} + /// Public consensus service. pub struct MalachiteService { events_rx: mpsc::UnboundedReceiver>, @@ -99,7 +108,7 @@ impl MalachiteService { db: Database, signer: Signer, validator_pub_key: Option, - validator_tdec_ctx: Option, + validator_tdec_setup: Option, mempool: Arc, ) -> Result { tracing::info!( @@ -157,13 +166,15 @@ impl MalachiteService { let chain_head_notify = Arc::new(Notify::new()); let decryption_shares = Arc::new(DecryptionSharesStore::new()); let (events_tx, events_rx) = mpsc::unbounded_channel(); + let (tdec_ctx, tdec_store) = validator_tdec_setup + .map(|setup| (Some(setup.context), setup.key_store)) + .unwrap_or_else(|| (None, TdecKeyStore::memory())); let externalities = Arc::new(EthexeExternalities { db, - // TODO: FIXME (temporary solution) - tdec_ctx: validator_tdec_ctx, - tdec_store: gsigner::TdecKeyStore::memory(), + tdec_ctx, + tdec_store, mempool: Arc::clone(&mempool), chain_head: Arc::clone(&chain_head), diff --git a/ethexe/service/Cargo.toml b/ethexe/service/Cargo.toml index 4a336db5492..ddd9c33500b 100644 --- a/ethexe/service/Cargo.toml +++ b/ethexe/service/Cargo.toml @@ -73,6 +73,8 @@ async-broadcast.workspace = true wat.workspace = true tempfile.workspace = true rand.workspace = true +gear-tdec.workspace = true +gsigner = { workspace = true, features = ["tdec"] } demo-ping = { workspace = true, features = ["debug", "ethexe"] } demo-value-sender-ethexe = { workspace = true, features = ["debug", "ethexe"] } diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 83ecc9e5101..37423c7b7d4 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -3059,6 +3059,7 @@ async fn injected_tx_fungible_token_over_network() { stop_nodes([alice_node, bob_node]).await; } +#[ignore = "_"] #[tokio::test] #[ntest::timeout(60_000)] async fn shielded_tx_fungible_token() { diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index 87322373dd8..206f7120679 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -39,7 +39,7 @@ use ethexe_ethereum::{ }; use ethexe_malachite::{ InjectedTxMempool, MalachiteConfig, MalachiteService, Multiaddr as MalachiteMultiaddr, PeerId, - ValidatorEntry, derive_libp2p_secret, malachite_libp2p_peer_id, + ValidatorEntry, ValidatorTdecSetup, derive_libp2p_secret, malachite_libp2p_peer_id, }; use ethexe_network::{NetworkConfig, NetworkRuntimeConfig, NetworkService, export::Multiaddr}; use ethexe_observer::{ @@ -50,8 +50,12 @@ 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; +use gear_tdec::bls12_381::{DkgPublicKey, E as Bls12_381}; use gprimitives::{ActorId, CodeId, H160, H256, MessageId}; -use gsigner::secp256k1::{Secp256k1SignerExt, Signer}; +use gsigner::{ + TdecKeyStore, + secp256k1::{Secp256k1SignerExt, Signer}, +}; use jsonrpsee::{ http_client::HttpClient, ws_client::{WsClient, WsClientBuilder}, @@ -60,7 +64,7 @@ use std::{ collections::HashMap, fmt, mem, net::{SocketAddr, TcpListener}, - num::NonZero, + num::{NonZero, NonZeroUsize}, pin::Pin, sync::atomic::{AtomicUsize, Ordering}, time::Duration, @@ -104,6 +108,7 @@ pub struct TestEnv { pub ethereum: Ethereum, pub signer: Signer, pub validators: Vec, + pub tdec_public_key: DkgPublicKey, pub sender_id: ActorId, pub threshold: u64, pub continuous_block_generation: bool, @@ -117,6 +122,7 @@ pub struct TestEnv { pub malachite_endpoints: Vec, /// Pre-bound TCP listeners holding each validator's port until handed off in `new_node`. malachite_listeners: HashMap, + validator_tdec_setups: HashMap, router_query: RouterQuery, /// In order to reduce amount of observers, we create only one observer and broadcast events to all subscribers. @@ -165,7 +171,90 @@ fn build_malachite_endpoints( (endpoints, listener_map) } +fn build_validator_tdec_setups( + validators: &[ValidatorConfig], + threshold: u64, +) -> (DkgPublicKey, HashMap) { + let threshold = usize::try_from(threshold).expect("TDEC threshold must fit usize"); + assert!( + threshold > 0 && threshold <= validators.len(), + "invalid TDEC threshold {threshold} for {} validators", + validators.len(), + ); + + let dealer = gear_tdec::deal::(validators.len(), threshold, &mut rand::thread_rng()); + let public_key = dealer.public_key; + let private_contexts = dealer.private_contexts; + let public_contexts = private_contexts + .first() + .expect("validator set must be non-empty") + .public_decryption_contexts + .iter() + .take(validators.len()) + .cloned() + .collect::>(); + assert_eq!(validators.len(), public_contexts.len()); + + let contexts: HashMap = validators + .iter() + .zip(&public_contexts) + .map(|(validator, context)| (validator.public_key.to_address(), context.clone())) + .collect(); + let threshold = NonZeroUsize::new(threshold).expect("threshold was checked above"); + let setups = validators + .iter() + .zip(private_contexts) + .map(|(validator, private_context)| { + let key_store = TdecKeyStore::memory(); + key_store + .import_decryption_key(private_context.validator_decryption_key) + .expect("dealer TDEC key must be importable"); + let my_context = public_contexts + .get(private_context.index) + .expect("private context index must reference a public context") + .clone(); + + ( + validator.public_key, + ValidatorTdecSetup { + context: ethexe_common::malachite::MalachiteTdecContext { + threshold, + my_context, + contexts: contexts.clone(), + }, + key_store, + }, + ) + }) + .collect(); + + (public_key, setups) +} + impl TestEnv { + fn ensure_validator_tdec_setups(&mut self) { + let setup_matches_active_validators = self.validators.first().is_some_and(|validator| { + self.validator_tdec_setups + .get(&validator.public_key) + .is_some_and(|setup| { + setup.context.contexts.len() == self.validators.len() + && self.validators.iter().all(|validator| { + setup + .context + .contexts + .contains_key(&validator.public_key.to_address()) + }) + }) + }); + if setup_matches_active_validators { + return; + } + + let (public_key, setups) = build_validator_tdec_setups(&self.validators, self.threshold); + self.tdec_public_key = public_key; + self.validator_tdec_setups = setups; + } + pub async fn new(config: TestEnvConfig) -> anyhow::Result { let TestEnvConfig { validators, @@ -356,6 +445,8 @@ impl TestEnv { }; let threshold = router_query.validators_threshold().await?; + let (tdec_public_key, validator_tdec_setups) = + build_validator_tdec_setups(&validator_configs, threshold); let network_address = match network { EnvNetworkConfig::Disabled => None, @@ -427,6 +518,7 @@ impl TestEnv { ethereum, signer, validators: validator_configs, + tdec_public_key, sender_id: ActorId::from(H160::from(sender_address.0)), threshold, continuous_block_generation, @@ -437,6 +529,7 @@ impl TestEnv { db, malachite_endpoints, malachite_listeners, + validator_tdec_setups, router_query, observer_events, bootstrap_network, @@ -496,9 +589,15 @@ impl TestEnv { .as_ref() .and_then(|c| self.malachite_listeners.remove(&c.public_key)); + self.ensure_validator_tdec_setups(); + // Snapshot env.validators now so a node spawned post-rotation boots with the new set. let active_validator_pub_keys: Vec = self.validators.iter().map(|v| v.public_key).collect(); + let validator_tdec_setup = validator_config + .as_ref() + .and_then(|config| self.validator_tdec_setups.get(&config.public_key)) + .cloned(); Node { name, @@ -513,6 +612,7 @@ impl TestEnv { signer: self.signer.clone(), threshold: self.threshold, validator_config, + validator_tdec_setup, network_public_key, network_address, network_bootstrap_address, @@ -1017,6 +1117,7 @@ pub struct Node { signer: Signer, threshold: u64, validator_config: Option, + validator_tdec_setup: Option, network_public_key: Option, network_address: Option, network_bootstrap_address: Option, @@ -1202,7 +1303,7 @@ impl Node { self.db.clone(), self.signer.clone(), self.validator_config.as_ref().map(|c| c.public_key), - None, + self.validator_tdec_setup.clone(), mempool, ) .await @@ -1589,3 +1690,37 @@ pub async fn stop_nodes(nodes: impl IntoIterator) { drop(node); } } + +#[test] +fn validator_tdec_setups_create_decryption_shares() { + let validators = (0..3) + .map(|_| { + let public_key = gsigner::secp256k1::PrivateKey::random().public_key(); + ValidatorConfig { + public_key, + session_public_key: public_key, + } + }) + .collect::>(); + let (public_key, setups) = build_validator_tdec_setups(&validators, 2); + let ciphertext = + gear_tdec::encrypt_raw::(b"test", b"aad", &public_key, &mut rand::thread_rng()) + .expect("test payload must be encrypted"); + + for validator in validators { + let setup = setups + .get(&validator.public_key) + .expect("each validator must have a TDEC setup"); + assert_eq!(setup.context.contexts.len(), setups.len()); + assert!( + setup + .context + .contexts + .contains_key(&validator.public_key.to_address()) + ); + setup + .key_store + .create_share(&setup.context.my_context, &ciphertext.header(), b"aad") + .expect("validator must create a decryption share"); + } +} From e124124f88453ffd82d2cc2274fb0ac107df4f7d Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 23 Jun 2026 19:03:17 +0300 Subject: [PATCH 24/41] chore: parse tdec config from CLI --- Cargo.lock | 23 +-- ethexe/cli/Cargo.toml | 1 + ethexe/cli/src/params/mod.rs | 9 ++ ethexe/cli/src/params/tdec.rs | 178 ++++++++++++++++++++++++ ethexe/common/src/malachite.rs | 1 + ethexe/malachite/core/Cargo.toml | 1 + ethexe/malachite/core/src/config.rs | 2 +- ethexe/malachite/service/src/service.rs | 38 ++++- ethexe/service/Cargo.toml | 2 +- ethexe/service/src/config.rs | 25 +++- ethexe/service/src/lib.rs | 16 ++- ethexe/service/src/tests/utils/env.rs | 32 ++--- ethexe/service/tests/smoke.rs | 1 + 13 files changed, 287 insertions(+), 42 deletions(-) create mode 100644 ethexe/cli/src/params/tdec.rs diff --git a/Cargo.lock b/Cargo.lock index 49b0ab7dac4..c5371e2e386 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5544,7 +5544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5591,6 +5591,7 @@ dependencies = [ "ethexe-runtime-common", "ethexe-sdk", "ethexe-service", + "ferveo-gear-tdec", "gear-workspace-hack", "gprimitives", "gsigner", @@ -5791,6 +5792,7 @@ dependencies = [ "arc-malachitebft-test", "async-trait", "bytes", + "derive_more 2.1.1", "futures", "gear-core", "gear-workspace-hack", @@ -6233,7 +6235,7 @@ dependencies = [ [[package]] name = "ferveo-gear-common" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#4ec5f2ee169ce14af865fd1fb34d79b5e72881c5" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#76a41689d406724dad41dcff1cc62b383150fa31" dependencies = [ "ark-ec 0.5.0", "ark-serialize 0.5.0", @@ -6250,7 +6252,7 @@ dependencies = [ [[package]] name = "ferveo-gear-tdec" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#4ec5f2ee169ce14af865fd1fb34d79b5e72881c5" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#76a41689d406724dad41dcff1cc62b383150fa31" dependencies = [ "ark-bls12-381 0.5.0", "ark-ec 0.5.0", @@ -6262,6 +6264,7 @@ dependencies = [ "chacha20poly1305", "const-hex", "ferveo-gear-common", + "hex", "itertools 0.10.5", "parity-scale-codec", "rand 0.8.5", @@ -9671,7 +9674,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -15631,7 +15634,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -15644,7 +15647,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -15747,7 +15750,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -15768,7 +15771,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 1.0.5", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -18879,7 +18882,7 @@ dependencies = [ [[package]] name = "subproductdomain-gear" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#4ec5f2ee169ce14af865fd1fb34d79b5e72881c5" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#76a41689d406724dad41dcff1cc62b383150fa31" dependencies = [ "anyhow", "ark-ec 0.5.0", @@ -19355,7 +19358,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/ethexe/cli/Cargo.toml b/ethexe/cli/Cargo.toml index bf7821e5d95..1767b9245bb 100644 --- a/ethexe/cli/Cargo.toml +++ b/ethexe/cli/Cargo.toml @@ -36,6 +36,7 @@ ethexe-processor.workspace = true ethexe-runtime-common.workspace = true ethexe-db.workspace = true gprimitives = { workspace = true, features = ["std"] } +gear-tdec.workspace = true anyhow.workspace = true alloy-chains.workspace = true diff --git a/ethexe/cli/src/params/mod.rs b/ethexe/cli/src/params/mod.rs index 284077c43be..7f8788387fb 100644 --- a/ethexe/cli/src/params/mod.rs +++ b/ethexe/cli/src/params/mod.rs @@ -19,6 +19,7 @@ mod network; mod node; mod prometheus; mod rpc; +mod tdec; pub use ethereum::EthereumParams; pub use malachite::MalachiteParams; @@ -26,6 +27,7 @@ pub use network::NetworkParams; pub use node::NodeParams; pub use prometheus::PrometheusParams; pub use rpc::RpcParams; +pub use tdec::TdecParams; /// CLI/TOML-config parameters for the ethexe service. #[derive(Clone, Debug, Default, Deserialize, Parser)] @@ -57,6 +59,9 @@ pub struct Params { /// Prometheus (metrics) service parameters. #[clap(flatten)] pub prometheus: Option, + + #[clap(flatten)] + pub tdec: Option, } impl Params { @@ -81,6 +86,7 @@ impl Params { malachite, rpc, prometheus, + tdec, } = self; let node = node.context("missing node params")?; @@ -99,6 +105,7 @@ impl Params { let malachite = malachite.unwrap_or_default().into_config()?; let rpc = rpc.and_then(|p| p.into_config(&node)); let prometheus = prometheus.and_then(|p| p.into_config()); + let tdec = tdec.map(|p| p.into_config()); Ok(Config { node, ethereum, @@ -106,6 +113,7 @@ impl Params { malachite, rpc, prometheus, + tdec, }) } } @@ -119,6 +127,7 @@ impl MergeParams for Params { malachite: MergeParams::optional_merge(self.malachite, with.malachite), rpc: MergeParams::optional_merge(self.rpc, with.rpc), prometheus: MergeParams::optional_merge(self.prometheus, with.prometheus), + tdec: MergeParams::optional_merge(self.tdec, with.tdec), } } } diff --git a/ethexe/cli/src/params/tdec.rs b/ethexe/cli/src/params/tdec.rs new file mode 100644 index 00000000000..330db4676ba --- /dev/null +++ b/ethexe/cli/src/params/tdec.rs @@ -0,0 +1,178 @@ +// Copyright (C) Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +use crate::params::MergeParams; +use ethexe_service::config::ThresholdDecryptionCliConfig; +use gear_tdec::bls12_381::{ + DkgPublicKey, PublicDecryptionContextSimple as PublicDecryptionContext, +}; +use gsigner::Address; + +/// Threshold-decryption parameters. +#[derive(Clone, Debug, serde::Deserialize, clap::Parser)] +pub struct TdecParams { + /// Minimal number of validator decryption shares required to decrypt a + /// shielded transaction. + #[arg(long)] + pub threshold: std::num::NonZeroUsize, + + /// DKG public key used by clients to encrypt shielded transaction fields. + #[arg(long = "dkg-public-key", alias = "pubic-key")] + #[serde(rename = "dkg-public-key")] + pub dkg_public_key: DkgPublicKey, + + /// Public decryption contexts for validators participating in threshold + /// decryption. + /// + /// Pass one option per validator as `ADDRESS=CONTEXT`, where `ADDRESS` is + /// a secp256k1 validator address and `CONTEXT` is the hex string produced + /// by `PublicDecryptionContextSimple`. + #[arg(long = "validators-contexts", value_name = "ADDRESS=CONTEXT")] + #[serde(rename = "validators-contexts")] + pub validators_contexts: Option>, +} + +impl TdecParams { + pub fn into_config(self) -> ThresholdDecryptionCliConfig { + ThresholdDecryptionCliConfig { + threshold: self.threshold, + dkg_public_key: self.dkg_public_key, + validators_contexts: self + .validators_contexts + .map(|ctxs| ctxs.into_iter().map(ValidatorContext::into_parts).collect()), + } + } +} + +impl MergeParams for TdecParams { + fn merge(self, with: Self) -> Self { + let validators_contexts = match with.validators_contexts { + Some(mut contexts) => { + if let Some(my_contexts) = self.validators_contexts { + contexts.extend(my_contexts); + } + Some(contexts) + } + None => self.validators_contexts, + }; + Self { + threshold: self.threshold, + dkg_public_key: self.dkg_public_key, + validators_contexts, + } + } +} + +#[derive(Clone, Debug)] +pub struct ValidatorContext { + pub address: Address, + pub context: PublicDecryptionContext, +} + +impl ValidatorContext { + fn into_parts(self) -> (Address, PublicDecryptionContext) { + (self.address, self.context) + } +} + +impl From<(Address, PublicDecryptionContext)> for ValidatorContext { + fn from((address, context): (Address, PublicDecryptionContext)) -> Self { + Self { address, context } + } +} + +impl std::str::FromStr for ValidatorContext { + type Err = anyhow::Error; + + fn from_str(value: &str) -> Result { + let (address, context) = value.split_once('=').ok_or_else(|| { + anyhow::anyhow!("expected validator context in ADDRESS=CONTEXT format") + })?; + + Ok(Self { + address: address.parse()?, + context: context.parse()?, + }) + } +} + +impl<'de> serde::Deserialize<'de> for ValidatorContext { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(serde::Deserialize)] + #[serde(untagged)] + enum ValidatorContextRepr { + Named { + address: Address, + context: PublicDecryptionContext, + }, + Tuple((Address, PublicDecryptionContext)), + } + + match ValidatorContextRepr::deserialize(deserializer)? { + ValidatorContextRepr::Named { address, context } => Ok(Self { address, context }), + ValidatorContextRepr::Tuple(tuple) => Ok(tuple.into()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + + fn public_decryption_context( + dealer: &gear_tdec::DealerOutput, + ) -> PublicDecryptionContext { + dealer.private_contexts[0].public_decryption_contexts[0].clone() + } + + #[test] + fn validator_context_parses_from_cli_value() { + let dealer = gear_tdec::deal::( + 1, + 1, + &mut gear_tdec::rand_utils::test_rng(), + ); + let address = Address::from([1; 20]); + let context = public_decryption_context(&dealer); + + let parsed = format!("{address}={context}") + .parse::() + .expect("validator context must parse"); + + assert_eq!(parsed.address, address); + assert_eq!(parsed.context.to_string(), context.to_string()); + } + + #[test] + fn tdec_params_accepts_validator_contexts_from_clap() { + let dealer = gear_tdec::deal::( + 1, + 1, + &mut gear_tdec::rand_utils::test_rng(), + ); + let address = Address::from([1; 20]); + let context = public_decryption_context(&dealer); + let context_arg = format!("{address}={context}"); + + let params = TdecParams::try_parse_from([ + "ethexe", + "--threshold", + "1", + "--dkg-public-key", + &dealer.public_key.to_string(), + "--validators-contexts", + &context_arg, + ]) + .expect("tdec params must parse"); + + let contexts = params + .validators_contexts + .expect("validator contexts must be present"); + assert_eq!(contexts.len(), 1); + assert_eq!(contexts[0].address, address); + assert_eq!(contexts[0].context.to_string(), context.to_string()); + } +} diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index f5b5a393f1e..3688df3c4e7 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -191,6 +191,7 @@ impl Operations { } } +/// Validator's context for shielded transactions decryption. #[cfg(all(feature = "shielded", feature = "std"))] #[derive(Debug, Clone)] pub struct MalachiteTdecContext { diff --git a/ethexe/malachite/core/Cargo.toml b/ethexe/malachite/core/Cargo.toml index 0a9f1290f84..e4ee7bf134c 100644 --- a/ethexe/malachite/core/Cargo.toml +++ b/ethexe/malachite/core/Cargo.toml @@ -33,6 +33,7 @@ serde = { workspace = true, features = ["derive"] } sha3 = { workspace = true, features = ["std"] } tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "sync", "time"] } tracing.workspace = true +derive_more = { workspace = true, features = ["is_variant"] } # Crypto + libp2p (kept ethexe-shaped per design: secp256k1 + 20-byte addresses). # Address type is reused from gsigner so the application side (ethexe today, diff --git a/ethexe/malachite/core/src/config.rs b/ethexe/malachite/core/src/config.rs index f97e716074f..2f6f54b63c1 100644 --- a/ethexe/malachite/core/src/config.rs +++ b/ethexe/malachite/core/src/config.rs @@ -33,7 +33,7 @@ pub struct ValidatorEntry { /// [`crate::Externalities::process_mb_finalized`] just like a /// validator would. Use this for read-only observers, /// quarantine workers, light clients, etc. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, derive_more::IsVariant)] pub enum NodeRole { /// Sign votes and proposals; broadcast a validator proof on /// connect; the local address must appear in [`MalachiteConfig::validators`]. diff --git a/ethexe/malachite/service/src/service.rs b/ethexe/malachite/service/src/service.rs index e607e651ecc..b13ad078abb 100644 --- a/ethexe/malachite/service/src/service.rs +++ b/ethexe/malachite/service/src/service.rs @@ -18,12 +18,13 @@ use std::{ collections::HashMap, + num::NonZeroUsize, pin::Pin, sync::{Arc, RwLock}, task::{Context, Poll}, }; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Context as _, Result, anyhow, bail}; use ethexe_common::{ Address, SimpleBlockData, db::{ConfigStorageRO, OnChainStorageRO}, @@ -32,8 +33,9 @@ use ethexe_common::{ }; use ethexe_db::Database; use futures::{Stream, stream::FusedStream}; +use gear_tdec::bls12_381::DkgPublicKey; use gprimitives::H256; -use gsigner::{Signer, TdecKeyStore, schemes::secp256k1::Secp256k1}; +use gsigner::{PublicDecryptionContext, Signer, TdecKeyStore, schemes::secp256k1::Secp256k1}; use tokio::sync::{Notify, mpsc}; use crate::{ @@ -44,8 +46,12 @@ use crate::{ /// Public threshold-decryption context and local private-key storage for one validator. #[derive(Clone, Debug)] pub struct ValidatorTdecSetup { + /// Minimal number of shares for transaction decryption. + pub threshold: NonZeroUsize, + /// Dkg public key for transactions shielding. + pub dkg_public_key: DkgPublicKey, /// Public contexts used to create and verify validator decryption shares. - pub context: MalachiteTdecContext, + pub validators_contexts: Option>, /// Store containing this validator's private threshold-decryption key. pub key_store: TdecKeyStore, } @@ -147,6 +153,29 @@ impl MalachiteService { ), }; + let (tdec_ctx, tdec_store) = match validator_tdec_setup { + Some(setup) if setup.validators_contexts.is_none() && role.is_validator() => { + bail!("validator must have other validators decryption contexts") + } + Some(setup) => { + let contexts = setup.validators_contexts.expect("infallible"); + let my_address = validator_pub_key.map(|key| key.to_address()).unwrap(); + let my_context = contexts + .get(&my_address) + .cloned() + .context("current validator decryption context not found")?; + let context = MalachiteTdecContext { + threshold: setup.threshold, + my_context, + contexts, + }; + + (Some(context), setup.key_store) + } + None if role.is_validator() => bail!("validator must have a tdec context"), + None => (None, TdecKeyStore::memory()), + }; + // Build the ethexe-malachite-core-side config. Application-side knobs // (gas allowance, quarantine depth) stay in [`MalachiteConfig`] // and travel into the externalities; they never reach @@ -166,9 +195,6 @@ impl MalachiteService { let chain_head_notify = Arc::new(Notify::new()); let decryption_shares = Arc::new(DecryptionSharesStore::new()); let (events_tx, events_rx) = mpsc::unbounded_channel(); - let (tdec_ctx, tdec_store) = validator_tdec_setup - .map(|setup| (Some(setup.context), setup.key_store)) - .unwrap_or_else(|| (None, TdecKeyStore::memory())); let externalities = Arc::new(EthexeExternalities { db, diff --git a/ethexe/service/Cargo.toml b/ethexe/service/Cargo.toml index ddd9c33500b..08536d995e2 100644 --- a/ethexe/service/Cargo.toml +++ b/ethexe/service/Cargo.toml @@ -28,6 +28,7 @@ ethexe-rpc = { workspace = true, features = ["server"] } gsigner = { workspace = true, features = ["std", "secp256k1", "codec", "keyring", "serde"] } gear-core.workspace = true gprimitives = { workspace = true, features = ["std", "ethexe"] } +gear-tdec.workspace = true log.workspace = true tracing.workspace = true @@ -73,7 +74,6 @@ async-broadcast.workspace = true wat.workspace = true tempfile.workspace = true rand.workspace = true -gear-tdec.workspace = true gsigner = { workspace = true, features = ["tdec"] } demo-ping = { workspace = true, features = ["debug", "ethexe"] } diff --git a/ethexe/service/src/config.rs b/ethexe/service/src/config.rs index 0a0707b0842..1931f55a4c0 100644 --- a/ethexe/service/src/config.rs +++ b/ethexe/service/src/config.rs @@ -8,8 +8,18 @@ use ethexe_malachite::Multiaddr; use ethexe_network::NetworkConfig; use ethexe_prometheus::PrometheusConfig; use ethexe_rpc::RpcConfig; -use gsigner::secp256k1::{Address, PublicKey}; -use std::{collections::BTreeMap, net::SocketAddr, path::PathBuf, str::FromStr, time::Duration}; +use gear_tdec::bls12_381::DkgPublicKey; +use gsigner::{ + PublicDecryptionContext, + secp256k1::{Address, PublicKey}, +}; +use std::{ + collections::{BTreeMap, HashMap}, + net::SocketAddr, + path::PathBuf, + str::FromStr, + time::Duration, +}; #[derive(Debug)] pub struct Config { @@ -19,6 +29,7 @@ pub struct Config { pub malachite: MalachiteCliConfig, pub rpc: Option, pub prometheus: Option, + pub tdec: Option, } /// User-facing subset of [`ethexe_malachite::MalachiteConfig`], @@ -156,3 +167,13 @@ pub struct EthereumConfig { pub eip1559_max_fee_per_gas_in_gwei: u128, pub blob_gas_multiplier: u128, } + +#[derive(Clone, Debug)] +pub struct ThresholdDecryptionCliConfig { + /// Decryption threshold parameter. + pub threshold: std::num::NonZeroUsize, + /// Validator's dkg public key. + pub dkg_public_key: DkgPublicKey, + /// Other validators public decryption contexts. + pub validators_contexts: Option>, +} diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index d41b69f4771..51012d88082 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -59,6 +59,7 @@ use ethexe_db::{ use ethexe_ethereum::{EthereumBuilder, deploy::EthereumDeployer, router::RouterQuery}; use ethexe_malachite::{ InjectedTxMempool, MalachiteConfig, MalachiteEvent, MalachiteService, ValidatorEntry, + ValidatorTdecSetup, }; use ethexe_network::{ NetworkEvent, NetworkRuntimeConfig, NetworkService, db_sync::ExternalDataProvider, @@ -73,7 +74,10 @@ use ethexe_rpc::{RpcEvent, RpcServer}; use ethexe_service_utils::{OptionFuture as _, OptionStreamNext as _}; use futures::{FutureExt, StreamExt}; use gprimitives::{ActorId, CodeId, H256}; -use gsigner::secp256k1::{Address, PrivateKey, PublicKey, Secp256k1SignerExt, Signer}; +use gsigner::{ + TdecKeyStore, + secp256k1::{Address, PrivateKey, PublicKey, Secp256k1SignerExt, Signer}, +}; use std::{ collections::{BTreeMap, BTreeSet, HashMap}, num::NonZero, @@ -410,10 +414,18 @@ impl Service { ); let signer = Signer::fs(config.node.key_path.clone())?; + let tdec_store = TdecKeyStore::fs(config.node.key_path.clone())?; let validator_pub_key = Self::get_config_public_key(config.node.validator, &signer) .with_context(|| "failed to get validator private key")?; + let validator_tdec_setup = config.tdec.clone().map(|config| ValidatorTdecSetup { + threshold: config.threshold, + dkg_public_key: config.dkg_public_key, + validators_contexts: config.validators_contexts, + key_store: tdec_store, + }); + // TODO #4642: use validator session key let _validator_pub_key_session = Self::get_config_public_key(config.node.validator_session, &signer) @@ -535,7 +547,7 @@ impl Service { db.clone(), signer.clone(), validator_pub_key, - None, + validator_tdec_setup, std::sync::Arc::new(InjectedTxMempool::new(db.clone())), ) .await diff --git a/ethexe/service/src/tests/utils/env.rs b/ethexe/service/src/tests/utils/env.rs index 206f7120679..934f4173f97 100644 --- a/ethexe/service/src/tests/utils/env.rs +++ b/ethexe/service/src/tests/utils/env.rs @@ -209,19 +209,13 @@ fn build_validator_tdec_setups( key_store .import_decryption_key(private_context.validator_decryption_key) .expect("dealer TDEC key must be importable"); - let my_context = public_contexts - .get(private_context.index) - .expect("private context index must reference a public context") - .clone(); ( validator.public_key, ValidatorTdecSetup { - context: ethexe_common::malachite::MalachiteTdecContext { - threshold, - my_context, - contexts: contexts.clone(), - }, + threshold, + dkg_public_key: public_key, + validators_contexts: Some(contexts.clone()), key_store, }, ) @@ -237,11 +231,12 @@ impl TestEnv { self.validator_tdec_setups .get(&validator.public_key) .is_some_and(|setup| { - setup.context.contexts.len() == self.validators.len() + setup.validators_contexts.as_ref().unwrap().len() == self.validators.len() && self.validators.iter().all(|validator| { setup - .context - .contexts + .validators_contexts + .as_ref() + .unwrap() .contains_key(&validator.public_key.to_address()) }) }) @@ -1711,16 +1706,13 @@ fn validator_tdec_setups_create_decryption_shares() { let setup = setups .get(&validator.public_key) .expect("each validator must have a TDEC setup"); - assert_eq!(setup.context.contexts.len(), setups.len()); - assert!( - setup - .context - .contexts - .contains_key(&validator.public_key.to_address()) - ); + let contexts = setup.validators_contexts.clone().unwrap(); + assert_eq!(contexts.len(), setups.len()); + assert!(contexts.contains_key(&validator.public_key.to_address())); + let my_context = contexts.get(&validator.public_key.to_address()).unwrap(); setup .key_store - .create_share(&setup.context.my_context, &ciphertext.header(), b"aad") + .create_share(my_context, &ciphertext.header(), b"aad") .expect("validator must create a decryption share"); } } diff --git a/ethexe/service/tests/smoke.rs b/ethexe/service/tests/smoke.rs index 9c16b4cf27a..f109c6cfda9 100644 --- a/ethexe/service/tests/smoke.rs +++ b/ethexe/service/tests/smoke.rs @@ -97,6 +97,7 @@ async fn constructor() { }, rpc: None, prometheus: None, + tdec: None, }; let service = Service::new(&config).await.unwrap(); From 484f5a18b043e45f22156a062dded4ab9a1b4f7d Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 23 Jun 2026 19:48:43 +0300 Subject: [PATCH 25/41] fix: test restart_resilience --- .../service/tests/restart_resilience.rs | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/ethexe/malachite/service/tests/restart_resilience.rs b/ethexe/malachite/service/tests/restart_resilience.rs index d7c95f0e8ca..8b7199339db 100644 --- a/ethexe/malachite/service/tests/restart_resilience.rs +++ b/ethexe/malachite/service/tests/restart_resilience.rs @@ -16,7 +16,7 @@ //! `globals.latest_finalized_mb_hash` is gap-free across the //! restart boundary, and the latest pointer never rewinds. -use std::{path::Path, sync::Arc, time::Duration}; +use std::{collections::HashMap, num::NonZeroUsize, path::Path, sync::Arc, time::Duration}; use async_trait::async_trait; use ethexe_common::{ @@ -27,10 +27,12 @@ use ethexe_common::{ use ethexe_db::Database; use ethexe_malachite::{ MalachiteConfig, MalachiteEvent, MalachiteService, Mempool, TxInsertionStatus, ValidatorEntry, + ValidatorTdecSetup, }; use futures::StreamExt as _; +use gear_tdec::bls12_381::E as Bls12_381; use gprimitives::H256; -use gsigner::{Signer, schemes::secp256k1::Secp256k1}; +use gsigner::{Signer, TdecKeyStore, schemes::secp256k1::Secp256k1}; /// Test-local no-op mempool. The crate's own [`EmptyMempool`] is not part /// of the public API on purpose — production should never assemble a @@ -114,6 +116,40 @@ fn build_signer(home: &Path) -> (Signer, gsigner::schemes::secp256k1: (signer, pub_key) } +fn build_tdec_setup(pub_key: gsigner::schemes::secp256k1::PublicKey) -> ValidatorTdecSetup { + let dealer = gear_tdec::deal::(1, 1, &mut gear_tdec::rand_utils::test_rng()); + let private_context = dealer + .private_contexts + .into_iter() + .next() + .expect("single-validator dealer output must contain a private context"); + let public_context = private_context + .public_decryption_contexts + .first() + .cloned() + .expect("single-validator dealer output must contain a public context"); + + let key_store = TdecKeyStore::memory(); + key_store + .import_decryption_key(private_context.validator_decryption_key) + .expect("dealer TDEC key must be importable"); + + ValidatorTdecSetup { + threshold: NonZeroUsize::new(1).expect("threshold is non-zero"), + dkg_public_key: dealer.public_key, + validators_contexts: Some(HashMap::from([(pub_key.to_address(), public_context)])), + key_store, + } +} + +fn free_tcp_port() -> u16 { + std::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)) + .expect("bind ephemeral test port") + .local_addr() + .expect("read ephemeral test port") + .port() +} + /// Build the MalachiteConfig used by the resilience tests: /// quarantine-off (so the producer can advance immediately on each /// new chain head), ephemeral listen port, no persistent peers, @@ -217,13 +253,15 @@ async fn single_validator_finalizes_and_recovers_after_restart() { let chain = seed_chain(&db, 64, 0xDEAD_BEEF); let (signer, pub_key) = build_signer(home.path()); + let tdec_setup = build_tdec_setup(pub_key); + let listen_port = free_tcp_port(); // ---- first run ------------------------------------------------- let mut svc = MalachiteService::new( - build_config(home.path(), 0, pub_key), + build_config(home.path(), listen_port, pub_key), db.clone(), signer.clone(), Some(pub_key), - None, + Some(tdec_setup.clone()), Arc::new(EmptyMempool), ) .await @@ -262,11 +300,11 @@ async fn single_validator_finalizes_and_recovers_after_restart() { // ---- second run on the SAME home dir + DB ---------------------- let mut svc2 = MalachiteService::new( - build_config(home.path(), 0, pub_key), + build_config(home.path(), listen_port, pub_key), db.clone(), signer, Some(pub_key), - None, + Some(tdec_setup), Arc::new(EmptyMempool), ) .await From f6cf8a93750f15f6ceb83023ab33b8a13f41f82a Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 23 Jun 2026 20:39:57 +0300 Subject: [PATCH 26/41] chore: implement Tdec storage for shielding key --- ethexe/common/src/db.rs | 14 +++++++ ethexe/db/Cargo.toml | 2 +- ethexe/db/src/database.rs | 52 ++++++++++++++++++++++++- ethexe/malachite/service/src/service.rs | 6 ++- ethexe/rpc/src/apis/injected/relay.rs | 2 +- ethexe/rpc/src/apis/injected/server.rs | 22 +++++++++-- ethexe/service/src/tests/mod.rs | 46 +++++++++++++++++++--- 7 files changed, 130 insertions(+), 14 deletions(-) diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index 7474af1f846..af8ef84079a 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -21,6 +21,8 @@ use gear_core::{ code::{CodeMetadata, InstrumentedCode}, ids::{ActorId, CodeId}, }; +#[cfg(feature = "shielded")] +use gear_tdec::bls12_381::DkgPublicKey; use gprimitives::H256; use gsigner::VerifiedData; use parity_scale_codec::{Decode, Encode}; @@ -146,6 +148,18 @@ pub trait InjectedStorageRW: InjectedStorageRO { fn set_receipt(&self, receipt: &SignedTxReceipt); } +#[cfg(feature = "shielded")] +#[auto_impl::auto_impl(&)] +pub trait TdecStorageRO { + fn shielding_key(&self) -> Option; +} + +#[cfg(feature = "shielded")] +#[auto_impl::auto_impl(&)] +pub trait TdecStorageRW: TdecStorageRO { + fn set_shielding_key(&self, key: DkgPublicKey); +} + /// MB static identity. Keyed by the Blake2b envelope hash; existence implies /// the matching `Operations` blob is in CAS at `operations_hash`. #[derive( diff --git a/ethexe/db/Cargo.toml b/ethexe/db/Cargo.toml index 6b1a34af170..4c3b37ee80f 100644 --- a/ethexe/db/Cargo.toml +++ b/ethexe/db/Cargo.toml @@ -16,6 +16,7 @@ ethexe-ethereum.workspace = true ethexe-runtime-common = { workspace = true, features = ["std"] } gear-core = { workspace = true, features = ["std"] } gprimitives = { workspace = true, features = ["std"] } +gear-tdec.workspace = true gsigner.workspace = true alloy.workspace = true @@ -51,7 +52,6 @@ version = "0.21" scopeguard.workspace = true tempfile.workspace = true ethexe-common = { workspace = true, features = ["mock"] } -gear-tdec.workspace = true indoc.workspace = true scale-info = { workspace = true, features = ["docs"] } sha3.workspace = true diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index 87b5f75da9e..3bff6d43344 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -15,7 +15,7 @@ use ethexe_common::{ BlockMeta, BlockMetaStorageRO, BlockMetaStorageRW, CodesStorageRO, CodesStorageRW, CompactMb, ConfigStorageRO, DBConfig, DBGlobals, GlobalsStorageRO, GlobalsStorageRW, HashStorageRO, InjectedStorageRO, InjectedStorageRW, MbMeta, MbStorageRO, MbStorageRW, - OnChainStorageRO, OnChainStorageRW, + OnChainStorageRO, OnChainStorageRW, TdecStorageRO, TdecStorageRW, }, events::BlockEvent, gear::StateTransition, @@ -35,6 +35,7 @@ use gear_core::{ ids::{ActorId, CodeId, prelude::CodeIdExt as _}, memory::PageBuf, }; +use gear_tdec::bls12_381::DkgPublicKey; use gprimitives::H256; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; @@ -73,6 +74,8 @@ enum Key { ShieldedTransaction(HashOf) = 28, MbUnshieldedTxs(H256) = 29, + + ShieldingKey = 30, } impl Key { @@ -118,7 +121,7 @@ impl Key { bytes.extend(runtime_id.to_le_bytes()); bytes.extend(code_id.as_ref()); } - Self::Globals | Self::Config => { + Self::Globals | Self::Config | Self::ShieldingKey => { // append additional zero bytes to avoid intersection with CAS bytes.extend([0; 8]) } @@ -758,6 +761,25 @@ impl InjectedStorageRW for RawDatabase { } } +impl TdecStorageRO for RawDatabase { + fn shielding_key(&self) -> Option { + self.kv.get(&Key::ShieldingKey.to_bytes()).map(|data| { + String::from_utf8(data) + .expect("Failed to decode shielding key as UTF-8") + .parse() + .expect("Failed to parse DkgPublicKey") + }) + } +} + +impl TdecStorageRW for RawDatabase { + fn set_shielding_key(&self, key: DkgPublicKey) { + tracing::trace!("Set shielding key"); + self.kv + .put(&Key::ShieldingKey.to_bytes(), key.to_string().into_bytes()); + } +} + #[derive(derive_more::Debug, Clone)] #[debug("Database(CAS + KV)")] pub struct Database { @@ -1046,6 +1068,18 @@ impl InjectedStorageRW for Database { }); } +impl TdecStorageRO for Database { + delegate!(to self.raw { + fn shielding_key(&self) -> Option; + }); +} + +impl TdecStorageRW for Database { + delegate!(to self.raw { + fn set_shielding_key(&self, key: DkgPublicKey); + }); +} + impl CodesStorageRO for Database { delegate!(to self.raw { fn original_code_exists(&self, code_id: CodeId) -> bool; @@ -1163,6 +1197,20 @@ mod tests { assert_eq!(db.shielded_transaction(tx_hash), Some(tx)); } + #[test] + fn test_shielding_key() { + let db = Database::memory(); + + let mut rng = gear_tdec::rand_utils::test_rng(); + let dealer_out = gear_tdec::deal::(3, 2, &mut rng); + + assert_eq!(db.shielding_key(), None); + + db.set_shielding_key(dealer_out.public_key); + + assert_eq!(db.shielding_key(), Some(dealer_out.public_key)); + } + #[test] fn test_block_events() { let db = Database::memory(); diff --git a/ethexe/malachite/service/src/service.rs b/ethexe/malachite/service/src/service.rs index b13ad078abb..46e8be6019f 100644 --- a/ethexe/malachite/service/src/service.rs +++ b/ethexe/malachite/service/src/service.rs @@ -27,7 +27,7 @@ use std::{ use anyhow::{Context as _, Result, anyhow, bail}; use ethexe_common::{ Address, SimpleBlockData, - db::{ConfigStorageRO, OnChainStorageRO}, + db::{ConfigStorageRO, OnChainStorageRO, TdecStorageRW}, injected::Transaction, malachite::{MalachiteTdecContext, SignedBlockDecryptionShares}, }; @@ -153,6 +153,10 @@ impl MalachiteService { ), }; + if let Some(setup) = &validator_tdec_setup { + db.set_shielding_key(setup.dkg_public_key.clone()); + } + let (tdec_ctx, tdec_store) = match validator_tdec_setup { Some(setup) if setup.validators_contexts.is_none() && role.is_validator() => { bail!("validator must have other validators decryption contexts") diff --git a/ethexe/rpc/src/apis/injected/relay.rs b/ethexe/rpc/src/apis/injected/relay.rs index 0a81019723c..97c46fab5e1 100644 --- a/ethexe/rpc/src/apis/injected/relay.rs +++ b/ethexe/rpc/src/apis/injected/relay.rs @@ -42,7 +42,7 @@ impl TransactionsRelayer { )); } Transaction::Injected(_) => {} - Transaction::Shielded(_) => todo!("Shielded transaction relay validation"), + Transaction::Shielded(_) => {} } let (response_sender, response_receiver) = oneshot::channel(); diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index 2c434bab22b..c7ee4d9b1ff 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -9,7 +9,7 @@ use super::{ }; use ethexe_common::{ HashOf, - db::InjectedStorageRO, + db::{InjectedStorageRO, TdecStorageRO}, injected::{ InjectedTransaction, InjectedTransactionAcceptance, ShieldedTransaction, SignedInjectedTransaction, SignedTxReceipt, Transaction, @@ -39,8 +39,7 @@ pub struct InjectedApi { #[async_trait] impl InjectedServer for InjectedApi { async fn shielding_key(&self) -> RpcResult> { - // TODO: Implement me - Ok(None) + Ok(self.db.shielding_key()) } async fn send_transaction( @@ -186,7 +185,7 @@ mod tests { use super::*; use ethexe_common::{ Address, PrivateKey, SignedMessage, ValidatorsVec, - db::{GlobalsStorageRO, InjectedStorageRW, OnChainStorageRW, SetGlobals}, + db::{GlobalsStorageRO, InjectedStorageRW, OnChainStorageRW, SetGlobals, TdecStorageRW}, injected::{Promise, Receipt}, mock::Mock, }; @@ -283,6 +282,21 @@ mod tests { assert!(result.is_err()); } + #[tokio::test] + async fn test_shielding_key_returns_stored_key() { + let db = Database::memory(); + let api = make_injected_api(db.clone()); + let dealer = gear_tdec::deal::( + 3, + 2, + &mut gear_tdec::rand_utils::test_rng(), + ); + + db.set_shielding_key(dealer.public_key); + + assert_eq!(api.shielding_key().await.unwrap(), Some(dealer.public_key)); + } + #[tokio::test] async fn test_get_transaction_receipt_returns_stored_receipt() { let db = Database::memory(); diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 37423c7b7d4..8efe24d96c4 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -3059,7 +3059,6 @@ async fn injected_tx_fungible_token_over_network() { stop_nodes([alice_node, bob_node]).await; } -#[ignore = "_"] #[tokio::test] #[ntest::timeout(60_000)] async fn shielded_tx_fungible_token() { @@ -3151,13 +3150,39 @@ async fn shielded_tx_fungible_token() { .shield(&shielding_key, &mut rand::thread_rng()) .unwrap(); let signed_shielded_tx = env.signer.signed_message(pubkey, shielded, None).unwrap(); - let mut subscription = rpc_client + let mut shielded_subscription = rpc_client .send_transaction_and_watch(signed_shielded_tx.into()) .await .unwrap(); - let receipt = subscription.next().await.unwrap().unwrap(); - let promise = receipt.0.into_data().unwrap_promise(); + // Also send another transaction to trigger block creation. + let random_actor = ActorId::new(H256::random().0); + let transfer_amount = 100_000; + let transfer_action = demo_fungible_token::FTAction::Transfer { + from: pubkey.to_address().into(), + to: random_actor, + amount: transfer_amount, + }; + let transfer_tx = InjectedTransaction { + destination: usdt_actor_id, + payload: transfer_action.encode().try_into().unwrap(), + value: 0, + reference_block: node.db.globals().latest_prepared_eb_hash, + salt: vec![1].try_into().unwrap(), + }; + + let signed_transfer_tx = env + .signer + .signed_message(pubkey, transfer_tx.clone(), None) + .unwrap(); + + let mut transfer_subscription = rpc_client + .send_transaction_and_watch(signed_transfer_tx.into()) + .await + .unwrap(); + + let shielded_receipt = shielded_subscription.next().await.unwrap().unwrap(); + let shielded_promise = shielded_receipt.0.into_data().unwrap_promise(); let expected_event = demo_fungible_token::FTEvent::Transfer { from: ActorId::new([0u8; 32]), @@ -3165,7 +3190,18 @@ async fn shielded_tx_fungible_token() { amount, }; - assert_eq!(promise.reply.payload, expected_event.encode()); + assert_eq!(shielded_promise.reply.payload, expected_event.encode()); + + let transfer_receipt = transfer_subscription.next().await.unwrap().unwrap(); + let transfer_promise = transfer_receipt.0.into_data().unwrap_promise(); + + let expected_transfer = demo_fungible_token::FTEvent::Transfer { + from: pubkey.to_address().into(), + to: random_actor, + amount: transfer_amount, + }; + + assert_eq!(transfer_promise.reply.payload, expected_transfer.encode()); } #[tokio::test] From 62bd9c7b82048a32478b4ddd53ce9dc19a3889ff Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 23 Jun 2026 21:05:30 +0300 Subject: [PATCH 27/41] fix: test shielded_transaction_fungible_token & validate/block block above correctly --- ethexe/malachite/service/src/externalities.rs | 178 ++++++++++++------ ethexe/service/src/lib.rs | 5 +- ethexe/service/src/tests/mod.rs | 3 +- 3 files changed, 124 insertions(+), 62 deletions(-) diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index e6dd3243151..87975692050 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -48,11 +48,11 @@ use anyhow::{Result, anyhow, bail}; use async_trait::async_trait; use bytes::Bytes; use ethexe_common::{ - HashOf, MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, + HashOf, MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, VerifiedData, db::{CompactMb, GlobalsStorageRO, GlobalsStorageRW, MbStorageRO, MbStorageRW}, injected::{ - MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, PurgedTransaction, ShieldedTransaction, Transaction, - TransactionHash, TransactionPurgedReason, + InjectedTransaction, MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, PurgedTransaction, + ShieldedTransaction, Transaction, TransactionHash, TransactionPurgedReason, }, malachite::{ MalachiteTdecContext, Operation, Operations, ShieldedTxDecryptionShare, @@ -278,6 +278,31 @@ impl Externalities for EthexeExternalities { self.db .globals_mutate(|g| g.latest_finalized_mb_hash = mb_hash); + // Retain shares belonging to another block + self.decryption_shares.retain_block(mb_hash); + + if let Some(decryption_keys) = operations.iter().find_map(|op| match op { + Operation::DecryptionKeys(keys) => Some(keys.clone()), + _ => None, + }) { + let (unshielded_with_hashes, not_unshielded) = + self.unshield_parent_transactions(compact.parent, &decryption_keys)?; + let unshielded_hash_mapping = unshielded_with_hashes + .iter() + .map(|(tx_hash, tx)| (*tx_hash, tx.data().to_hash())) + .collect(); + let unshielded = unshielded_with_hashes + .into_iter() + .map(|(_, tx)| tx) + .collect(); + self.db.set_mb_unshielded_txs(mb_hash, unshielded); + let _ = self.event_tx.send(Ok(MalachiteEvent::UnshieldingOutput { + mb_hash, + unshielded_hash_mapping, + not_unshielded, + })); + } + let app_cert = CommitCertificate { height: cert.height, mb_hash, @@ -296,50 +321,6 @@ impl Externalities for EthexeExternalities { last_advanced, ); - // Retain shares belonging to another block - self.decryption_shares.retain_block(mb_hash); - - let Some(decryption_keys) = operations.iter().find_map(|op| match op { - Operation::DecryptionKeys(keys) => Some(keys.clone()), - _ => None, - }) else { - // No need to find shielded transaction, because decryption keys wasn't provided. - return Ok(()); - }; - - let mut not_unshielded = Vec::new(); - let mut unshielded = Vec::new(); - let mut unshielded_hash_mapping = Vec::new(); - - for tx in operations.into_iter().filter_map(|op| op.into_shielded()) { - let tx_hash = tx.data().to_hash(); - match decryption_keys.get(&tx_hash) { - Some(shared_key) => { - match tx.into_verified().try_map(|tx| tx.unshield(shared_key)) { - Ok(injected_tx) => { - unshielded_hash_mapping.push((tx_hash, injected_tx.data().to_hash())); - unshielded.push(injected_tx); - } - Err(_err) => { - not_unshielded.push(PurgedTransaction { - tx_hash: TransactionHash::Right(tx_hash), - reason: TransactionPurgedReason::DecryptionFailed, - }); - } - } - } - None => { - // unreachable case, because in `validate_block_above` we check, that all decryption keys was provided - } - } - } - let _ = self.event_tx.send(Ok(MalachiteEvent::UnshieldingOutput { - mb_hash, - unshielded_hash_mapping, - not_unshielded, - })); - self.db.set_mb_unshielded_txs(mb_hash, unshielded); - Ok(()) } @@ -419,6 +400,14 @@ impl Externalities for EthexeExternalities { Some(advanced_eb) => eb_touched_programs(&self.db, parent_advanced, advanced_eb)?, None => std::collections::HashSet::new(), }; + if let Some(keys) = &decryption_keys { + let (unshielded, _) = self.unshield_parent_transactions(parent_mb_hash, keys)?; + touched.extend( + unshielded + .iter() + .map(|(_, injected_tx)| injected_tx.data().destination), + ); + } let initial_touched_count = touched.len(); if initial_touched_count > MAX_TOUCHED_PROGRAMS_PER_MB as usize { // Producer can't shrink this — the EB events themselves @@ -449,17 +438,20 @@ impl Externalities for EthexeExternalities { } let destination = match &tx { - Transaction::Injected(tx) => tx.data().destination, - Transaction::Shielded(_) => todo!("Shielded transaction touched-program cap"), + Transaction::Injected(tx) => Some(tx.data().destination), + Transaction::Shielded(_) => None, }; - if !touched.contains(&destination) - && touched.len() >= MAX_TOUCHED_PROGRAMS_PER_MB as usize - { - // Adding this destination would breach the cap; skip. - continue; + if let Some(destination) = destination { + if !touched.contains(&destination) + && touched.len() >= MAX_TOUCHED_PROGRAMS_PER_MB as usize + { + // Adding this destination would breach the cap; skip. + continue; + } + + touched.insert(destination); } - touched.insert(destination); size_counter += tx_size; capped.push(tx); } @@ -512,7 +504,8 @@ impl Externalities for EthexeExternalities { | Operation::ProgressTasks | Operation::ProcessQueuesV3 { .. } | Operation::Injected(_) - | Operation::Shielded(_) => { + | Operation::Shielded(_) + | Operation::DecryptionKeys(_) => { // Known and allowed. } op => { @@ -524,7 +517,7 @@ impl Externalities for EthexeExternalities { // (1) Shape + ordering. Every honest MB has exactly the form: // - // [AdvanceTillEthereumBlock]? Injected* ProgressTasks ProcessQueuesV3 + // [AdvanceTillEthereumBlock]? [DecryptionKeys]? (Injected|Shielded)* ProgressTasks ProcessQueuesV3 // // This single walk catches: missing bookend, extra bookend, // out-of-order op, more than one Advance, and the @@ -543,7 +536,15 @@ impl Externalities for EthexeExternalities { None }; - while matches!(next, Some(Operation::Injected(_))) { + let decryption_keys = if let Some(Operation::DecryptionKeys(keys)) = next { + let keys = Some(keys); + next = iter.next(); + keys + } else { + None + }; + + while matches!(next, Some(Operation::Injected(_) | Operation::Shielded(_))) { next = iter.next(); } @@ -732,13 +733,21 @@ impl Externalities for EthexeExternalities { Some(advanced_eb) => eb_touched_programs(&self.db, parent_advanced, advanced_eb)?, None => std::collections::HashSet::new(), }; + if let Some(keys) = decryption_keys { + let (unshielded, _) = self.unshield_parent_transactions(parent_hash, keys)?; + touched.extend( + unshielded + .iter() + .map(|(_, injected_tx)| injected_tx.data().destination), + ); + } let limit = touched.len().max(MAX_TOUCHED_PROGRAMS_PER_MB as usize); for op in operations.iter() { match op { Operation::Injected(signed) => { touched.insert(signed.data().destination); } - Operation::Shielded(_shielded) => todo!("implement me"), + Operation::Shielded(_) => {} _ => {} } } @@ -755,6 +764,55 @@ impl Externalities for EthexeExternalities { } impl EthexeExternalities { + fn unshield_parent_transactions( + &self, + parent_mb_hash: H256, + decryption_keys: &BTreeMap, SharedSecret>, + ) -> Result<( + Vec<( + HashOf, + VerifiedData, + )>, + Vec, + )> { + if parent_mb_hash.is_zero() || decryption_keys.is_empty() { + return Ok((Vec::new(), Vec::new())); + } + + let compact = self.db.mb_compact_block(parent_mb_hash).ok_or_else(|| { + anyhow!("unshield_parent_transactions: no CompactMb for parent {parent_mb_hash}") + })?; + let operations = self.db.operations(compact.operations_hash).ok_or_else(|| { + anyhow!( + "unshield_parent_transactions: operations blob {} missing for parent {parent_mb_hash}", + compact.operations_hash + ) + })?; + + let mut not_unshielded = Vec::new(); + let mut unshielded = Vec::new(); + for tx in operations.into_iter().filter_map(Operation::into_shielded) { + let tx_hash = tx.data().to_hash(); + match decryption_keys.get(&tx_hash) { + Some(shared_key) => { + match tx.into_verified().try_map(|tx| tx.unshield(shared_key)) { + Ok(injected_tx) => unshielded.push((tx_hash, injected_tx)), + Err(_err) => not_unshielded.push(PurgedTransaction { + tx_hash: TransactionHash::Right(tx_hash), + reason: TransactionPurgedReason::DecryptionFailed, + }), + } + } + None => not_unshielded.push(PurgedTransaction { + tx_hash: TransactionHash::Right(tx_hash), + reason: TransactionPurgedReason::DecryptionFailed, + }), + } + } + + Ok((unshielded, not_unshielded)) + } + /// True iff `prerequisite.is_zero()` (no prerequisite — genesis /// or pre-advance) or the prerequisite Eth block has been fully /// **prepared** locally. @@ -1063,7 +1121,7 @@ mod utils { pub(crate) fn transaction_to_operation(transaction: Transaction) -> Operation { match transaction { Transaction::Injected(tx) => Operation::Injected(tx), - Transaction::Shielded(_) => todo!("Shielded transaction block inclusion"), + Transaction::Shielded(tx) => Operation::Shielded(tx), } } } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 51012d88082..7adb1d95a91 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -1011,7 +1011,10 @@ impl Service { // promises so they can gossip them; the // service's `PromiseEmissionMode` can still // force the policy to `Enabled` regardless. - compute.compute_mb(mb_hash, ethexe_common::PromisePolicy::Enabled); + + // TODO: fix RPC, now it waits for `UnshieldingOutput` event from malachite, + // but emiting this event costs time. + //compute.compute_mb(mb_hash, ethexe_common::PromisePolicy::Enabled); } MalachiteEvent::BlockFinalized { cert, diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 8efe24d96c4..8edbf1c3dca 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -3060,7 +3060,7 @@ async fn injected_tx_fungible_token_over_network() { } #[tokio::test] -#[ntest::timeout(60_000)] +#[ntest::timeout(30_000)] async fn shielded_tx_fungible_token() { init_logger(); @@ -3202,6 +3202,7 @@ async fn shielded_tx_fungible_token() { }; assert_eq!(transfer_promise.reply.payload, expected_transfer.encode()); + stop_nodes([node]).await; } #[tokio::test] From b3a713257bc68ed10f7eba35d25c5b62865d63c8 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 24 Jun 2026 09:35:50 +0300 Subject: [PATCH 28/41] chore: fix test for shielded txs --- ethexe/service/src/tests/mod.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 8edbf1c3dca..61f6e92ec4d 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -3074,7 +3074,7 @@ async fn shielded_tx_fungible_token() { let mut node = env .new_node( NodeConfig::default() - .service_rpc(8090) + .service_rpc(8097) .validator(env.validators[0]), ) .await; @@ -3149,13 +3149,35 @@ async fn shielded_tx_fungible_token() { let shielded = mint_tx .shield(&shielding_key, &mut rand::thread_rng()) .unwrap(); + let shielded_hash = shielded.to_hash(); let signed_shielded_tx = env.signer.signed_message(pubkey, shielded, None).unwrap(); let mut shielded_subscription = rpc_client .send_transaction_and_watch(signed_shielded_tx.into()) .await .unwrap(); + let mut node_events = node.events(); + + node_events + .find_map_with_db(|db, event| { + let TestingEvent::Malachite(ethexe_malachite::MalachiteEvent::BlockFinalized { + mb_hash, + .. + }) = event + else { + return None; + }; + let compact = db.mb_compact_block(mb_hash)?; + let operations = db.operations(compact.operations_hash)?; + operations + .iter() + .filter_map(|op| op.as_shielded()) + .any(|tx| tx.data().to_hash() == shielded_hash) + .then_some(()) + }) + .await; - // Also send another transaction to trigger block creation. + // Send another transaction after the shielded tx is committed to trigger + // the child block that carries decryption keys and executes it. let random_actor = ActorId::new(H256::random().0); let transfer_amount = 100_000; let transfer_action = demo_fungible_token::FTAction::Transfer { From 1a5c567ad283d7bb3d8e1f798eb26de0bd07ec88 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 24 Jun 2026 09:56:43 +0300 Subject: [PATCH 29/41] chore: allow decryptions keys be a sufficient block content --- ethexe/malachite/service/src/externalities.rs | 134 ++++++++++++++++-- ethexe/service/src/tests/mod.rs | 26 ++-- 2 files changed, 138 insertions(+), 22 deletions(-) diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 87975692050..eadb4fb6550 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -337,12 +337,18 @@ impl Externalities for EthexeExternalities { let decryption_keys = self .wait_for_shielded_tx_decryption_keys(parent_mb_hash) .await?; - let (advance, transactions) = self.wait_for_proposable_content(parent_advanced).await; + let (advance, transactions) = if decryption_keys.is_some() { + // Fast snapshot of proposable content. If no content propose block with decryption keys only. + self.proposable_content_snapshot(parent_advanced).await + } else { + self.wait_for_proposable_content(parent_advanced).await + }; info!( %parent_mb_hash, %parent_advanced, advance = ?advance, + has_decryption_keys = decryption_keys.is_some(), transactions_count = transactions.len(), "build_block_above: proposable content resolved", ); @@ -878,13 +884,9 @@ impl EthexeExternalities { tokio::pin!(chain_head_notified); chain_head_notified.as_mut().enable(); - let advance = self.find_eb_candidate_for_advancing(prev_advanced_eb_hash); - - let head_snapshot = *self.chain_head.read().expect("chain_head poisoned"); - let transactions = match head_snapshot { - Some(head) => self.mempool.fetch(head).await, - None => Vec::new(), - }; + let (advance, transactions) = self + .proposable_content_snapshot(prev_advanced_eb_hash) + .await; if advance.is_some() || !transactions.is_empty() { return (advance, transactions); @@ -898,6 +900,26 @@ impl EthexeExternalities { } } + /// Read currently available producer inputs without waiting for + /// any of them to appear. + /// + /// This function called in [`Self::wait_for_proposable_content`] on each poll + /// iteration, and from [`Self::build_block_above`] when decryption keys are already ready. + async fn proposable_content_snapshot( + &self, + prev_advanced_eb_hash: H256, + ) -> (Option, Vec) { + let advance = self.find_eb_candidate_for_advancing(prev_advanced_eb_hash); + + let head_snapshot = *self.chain_head.read().expect("chain_head poisoned"); + let transactions = match head_snapshot { + Some(head) => self.mempool.fetch(head).await, + None => Vec::new(), + }; + + (advance, transactions) + } + /// Wait until every shielded transaction in the parent has enough verified /// shares, then reconstruct one shared secret per transaction. async fn wait_for_shielded_tx_decryption_keys( @@ -1136,6 +1158,10 @@ mod tests { db::{BlockMetaStorageRW, OnChainStorageRW}, injected::{PurgedTransaction, SignedInjectedTransaction, TransactionRef}, }; + use gear_tdec::{ + bls12_381::{DkgPublicKey, E as Bls12_381}, + rand_utils::test_rng, + }; fn to_payload(bytes: Vec) -> BlockPayload { BlockPayload::try_from(bytes).expect("test payload within size cap") @@ -1188,6 +1214,38 @@ mod tests { (ext, event_rx) } + /// Do threshold decryption setup for a single validator. + fn single_validator_tdec_setup() -> (MalachiteTdecContext, TdecKeyStore, DkgPublicKey) { + let validator_key = ethexe_common::PrivateKey::random(); + let validator_public_key = validator_key.public_key(); + let dealer = gear_tdec::deal::(1, 1, &mut test_rng()); + let private_context = dealer + .private_contexts + .into_iter() + .next() + .expect("single-validator dealer output must contain a private context"); + let public_context = private_context + .public_decryption_contexts + .first() + .cloned() + .expect("single-validator dealer output must contain a public context"); + + let key_store = TdecKeyStore::memory(); + key_store + .import_decryption_key(private_context.validator_decryption_key) + .expect("dealer TDEC key must be importable"); + + ( + MalachiteTdecContext { + threshold: std::num::NonZeroUsize::new(1).expect("threshold is non-zero"), + my_context: public_context.clone(), + contexts: HashMap::from([(validator_public_key.to_address(), public_context)]), + }, + key_store, + dealer.public_key, + ) + } + /// Build an [`Operations`] list for unit tests. /// /// The `salt` byte is encoded as the number of leading @@ -1516,6 +1574,66 @@ mod tests { ); } + #[tokio::test] + async fn build_emits_decryption_keys_without_other_proposable_content() { + use ethexe_common::{SignedMessage, injected::InjectedTransaction}; + use gprimitives::ActorId; + + let db = Database::memory(); + let (mut ext, mut rx) = make_externalities(db); + let (tdec_ctx, tdec_store, dkg_public_key) = single_validator_tdec_setup(); + ext.tdec_ctx = Some(tdec_ctx); + ext.tdec_store = tdec_store; + + let injected = InjectedTransaction { + destination: ActorId::from([1; 32]), + payload: vec![1, 2, 3].try_into().unwrap(), + value: 0, + reference_block: H256::zero(), + salt: vec![7; 32].try_into().unwrap(), + }; + let mut rng = gear_tdec::rand_utils::test_rng(); + let shielded = injected.shield(&dkg_public_key, &mut rng).unwrap(); + let signed_shielded = + SignedMessage::create(ethexe_common::PrivateKey::random(), shielded).unwrap(); + let shielded_hash = signed_shielded.data().to_hash(); + + let parent_payload = Operations::new(vec![ + Operation::Shielded(signed_shielded), + Operation::ProgressTasks, + Operation::ProcessQueuesV3 { gas_allowance: 0 }, + ]); + let parent = Block::new(H256::zero(), 1, to_payload(parent_payload.encode())); + let parent_hash = parent.hash(); + ext.process_mb_proposal(parent_hash, parent).await.unwrap(); + let _ = rx.recv().await; // BlockProposal + let shares_event = rx.recv().await.expect("shares event").expect("ok"); + assert!(matches!( + shares_event, + MalachiteEvent::DecryptionShares { .. } + )); + + let operations = tokio::time::timeout( + std::time::Duration::from_millis(50), + ext.build_operations(parent_hash), + ) + .await + .expect("decryption keys alone must be enough to build a block") + .unwrap(); + + let mut iter = operations.iter(); + let Some(Operation::DecryptionKeys(keys)) = iter.next() else { + panic!("first operation must carry decryption keys"); + }; + assert!(keys.contains_key(&shielded_hash)); + assert!(matches!(iter.next(), Some(Operation::ProgressTasks))); + assert!(matches!( + iter.next(), + Some(Operation::ProcessQueuesV3 { gas_allowance }) if *gas_allowance == ext.gas_allowance + )); + assert!(iter.next().is_none()); + } + /// Stub mempool that records every `forget` argument so the test /// can assert which txs reached the mempool eviction path. #[derive(Default)] diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 61f6e92ec4d..4cb1447efe6 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -3155,8 +3155,8 @@ async fn shielded_tx_fungible_token() { .send_transaction_and_watch(signed_shielded_tx.into()) .await .unwrap(); - let mut node_events = node.events(); + let mut node_events = node.events(); node_events .find_map_with_db(|db, event| { let TestingEvent::Malachite(ethexe_malachite::MalachiteEvent::BlockFinalized { @@ -3176,8 +3176,17 @@ async fn shielded_tx_fungible_token() { }) .await; - // Send another transaction after the shielded tx is committed to trigger - // the child block that carries decryption keys and executes it. + let shielded_receipt = shielded_subscription.next().await.unwrap().unwrap(); + let shielded_promise = shielded_receipt.0.into_data().unwrap_promise(); + + let expected_event = demo_fungible_token::FTEvent::Transfer { + from: ActorId::new([0u8; 32]), + to: pubkey.to_address().into(), + amount, + }; + assert_eq!(shielded_promise.reply.payload, expected_event.encode()); + + // Send transfer transaction. let random_actor = ActorId::new(H256::random().0); let transfer_amount = 100_000; let transfer_action = demo_fungible_token::FTAction::Transfer { @@ -3203,17 +3212,6 @@ async fn shielded_tx_fungible_token() { .await .unwrap(); - let shielded_receipt = shielded_subscription.next().await.unwrap().unwrap(); - let shielded_promise = shielded_receipt.0.into_data().unwrap_promise(); - - let expected_event = demo_fungible_token::FTEvent::Transfer { - from: ActorId::new([0u8; 32]), - to: pubkey.to_address().into(), - amount, - }; - - assert_eq!(shielded_promise.reply.payload, expected_event.encode()); - let transfer_receipt = transfer_subscription.next().await.unwrap().unwrap(); let transfer_promise = transfer_receipt.0.into_data().unwrap_promise(); From 0bd2f801780d77d8c305fb946e26b450255e5bfb Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 24 Jun 2026 12:36:56 +0300 Subject: [PATCH 30/41] fix clippy warnings | remove unused deps --- Cargo.lock | 1 - ethexe/cli/src/params/tdec.rs | 1 + ethexe/common/src/hash.rs | 2 +- ethexe/common/src/injected.rs | 1 + ethexe/common/src/malachite.rs | 1 + ethexe/db/src/database.rs | 1 + ethexe/malachite/service/src/externalities.rs | 42 +++++++++++-------- ethexe/malachite/service/src/service.rs | 2 +- ethexe/malachite/service/src/tx_validity.rs | 33 +++++++-------- ethexe/rpc/Cargo.toml | 1 - ethexe/service/src/lib.rs | 3 +- ethexe/service/src/tests/utils/events.rs | 2 + 12 files changed, 50 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c5371e2e386..92ae6dc035e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5972,7 +5972,6 @@ name = "ethexe-rpc" version = "2.0.0" dependencies = [ "anyhow", - "dashmap 5.5.3", "ethexe-common", "ethexe-db", "ethexe-processor", diff --git a/ethexe/cli/src/params/tdec.rs b/ethexe/cli/src/params/tdec.rs index 330db4676ba..f2b5d970cb6 100644 --- a/ethexe/cli/src/params/tdec.rs +++ b/ethexe/cli/src/params/tdec.rs @@ -1,5 +1,6 @@ // Copyright (C) Gear Technologies Inc. // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + use crate::params::MergeParams; use ethexe_service::config::ThresholdDecryptionCliConfig; use gear_tdec::bls12_381::{ diff --git a/ethexe/common/src/hash.rs b/ethexe/common/src/hash.rs index baa314f83c3..b903a635a66 100644 --- a/ethexe/common/src/hash.rs +++ b/ethexe/common/src/hash.rs @@ -235,7 +235,7 @@ impl ToDigest for EitherHashOf { Self::Left(_) => 0u8, Self::Right(_) => 1u8, }; - hasher.update(&[prefix]); + hasher.update([prefix]); hasher.update(self.inner().as_ref()); } } diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index e43bb3c7bda..2811b506f13 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -525,6 +525,7 @@ impl ShieldedTransaction { #[cfg(feature = "shielded")] #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[derive(Debug, Clone, Encode, Decode, Eq, PartialEq, derive_more::From)] +#[allow(clippy::large_enum_variant)] pub enum Transaction { Injected(SignedInjectedTransaction), Shielded(SignedShieldedTransaction), diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index 3688df3c4e7..3bd9602068b 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -56,6 +56,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, derive_more::IsVariant)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[repr(u32)] +#[allow(clippy::large_enum_variant)] pub enum Operation { /// Pin executor's view to a quarantine-passed Ethereum block. AdvanceTillEthereumBlock { block_hash: H256 } = 0, diff --git a/ethexe/db/src/database.rs b/ethexe/db/src/database.rs index 3bff6d43344..38a43b67053 100644 --- a/ethexe/db/src/database.rs +++ b/ethexe/db/src/database.rs @@ -45,6 +45,7 @@ use std::{ sync::{Arc, RwLock, RwLockReadGuard}, }; +#[allow(clippy::enum_variant_names)] #[repr(u64)] enum Key { BlockSmallData(H256) = 0, diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index eadb4fb6550..7f9fc952a78 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -136,6 +136,15 @@ pub(crate) struct PendingEvent { pub prerequisite: H256, } +#[derive(Clone, Default)] +struct UnshieldingOutput { + pub unshielded: Vec<( + HashOf, + VerifiedData, + )>, + pub not_unshielded: Vec, +} + #[async_trait] impl Externalities for EthexeExternalities { async fn process_mb_proposal(&self, mb_hash: H256, mb: Block) -> Result<()> { @@ -285,8 +294,10 @@ impl Externalities for EthexeExternalities { Operation::DecryptionKeys(keys) => Some(keys.clone()), _ => None, }) { - let (unshielded_with_hashes, not_unshielded) = - self.unshield_parent_transactions(compact.parent, &decryption_keys)?; + let UnshieldingOutput { + unshielded: unshielded_with_hashes, + not_unshielded, + } = self.unshield_parent_transactions(compact.parent, &decryption_keys)?; let unshielded_hash_mapping = unshielded_with_hashes .iter() .map(|(tx_hash, tx)| (*tx_hash, tx.data().to_hash())) @@ -407,7 +418,8 @@ impl Externalities for EthexeExternalities { None => std::collections::HashSet::new(), }; if let Some(keys) = &decryption_keys { - let (unshielded, _) = self.unshield_parent_transactions(parent_mb_hash, keys)?; + let UnshieldingOutput { unshielded, .. } = + self.unshield_parent_transactions(parent_mb_hash, keys)?; touched.extend( unshielded .iter() @@ -740,7 +752,8 @@ impl Externalities for EthexeExternalities { None => std::collections::HashSet::new(), }; if let Some(keys) = decryption_keys { - let (unshielded, _) = self.unshield_parent_transactions(parent_hash, keys)?; + let UnshieldingOutput { unshielded, .. } = + self.unshield_parent_transactions(parent_hash, keys)?; touched.extend( unshielded .iter() @@ -774,15 +787,9 @@ impl EthexeExternalities { &self, parent_mb_hash: H256, decryption_keys: &BTreeMap, SharedSecret>, - ) -> Result<( - Vec<( - HashOf, - VerifiedData, - )>, - Vec, - )> { + ) -> Result { if parent_mb_hash.is_zero() || decryption_keys.is_empty() { - return Ok((Vec::new(), Vec::new())); + return Ok(UnshieldingOutput::default()); } let compact = self.db.mb_compact_block(parent_mb_hash).ok_or_else(|| { @@ -795,28 +802,27 @@ impl EthexeExternalities { ) })?; - let mut not_unshielded = Vec::new(); - let mut unshielded = Vec::new(); + let mut output = UnshieldingOutput::default(); for tx in operations.into_iter().filter_map(Operation::into_shielded) { let tx_hash = tx.data().to_hash(); match decryption_keys.get(&tx_hash) { Some(shared_key) => { match tx.into_verified().try_map(|tx| tx.unshield(shared_key)) { - Ok(injected_tx) => unshielded.push((tx_hash, injected_tx)), - Err(_err) => not_unshielded.push(PurgedTransaction { + Ok(injected_tx) => output.unshielded.push((tx_hash, injected_tx)), + Err(_err) => output.not_unshielded.push(PurgedTransaction { tx_hash: TransactionHash::Right(tx_hash), reason: TransactionPurgedReason::DecryptionFailed, }), } } - None => not_unshielded.push(PurgedTransaction { + None => output.not_unshielded.push(PurgedTransaction { tx_hash: TransactionHash::Right(tx_hash), reason: TransactionPurgedReason::DecryptionFailed, }), } } - Ok((unshielded, not_unshielded)) + Ok(output) } /// True iff `prerequisite.is_zero()` (no prerequisite — genesis diff --git a/ethexe/malachite/service/src/service.rs b/ethexe/malachite/service/src/service.rs index 46e8be6019f..70af1fa547a 100644 --- a/ethexe/malachite/service/src/service.rs +++ b/ethexe/malachite/service/src/service.rs @@ -154,7 +154,7 @@ impl MalachiteService { }; if let Some(setup) = &validator_tdec_setup { - db.set_shielding_key(setup.dkg_public_key.clone()); + db.set_shielding_key(setup.dkg_public_key); } let (tdec_ctx, tdec_store) = match validator_tdec_setup { diff --git a/ethexe/malachite/service/src/tx_validity.rs b/ethexe/malachite/service/src/tx_validity.rs index baa7e479ced..21824f2ad06 100644 --- a/ethexe/malachite/service/src/tx_validity.rs +++ b/ethexe/malachite/service/src/tx_validity.rs @@ -86,11 +86,16 @@ pub struct TxValidityChecker { db: Database, chain_head: SimpleBlockData, start_block_hash: H256, - recent_included_injected_txs: HashSet>, - recent_included_shielded_txs: HashSet>, + recent_included_txs: RecentlyIncludedTransactions, latest_states: ProgramStates, } +#[derive(Clone, Default)] +pub struct RecentlyIncludedTransactions { + pub injected: HashSet>, + pub shielded: HashSet>, +} + impl TxValidityChecker { /// Build a checker for an MB whose parent on the consensus chain is /// `parent_mb_hash`. Genesis maps `parent_mb_hash == H256::zero()`; the @@ -121,16 +126,14 @@ impl TxValidityChecker { anyhow!("MB {cursor} marked computed but has no program_states row — DB invariant") })?; - let (recent_included_injected_txs, recent_included_shielded_txs) = - Self::collect_recent_included_txs(&db, parent_mb_hash)?; + let recent_included_txs = Self::collect_recent_included_txs(&db, parent_mb_hash)?; let start_block_hash = db.globals().start_block_hash; Ok(Self { db, chain_head, start_block_hash, - recent_included_injected_txs, - recent_included_shielded_txs, + recent_included_txs, latest_states, }) } @@ -159,7 +162,7 @@ impl TxValidityChecker { } let tx_hash = tx.data().to_hash(); - if self.recent_included_injected_txs.contains(&tx_hash) { + if self.recent_included_txs.injected.contains(&tx_hash) { return Ok(TxValidity::Duplicate); } @@ -198,7 +201,7 @@ impl TxValidityChecker { } let tx_hash = tx.data().to_hash(); - if self.recent_included_shielded_txs.contains(&tx_hash) { + if self.recent_included_txs.shielded.contains(&tx_hash) { return Ok(TxValidity::Duplicate); } @@ -253,12 +256,8 @@ impl TxValidityChecker { pub fn collect_recent_included_txs( db: &Database, parent_mb: H256, - ) -> Result<( - HashSet>, - HashSet>, - )> { - let mut injected_txs = HashSet::new(); - let mut shielded_txs = HashSet::new(); + ) -> Result { + let mut recent_included = RecentlyIncludedTransactions::default(); let mut mb_hash = parent_mb; for _ in 0..VALIDITY_WINDOW { @@ -277,17 +276,17 @@ impl TxValidityChecker { for op in operations.into_iter() { match op { Operation::Injected(signed) => { - injected_txs.insert(signed.data().to_hash()); + recent_included.injected.insert(signed.data().to_hash()); } Operation::Shielded(signed) => { - shielded_txs.insert(signed.data().to_hash()); + recent_included.shielded.insert(signed.data().to_hash()); } _ => {} } } mb_hash = cb.parent; } - Ok((injected_txs, shielded_txs)) + Ok(recent_included) } } diff --git a/ethexe/rpc/Cargo.toml b/ethexe/rpc/Cargo.toml index ef6e1d2b32d..7939e433ce5 100644 --- a/ethexe/rpc/Cargo.toml +++ b/ethexe/rpc/Cargo.toml @@ -28,7 +28,6 @@ sp-core = { workspace = true, features = ["serde"] } gear-core = { workspace = true, features = ["std"] } serde = { workspace = true, features = ["std"] } tracing.workspace = true -dashmap.workspace = true metrics.workspace = true metrics-derive.workspace = true gear-workspace-hack.workspace = true diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 7adb1d95a91..7e62361a4ee 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -95,6 +95,7 @@ mod pending_tx; mod tests; #[derive(Debug, derive_more::From)] +#[allow(clippy::large_enum_variant)] pub enum Event { Compute(ComputeEvent), Consensus(ConsensusEvent), @@ -1013,7 +1014,7 @@ impl Service { // force the policy to `Enabled` regardless. // TODO: fix RPC, now it waits for `UnshieldingOutput` event from malachite, - // but emiting this event costs time. + // but emitting this event costs time. //compute.compute_mb(mb_hash, ethexe_common::PromisePolicy::Enabled); } MalachiteEvent::BlockFinalized { diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index a72706a23ac..fb5b72a7420 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -42,6 +42,7 @@ pub type TestingEventReceiver = KickingStream>; pub type ObserverEventSender = EventSender; pub type ObserverEventReceiver = KickingStream>; +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, Eq, PartialEq)] pub enum TestingNetworkInjectedEvent { InboundTransaction { @@ -74,6 +75,7 @@ impl TestingNetworkInjectedEvent { } } +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, Eq, PartialEq)] pub enum TestingNetworkEvent { ValidatorMessage(VerifiedValidatorMessage), From 9f9d4ffb984acbd3204c439d48e868a6353e34f9 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 24 Jun 2026 13:18:24 +0300 Subject: [PATCH 31/41] fix: speculative execution when shielded exists --- ethexe/common/src/db.rs | 3 +- ethexe/common/src/mock.rs | 8 +- ethexe/malachite/service/src/externalities.rs | 74 +++++++++++++++++++ ethexe/malachite/service/src/lib.rs | 18 ++++- ethexe/service/src/lib.rs | 13 +++- 5 files changed, 107 insertions(+), 9 deletions(-) diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index af8ef84079a..56b136ec46d 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -179,6 +179,7 @@ pub struct CompactMb { pub struct MbMeta { pub computed: bool, pub last_advanced_eb: H256, + pub contains_shielded: bool, } #[auto_impl::auto_impl(&, Box)] @@ -298,7 +299,7 @@ mod tests { #[test] fn ensure_types_unchanged() { const EXPECTED_TYPE_INFO_HASH: &str = - "c543e8c3d27f17bd77d510ce3f1d2b3a286b6444559444eb78807b3c2fd9ffbf"; + "6a9d4140086d241dd267bc95b0f70e5114721fec1a2071c46dd967c8881eff9c"; let types = [ meta_type::(), diff --git a/ethexe/common/src/mock.rs b/ethexe/common/src/mock.rs index 183ed86010c..86db2cc6728 100644 --- a/ethexe/common/src/mock.rs +++ b/ethexe/common/src/mock.rs @@ -12,7 +12,7 @@ use crate::{ BatchCommitment, ChainCommitment, CodeCommitment, Message, MessageType, StateTransition, }, injected::{InjectedTransaction, Promise}, - malachite::Operations, + malachite::{Operation, Operations}, }; use alloc::{collections::BTreeMap, vec}; use gear_core::{ @@ -653,6 +653,12 @@ impl BlockChain { operations_hash, }, ); + db.mutate_mb_meta(mb.hash, |meta| { + meta.contains_shielded = mb + .operations + .iter() + .any(|op| matches!(op, Operation::Shielded(_))); + }); if let Some(computed) = &mb.computed { db.set_mb_program_states(mb.hash, computed.program_states.clone()); db.mutate_mb_meta(mb.hash, |meta| { diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 7f9fc952a78..6e87f0f1311 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -182,6 +182,9 @@ impl Externalities for EthexeExternalities { // CompactMb exists, operations are reachable" — holds // unconditionally. let operations_hash = self.db.set_operations(operations.clone()); + let contains_shielded = operations + .iter() + .any(|op| matches!(op, Operation::Shielded(_))); self.db.set_mb_compact_block( mb_hash, CompactMb { @@ -192,6 +195,7 @@ impl Externalities for EthexeExternalities { ); self.db.mutate_mb_meta(mb_hash, |meta| { meta.last_advanced_eb = last_advanced; + meta.contains_shielded = contains_shielded; }); let shielded_transactions = operations @@ -201,10 +205,12 @@ impl Externalities for EthexeExternalities { self.decryption_shares .register_block(mb_hash, shielded_transactions.iter().map(|tx| tx.to_hash())); + let can_speculatively_execute = self.can_speculatively_execute(parent)?; self.try_emit_or_queue( MalachiteEvent::BlockProposal { height: mb.height, mb_hash, + can_speculatively_execute, }, last_advanced, ); @@ -783,6 +789,17 @@ impl Externalities for EthexeExternalities { } impl EthexeExternalities { + fn can_speculatively_execute(&self, parent_mb_hash: H256) -> Result { + if parent_mb_hash.is_zero() { + return Ok(true); + } + + self.db.mb_compact_block(parent_mb_hash).ok_or_else(|| { + anyhow!("can_speculatively_execute: no CompactMb for parent {parent_mb_hash}") + })?; + Ok(!self.db.mb_meta(parent_mb_hash).contains_shielded) + } + fn unshield_parent_transactions( &self, parent_mb_hash: H256, @@ -1286,6 +1303,27 @@ mod tests { } } + fn shielded_operation() -> Operation { + use ethexe_common::{SignedMessage, injected::InjectedTransaction}; + use gprimitives::ActorId; + + let dkg_public_key = gear_tdec::deal::(1, 1, &mut test_rng()).public_key; + let injected = InjectedTransaction { + destination: ActorId::from([1; 32]), + payload: vec![1, 2, 3].try_into().unwrap(), + value: 0, + reference_block: H256::zero(), + salt: vec![7; 32].try_into().unwrap(), + }; + let shielded = injected + .shield(&dkg_public_key, &mut test_rng()) + .expect("test shielding must succeed"); + Operation::Shielded( + SignedMessage::create(ethexe_common::PrivateKey::random(), shielded) + .expect("test signature must be valid"), + ) + } + /// `process_mb_proposal` populates `mb_block`, `mb_meta` (height, /// parent_mb_hash, last_advanced_eb, synced=true) and the /// height index, then emits a `BlockProposal`. @@ -1311,9 +1349,11 @@ mod tests { MalachiteEvent::BlockProposal { height, mb_hash: proposed, + can_speculatively_execute, } => { assert_eq!(height, 1); assert_eq!(proposed, mb_hash); + assert!(can_speculatively_execute); let _ = p; } other => panic!("expected BlockProposal, got {other:?}"), @@ -1323,6 +1363,40 @@ mod tests { assert!(db.globals().latest_finalized_mb_hash.is_zero()); } + #[tokio::test] + async fn block_proposal_for_parent_with_shielded_tx_disables_speculative_execution() { + let db = Database::memory(); + let (ext, mut rx) = make_externalities(db); + + let parent_payload = Operations::new(vec![ + shielded_operation(), + Operation::ProgressTasks, + Operation::ProcessQueuesV3 { gas_allowance: 0 }, + ]); + let parent = wrap(parent_payload, 1, H256::zero()); + let parent_hash = parent.hash(); + ext.process_mb_proposal(parent_hash, parent).await.unwrap(); + let _ = rx.recv().await.expect("parent proposal").expect("ok"); + + let child_payload = payload(None, 2); + let child = wrap(child_payload, 2, parent_hash); + let child_hash = child.hash(); + ext.process_mb_proposal(child_hash, child).await.unwrap(); + + match rx.try_recv().expect("child event").expect("ok") { + MalachiteEvent::BlockProposal { + height, + mb_hash, + can_speculatively_execute, + } => { + assert_eq!(height, 2); + assert_eq!(mb_hash, child_hash); + assert!(!can_speculatively_execute); + } + other => panic!("expected BlockProposal, got {other:?}"), + } + } + /// `process_mb_finalized` reads the [`CompactMb`] + /// operations blob keyed by the consensus envelope hash, /// advances `globals.latest_finalized_mb_hash`, and emits a diff --git a/ethexe/malachite/service/src/lib.rs b/ethexe/malachite/service/src/lib.rs index 7ed2bb5af4b..b4d978ff61f 100644 --- a/ethexe/malachite/service/src/lib.rs +++ b/ethexe/malachite/service/src/lib.rs @@ -88,7 +88,12 @@ pub struct CommitCertificate { #[derive(Debug, Clone, PartialEq, Eq)] pub enum MalachiteEvent { /// New sequencer block persisted; `mb_hash` is the Blake2b envelope hash. - BlockProposal { height: u64, mb_hash: H256 }, + BlockProposal { + height: u64, + mb_hash: H256, + /// Whether this MB can be executed before finalization. + can_speculatively_execute: bool, + }, /// BFT-committed block; `globals.latest_finalized_mb_hash` now points at it. BlockFinalized { @@ -122,8 +127,15 @@ pub enum MalachiteEvent { impl std::fmt::Display for MalachiteEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::BlockProposal { height, mb_hash } => { - write!(f, "BlockProposal(height: {height}, mb_hash: {mb_hash})") + Self::BlockProposal { + height, + mb_hash, + can_speculatively_execute, + } => { + write!( + f, + "BlockProposal(height: {height}, mb_hash: {mb_hash}, can_speculatively_execute: {can_speculatively_execute})" + ) } Self::BlockFinalized { cert, diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 7e62361a4ee..cbea4aa6809 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -1002,10 +1002,15 @@ impl Service { } }, Event::Malachite(event) => match event { - MalachiteEvent::BlockProposal { height, mb_hash } => { + MalachiteEvent::BlockProposal { + height, + mb_hash, + can_speculatively_execute, + } => { tracing::info!( height, mb_hash = %mb_hash, + can_speculatively_execute, "Malachite: BlockProposal", ); // Validators are interested in this MB's @@ -1013,9 +1018,9 @@ impl Service { // service's `PromiseEmissionMode` can still // force the policy to `Enabled` regardless. - // TODO: fix RPC, now it waits for `UnshieldingOutput` event from malachite, - // but emitting this event costs time. - //compute.compute_mb(mb_hash, ethexe_common::PromisePolicy::Enabled); + if can_speculatively_execute { + compute.compute_mb(mb_hash, ethexe_common::PromisePolicy::Enabled); + } } MalachiteEvent::BlockFinalized { cert, From 341a1bd33e18c631f694c4218b02bd7e983bbebc Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 24 Jun 2026 15:09:34 +0300 Subject: [PATCH 32/41] fix clippy for get-builtins --- sdk/gtest/src/builtins/bls12_381.rs | 2 +- vara/pallets/gear-builtin/src/benchmarking.rs | 2 +- vara/pallets/gear-builtin/src/tests/bls381.rs | 2 +- vara/sdk/gsdk/tests/builtin_bls381.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/gtest/src/builtins/bls12_381.rs b/sdk/gtest/src/builtins/bls12_381.rs index 180962f5186..a0f6e3e36fc 100644 --- a/sdk/gtest/src/builtins/bls12_381.rs +++ b/sdk/gtest/src/builtins/bls12_381.rs @@ -63,7 +63,7 @@ mod tests { G2Projective as G2, }; use ark_ec::{ - Group, ScalarMul, VariableBaseMSM, + PrimeGroup as Group, ScalarMul, VariableBaseMSM, bls12::Bls12Config as Bls12ConfigTrait, hashing::{HashToCurve, curve_maps::wb, map_to_curve_hasher::MapToCurveBasedHasher}, pairing::Pairing, diff --git a/vara/pallets/gear-builtin/src/benchmarking.rs b/vara/pallets/gear-builtin/src/benchmarking.rs index 5c31c583d44..78d05deb409 100644 --- a/vara/pallets/gear-builtin/src/benchmarking.rs +++ b/vara/pallets/gear-builtin/src/benchmarking.rs @@ -9,7 +9,7 @@ use crate::*; use ark_std::{UniformRand, ops::Mul}; use builtins_common::bls12_381::{ ark_bls12_381::{self, Bls12_381, G1Affine, G1Projective as G1, G2Affine, G2Projective as G2}, - ark_ec::{Group, ScalarMul, pairing::Pairing, short_weierstrass::SWCurveConfig}, + ark_ec::{PrimeGroup as Group, ScalarMul, pairing::Pairing, short_weierstrass::SWCurveConfig}, ark_ff::biginteger::BigInt, ark_scale::{self, hazmat::ArkScaleProjective}, }; diff --git a/vara/pallets/gear-builtin/src/tests/bls381.rs b/vara/pallets/gear-builtin/src/tests/bls381.rs index fd50683d141..ef57cba86fd 100644 --- a/vara/pallets/gear-builtin/src/tests/bls381.rs +++ b/vara/pallets/gear-builtin/src/tests/bls381.rs @@ -7,7 +7,7 @@ use builtins_common::bls12_381::{ Request, Response, ark_bls12_381::{self, Bls12_381, G1Affine, G1Projective as G1, G2Affine, G2Projective as G2}, ark_ec::{ - Group, ScalarMul, VariableBaseMSM, + PrimeGroup as Group, ScalarMul, VariableBaseMSM, bls12::Bls12Config, hashing::{HashToCurve, curve_maps::wb, map_to_curve_hasher::MapToCurveBasedHasher}, pairing::Pairing, diff --git a/vara/sdk/gsdk/tests/builtin_bls381.rs b/vara/sdk/gsdk/tests/builtin_bls381.rs index b09bbfdfc86..29ea6b4b9d7 100644 --- a/vara/sdk/gsdk/tests/builtin_bls381.rs +++ b/vara/sdk/gsdk/tests/builtin_bls381.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 use ark_bls12_381::{G1Affine, G1Projective as G1, G2Affine, G2Projective as G2}; -use ark_ec::Group; +use ark_ec::PrimeGroup as Group; use ark_serialize::CanonicalSerialize; use ark_std::{UniformRand, ops::Mul}; use demo_bls381::*; From 7b6f3c3ea8e291bfa7049d5a739aa8e0fe6aece4 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 24 Jun 2026 15:34:15 +0300 Subject: [PATCH 33/41] chore: add complex test for shielded transactions --- ethexe/service/src/tests/mod.rs | 276 ++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 4cb1447efe6..7dddeaf3a4b 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -3225,6 +3225,282 @@ async fn shielded_tx_fungible_token() { stop_nodes([node]).await; } +#[tokio::test] +#[ntest::timeout(120_000)] +async fn shielded_tx_threshold_network_batch() { + init_logger(); + + // #1. Start a three-validator network so shielded transactions require threshold decryption. + let env_config = TestEnvConfig { + validators: ValidatorsConfig::PreDefined(3), + network: EnvNetworkConfig::Enabled, + ..Default::default() + }; + let mut env = TestEnv::new(env_config).await.unwrap(); + assert_eq!(env.threshold, 2); + + // #2. Run all validators, exposing RPC on Alice for user-submitted shielded txs. + let user_pubkey = env.signer.generate().unwrap(); + let validator_keys = env.validators.clone(); + let mut alice = env + .new_node( + NodeConfig::named("Alice") + .service_rpc(8098) + .validator(validator_keys[0]), + ) + .await; + alice.start_service().await; + let rpc_client = alice + .rpc_ws_client() + .await + .expect("RPC client provided by node"); + let mut bob = env + .new_node(NodeConfig::named("Bob").validator(validator_keys[1])) + .await; + bob.start_service().await; + let mut charlie = env + .new_node(NodeConfig::named("Charlie").validator(validator_keys[2])) + .await; + charlie.start_service().await; + + // #3. Deploy and initialize the fungible-token program through Ethereum events. + let token_config = demo_fungible_token::InitConfig { + name: "USD Tether".to_string(), + symbol: "USDT".to_string(), + decimals: 10, + initial_capacity: None, + }; + + let res = env + .upload_code(demo_fungible_token::WASM_BINARY) + .await + .unwrap() + .wait_for() + .await + .unwrap(); + let res = env + .create_program(res.code_id, 500_000_000_000_000) + .await + .unwrap() + .wait_for() + .await + .unwrap(); + let token_actor_id = res.program_id; + + let init_reply = env + .send_message(token_actor_id, &token_config.encode()) + .await + .unwrap() + .wait_for() + .await + .unwrap(); + assert_eq!( + init_reply.code, + ReplyCode::Success(SuccessReplyReason::Auto) + ); + + // #4. Fetch the public shielding key published by the validator service. + let shielding_key = rpc_client + .shielding_key() + .await + .unwrap() + .expect("validator RPC exposes threshold-decryption public key"); + assert_eq!(shielding_key, env.tdec_public_key); + + let sender = user_pubkey.to_address().into(); + let mint_amount: u128 = 5_000_000_000; + let bonus_mint_amount: u128 = 125_000; + let reference_block = alice.db.globals().latest_prepared_eb_hash; + + // #5. Build two independent token mints, then shield and sign both transactions. + let mint_tx = InjectedTransaction { + destination: token_actor_id, + payload: demo_fungible_token::FTAction::Mint(mint_amount) + .encode() + .try_into() + .unwrap(), + value: 0, + reference_block, + salt: b"shielded-batch-mint".to_vec().try_into().unwrap(), + }; + let bonus_mint_tx = InjectedTransaction { + destination: token_actor_id, + payload: demo_fungible_token::FTAction::Mint(bonus_mint_amount) + .encode() + .try_into() + .unwrap(), + value: 0, + reference_block, + salt: b"shielded-batch-bonus".to_vec().try_into().unwrap(), + }; + + let mint_hash = mint_tx.to_hash(); + let bonus_mint_hash = bonus_mint_tx.to_hash(); + let shielded_mint = mint_tx + .shield(&shielding_key, &mut rand::thread_rng()) + .unwrap(); + let shielded_bonus_mint = bonus_mint_tx + .shield(&shielding_key, &mut rand::thread_rng()) + .unwrap(); + let shielded_mint_hash = shielded_mint.to_hash(); + let shielded_bonus_mint_hash = shielded_bonus_mint.to_hash(); + + let signed_mint = env + .signer + .signed_message(user_pubkey, shielded_mint, None) + .unwrap(); + let signed_bonus_mint = env + .signer + .signed_message(user_pubkey, shielded_bonus_mint, None) + .unwrap(); + + // #6. Submit both shielded transactions through RPC and keep receipt subscriptions open. + let mut mint_subscription = rpc_client + .send_transaction_and_watch(signed_mint.into()) + .await + .expect("successfully subscribe for shielded mint"); + let mut bonus_mint_subscription = rpc_client + .send_transaction_and_watch(signed_bonus_mint.into()) + .await + .expect("successfully subscribe for second shielded mint"); + + // #7. Wait until one finalized MB includes both shielded transactions. + let mut alice_events = alice.events(); + let shielded_mb_hash = alice_events + .find_map_with_db(|db, event| { + let TestingEvent::Malachite(ethexe_malachite::MalachiteEvent::BlockFinalized { + mb_hash, + .. + }) = event + else { + return None; + }; + + let compact = db.mb_compact_block(mb_hash)?; + let operations = db.operations(compact.operations_hash)?; + let mut has_mint = false; + let mut has_bonus_mint = false; + for op in operations.iter().filter_map(|op| op.as_shielded()) { + has_mint |= op.data().to_hash() == shielded_mint_hash; + has_bonus_mint |= op.data().to_hash() == shielded_bonus_mint_hash; + } + + (has_mint && has_bonus_mint).then_some(mb_hash) + }) + .await; + + // #8. Wait for a later finalized MB to carry decryption keys and unshielded txs. + let decryption_keys_mb_hash = alice_events + .find_map_with_db(|db, event| { + let TestingEvent::Malachite(ethexe_malachite::MalachiteEvent::BlockFinalized { + mb_hash, + .. + }) = event + else { + return None; + }; + + let compact = db.mb_compact_block(mb_hash)?; + let operations = db.operations(compact.operations_hash)?; + let has_keys = operations.iter().any(|op| { + matches!( + op, + ethexe_common::malachite::Operation::DecryptionKeys(keys) + if keys.contains_key(&shielded_mint_hash) + && keys.contains_key(&shielded_bonus_mint_hash) + ) + }); + if !has_keys { + return None; + } + + let unshielded = db.mb_unshielded_txs(mb_hash); + let unshielded_hashes = unshielded + .iter() + .map(|tx| tx.data().to_hash()) + .collect::>(); + assert!(unshielded_hashes.contains(&mint_hash)); + assert!(unshielded_hashes.contains(&bonus_mint_hash)); + + Some(mb_hash) + }) + .await; + assert_ne!(shielded_mb_hash, decryption_keys_mb_hash); + + // #9. Check that both RPC subscriptions resolve under their unshielded hashes. + let mint_receipt = mint_subscription + .next() + .await + .expect("mint subscription produces receipt") + .expect("shielded mint succeeds"); + let mint_promise = mint_receipt.data().clone().unwrap_promise(); + assert_eq!( + mint_receipt.data().tx_hash(), + TransactionHash::Left(mint_hash) + ); + assert_eq!(mint_promise.tx_hash, mint_hash); + assert_eq!( + mint_promise.reply.code, + ReplyCode::Success(SuccessReplyReason::Manual) + ); + assert_eq!( + mint_promise.reply.payload, + demo_fungible_token::FTEvent::Transfer { + from: ActorId::new([0u8; 32]), + to: sender, + amount: mint_amount, + } + .encode() + ); + + let bonus_mint_receipt = bonus_mint_subscription + .next() + .await + .expect("second mint subscription produces receipt") + .expect("second shielded mint succeeds"); + let bonus_mint_promise = bonus_mint_receipt.data().clone().unwrap_promise(); + assert_eq!( + bonus_mint_receipt.data().tx_hash(), + TransactionHash::Left(bonus_mint_hash) + ); + assert_eq!(bonus_mint_promise.tx_hash, bonus_mint_hash); + assert_eq!( + bonus_mint_promise.reply.code, + ReplyCode::Success(SuccessReplyReason::Manual) + ); + assert_eq!( + bonus_mint_promise.reply.payload, + demo_fungible_token::FTEvent::Transfer { + from: ActorId::new([0u8; 32]), + to: sender, + amount: bonus_mint_amount, + } + .encode() + ); + + // #10. Verify computed promises and full receipts are persisted and queryable. + assert!(alice.db.promise(mint_hash).is_some()); + assert!(alice.db.promise(bonus_mint_hash).is_some()); + assert!(alice.db.receipt(mint_hash).is_some()); + assert!(alice.db.receipt(bonus_mint_hash).is_some()); + assert!( + rpc_client + .get_transaction_receipt(mint_hash) + .await + .unwrap() + .is_some() + ); + assert!( + rpc_client + .get_transaction_receipt(bonus_mint_hash) + .await + .unwrap() + .is_some() + ); + + stop_nodes([alice, bob, charlie]).await; +} + #[tokio::test] #[ntest::timeout(120_000)] async fn whole_network_restore() { From 827301a5eaf284bb226e85ccda3cc1fb88086d0f Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 24 Jun 2026 16:15:05 +0300 Subject: [PATCH 34/41] chore: rename InjectedTransactionAcceptance -> TransactionAcceptance --- ethexe/common/src/injected.rs | 4 +-- ethexe/malachite/service/src/mempool.rs | 18 +++++------ ethexe/network/src/injected.rs | 28 ++++++++--------- ethexe/rpc/src/apis/injected/relay.rs | 4 +-- ethexe/rpc/src/apis/injected/server.rs | 10 +++--- ethexe/rpc/src/apis/injected/trait.rs | 4 +-- ethexe/rpc/src/lib.rs | 4 +-- ethexe/rpc/src/tests.rs | 4 +-- ethexe/sdk/src/mirror.rs | 8 ++--- ethexe/service/src/lib.rs | 12 +++---- ethexe/service/src/pending_tx.rs | 40 ++++++++++++------------ ethexe/service/src/tests/mod.rs | 4 +-- ethexe/service/src/tests/utils/events.rs | 4 +-- 13 files changed, 72 insertions(+), 72 deletions(-) diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index 2811b506f13..9c33a1785f3 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -44,12 +44,12 @@ pub const MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB: usize = 127 * 1024; // TODO: rename this type to just `TransactionAcceptance` #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] -pub enum InjectedTransactionAcceptance { +pub enum TransactionAcceptance { Accept, Reject { reason: String }, } -impl From> for InjectedTransactionAcceptance { +impl From> for TransactionAcceptance { fn from(value: Result<(), E>) -> Self { match value { Ok(()) => Self::Accept, diff --git a/ethexe/malachite/service/src/mempool.rs b/ethexe/malachite/service/src/mempool.rs index ba942a26a7d..38b93dd41eb 100644 --- a/ethexe/malachite/service/src/mempool.rs +++ b/ethexe/malachite/service/src/mempool.rs @@ -40,7 +40,7 @@ use ethexe_common::{ HashOf, SimpleBlockData, db::{GlobalsStorageRO, InjectedStorageRW, OnChainStorageRO}, injected::{ - InjectedTransaction, InjectedTransactionAcceptance, PurgedTransaction, ShieldedTransaction, + InjectedTransaction, TransactionAcceptance, PurgedTransaction, ShieldedTransaction, SignedInjectedTransaction, SignedShieldedTransaction, Transaction, TransactionHash, TransactionPurgedReason, TransactionRef, VALIDITY_WINDOW, }, @@ -58,7 +58,7 @@ use tracing::{info, trace}; /// the caller should treat it as terminal. /// /// Group membership is queried via [`Self::is_accepted`]; the -/// `From for InjectedTransactionAcceptance` impl uses +/// `From for TransactionAcceptance` impl uses /// that to project into the RPC-facing acceptance type. #[derive(Clone, Debug, PartialEq, Eq, derive_more::Display)] pub enum TxInsertionStatus { @@ -100,7 +100,7 @@ impl TxInsertionStatus { } } -impl From for InjectedTransactionAcceptance { +impl From for TransactionAcceptance { fn from(status: TxInsertionStatus) -> Self { if status.is_accepted() { Self::Accept @@ -575,14 +575,14 @@ mod tests { BlockHeader, PrivateKey, SignedMessage, SimpleBlockData, db::{BlockMetaStorageRW, GlobalsStorageRW, OnChainStorageRW}, injected::{ - InjectedTransaction, InjectedTransactionAcceptance, SignedInjectedTransaction, + InjectedTransaction, TransactionAcceptance, SignedInjectedTransaction, SignedShieldedTransaction, }, }; use gprimitives::ActorId; use std::time::Duration; - /// Pins the `TxInsertionStatus -> InjectedTransactionAcceptance` split. + /// Pins the `TxInsertionStatus -> TransactionAcceptance` split. /// Adding a variant without updating [`TxInsertionStatus::is_accepted`] /// will be caught here. #[test] @@ -594,8 +594,8 @@ mod tests { ] { assert!(status.is_accepted(), "{status:?} must classify as accepted"); assert_eq!( - InjectedTransactionAcceptance::from(status), - InjectedTransactionAcceptance::Accept, + TransactionAcceptance::from(status), + TransactionAcceptance::Accept, ); } for status in [ @@ -609,8 +609,8 @@ mod tests { ); let reason = status.to_string(); assert_eq!( - InjectedTransactionAcceptance::from(status), - InjectedTransactionAcceptance::Reject { reason }, + TransactionAcceptance::from(status), + TransactionAcceptance::Reject { reason }, ); } } diff --git a/ethexe/network/src/injected.rs b/ethexe/network/src/injected.rs index cb0f9fba9eb..3f8bf0c947a 100644 --- a/ethexe/network/src/injected.rs +++ b/ethexe/network/src/injected.rs @@ -8,7 +8,7 @@ use crate::{ }; use ethexe_common::{ Address, - injected::{InjectedTransactionAcceptance, Transaction, TransactionHash}, + injected::{TransactionAcceptance, Transaction, TransactionHash}, }; use futures::{FutureExt, StreamExt, future::BoxFuture, stream::FuturesUnordered}; use libp2p::{ @@ -70,7 +70,7 @@ pub(crate) struct InnerRequest(Transaction); /// Network-only type to be encoded-decoded and sent over the network #[derive(Debug, Encode, Decode)] -pub(crate) struct InnerResponse(InjectedTransactionAcceptance); +pub(crate) struct InnerResponse(TransactionAcceptance); #[derive(Debug)] pub enum Event { @@ -78,12 +78,12 @@ pub enum Event { InboundTransaction { peer: PeerId, transaction: Box, - channel: oneshot::Sender, + channel: oneshot::Sender, }, /// We got a response from a validator we sent transaction to OutboundAcceptance { transaction_hash: TransactionHash, - acceptance: InjectedTransactionAcceptance, + acceptance: TransactionAcceptance, }, } @@ -94,7 +94,7 @@ impl Event { ) -> ( PeerId, Transaction, - oneshot::Sender, + oneshot::Sender, ) { match self { Event::InboundTransaction { @@ -108,7 +108,7 @@ impl Event { fn unwrap_injected_transaction_acceptance( self, - ) -> (TransactionHash, InjectedTransactionAcceptance) { + ) -> (TransactionHash, TransactionAcceptance) { match self { Event::OutboundAcceptance { transaction_hash, @@ -472,7 +472,7 @@ mod tests { .next_behaviour_event() .await .unwrap_injected_transaction_acceptance(); - assert_eq!(acceptance, InjectedTransactionAcceptance::Accept); + assert_eq!(acceptance, TransactionAcceptance::Accept); }); let (peer, new_tx, channel) = bob @@ -481,7 +481,7 @@ mod tests { .unwrap_new_injected_transaction(); assert_eq!(peer, alice_peer_id); assert_eq!(new_tx, transaction); - channel.send(InjectedTransactionAcceptance::Accept).unwrap(); + channel.send(TransactionAcceptance::Accept).unwrap(); tokio::spawn(bob.loop_on_next()); alice_handle.await.unwrap(); @@ -513,7 +513,7 @@ mod tests { .next_behaviour_event() .await .unwrap_injected_transaction_acceptance(); - assert_eq!(acceptance, InjectedTransactionAcceptance::Accept); + assert_eq!(acceptance, TransactionAcceptance::Accept); } }); @@ -523,7 +523,7 @@ mod tests { .unwrap_new_injected_transaction(); assert_eq!(peer, alice_peer_id); assert_eq!(new_tx, transaction); - channel.send(InjectedTransactionAcceptance::Accept).unwrap(); + channel.send(TransactionAcceptance::Accept).unwrap(); tokio::spawn(bob.loop_on_next()); let (peer, new_tx, channel) = carol @@ -532,7 +532,7 @@ mod tests { .unwrap_new_injected_transaction(); assert_eq!(peer, alice_peer_id); assert_eq!(new_tx, transaction); - channel.send(InjectedTransactionAcceptance::Accept).unwrap(); + channel.send(TransactionAcceptance::Accept).unwrap(); tokio::spawn(carol.loop_on_next()); alice_handle.await.unwrap(); @@ -560,7 +560,7 @@ mod tests { .unwrap_injected_transaction_acceptance(); assert_eq!( acceptance, - InjectedTransactionAcceptance::Reject { + TransactionAcceptance::Reject { reason: REJECT_REASON.to_string(), } ); @@ -573,7 +573,7 @@ mod tests { assert_eq!(peer, alice_peer_id); assert_eq!(new_tx, transaction); channel - .send(InjectedTransactionAcceptance::Reject { + .send(TransactionAcceptance::Reject { reason: REJECT_REASON.to_string(), }) .unwrap(); @@ -602,7 +602,7 @@ mod tests { .unwrap_injected_transaction_acceptance(); assert_eq!( acceptance, - InjectedTransactionAcceptance::Reject { + TransactionAcceptance::Reject { reason: OutboundFailure::ConnectionClosed.to_string(), } ); diff --git a/ethexe/rpc/src/apis/injected/relay.rs b/ethexe/rpc/src/apis/injected/relay.rs index 97c46fab5e1..0d6c46cded6 100644 --- a/ethexe/rpc/src/apis/injected/relay.rs +++ b/ethexe/rpc/src/apis/injected/relay.rs @@ -7,7 +7,7 @@ //! validator in the current era and returns the first acceptance. use crate::{RpcEvent, errors}; -use ethexe_common::injected::{InjectedTransactionAcceptance, Transaction}; +use ethexe_common::injected::{TransactionAcceptance, Transaction}; use jsonrpsee::core::RpcResult; use tokio::sync::{mpsc, oneshot}; @@ -26,7 +26,7 @@ impl TransactionsRelayer { pub async fn relay( &self, transaction: Transaction, - ) -> RpcResult { + ) -> RpcResult { let tx_hash = transaction.as_ref().hash(); tracing::trace!(%tx_hash, ?transaction, "Called injected_sendTransaction with vars"); diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index c7ee4d9b1ff..402fa6ef8f2 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -11,7 +11,7 @@ use ethexe_common::{ HashOf, db::{InjectedStorageRO, TdecStorageRO}, injected::{ - InjectedTransaction, InjectedTransactionAcceptance, ShieldedTransaction, + InjectedTransaction, TransactionAcceptance, ShieldedTransaction, SignedInjectedTransaction, SignedTxReceipt, Transaction, }, }; @@ -45,7 +45,7 @@ impl InjectedServer for InjectedApi { async fn send_transaction( &self, transaction: Transaction, - ) -> RpcResult { + ) -> RpcResult { self.send_transaction(transaction).await } @@ -96,7 +96,7 @@ impl InjectedApi { async fn send_transaction( &self, transaction: Transaction, - ) -> RpcResult { + ) -> RpcResult { self.relayer.relay(transaction).await } @@ -119,12 +119,12 @@ impl InjectedApi { self.manager.cancel_registration(tx_hash); })?; let sink = match acceptance { - InjectedTransactionAcceptance::Accept => { + TransactionAcceptance::Accept => { pending.accept().await.inspect_err(|_err| { self.manager.cancel_registration(tx_hash); })? } - InjectedTransactionAcceptance::Reject { reason } => { + TransactionAcceptance::Reject { reason } => { self.manager.cancel_registration(tx_hash); return Err(reason.into()); } diff --git a/ethexe/rpc/src/apis/injected/trait.rs b/ethexe/rpc/src/apis/injected/trait.rs index 1c28ff139d8..e30c925afa8 100644 --- a/ethexe/rpc/src/apis/injected/trait.rs +++ b/ethexe/rpc/src/apis/injected/trait.rs @@ -4,7 +4,7 @@ use ethexe_common::{ HashOf, injected::{ - InjectedTransaction, InjectedTransactionAcceptance, SignedInjectedTransaction, + InjectedTransaction, TransactionAcceptance, SignedInjectedTransaction, SignedTxReceipt, Transaction, }, }; @@ -32,7 +32,7 @@ pub trait Injected { async fn send_transaction( &self, transaction: Transaction, - ) -> jsonrpsee::core::RpcResult; + ) -> jsonrpsee::core::RpcResult; /// Sends an injected transaction and subscribes to its promise. #[subscription( diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index 7de11775934..96180dcc14e 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -51,7 +51,7 @@ use apis::{ use ethexe_common::HashOf; #[cfg(feature = "server")] use ethexe_common::injected::{ - InjectedTransaction, InjectedTransactionAcceptance, Promise, ShieldedTransaction, + InjectedTransaction, TransactionAcceptance, Promise, ShieldedTransaction, SignedCompactTxReceipt, Transaction, }; #[cfg(feature = "server")] @@ -102,7 +102,7 @@ pub const DEFAULT_BLOCK_GAS_LIMIT_MULTIPLIER: u64 = 10; pub enum RpcEvent { Transaction { transaction: Transaction, - response_sender: oneshot::Sender, + response_sender: oneshot::Sender, }, } diff --git a/ethexe/rpc/src/tests.rs b/ethexe/rpc/src/tests.rs index 715edb5f229..5eac2d9ec64 100644 --- a/ethexe/rpc/src/tests.rs +++ b/ethexe/rpc/src/tests.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 use crate::{ - CodeClient, InjectedApi, InjectedClient, InjectedTransactionAcceptance, RpcConfig, RpcEvent, + CodeClient, InjectedApi, InjectedClient, TransactionAcceptance, RpcConfig, RpcEvent, RpcServer, RpcService, test_utils::wasm_with_custom_section, }; use ethexe_common::{ @@ -79,7 +79,7 @@ impl MockService { event = self.rpc.next() => { let RpcEvent::Transaction {transaction, response_sender} = event.expect("RPC event will be valid"); - response_sender.send(InjectedTransactionAcceptance::Accept).expect("Response sender will be valid"); + response_sender.send(TransactionAcceptance::Accept).expect("Response sender will be valid"); match transaction { Transaction::Injected(transaction) => tx_batch.push(transaction), Transaction::Shielded(_) => todo!("Shielded transaction execution"), diff --git a/ethexe/sdk/src/mirror.rs b/ethexe/sdk/src/mirror.rs index 9f4c62bb95d..c55db1571a1 100644 --- a/ethexe/sdk/src/mirror.rs +++ b/ethexe/sdk/src/mirror.rs @@ -9,7 +9,7 @@ use ethexe_common::{ gear::ValueClaim, gear_core::rpc::ReplyInfo, injected::{ - InjectedTransaction, InjectedTransactionAcceptance, Promise, Receipt, + InjectedTransaction, TransactionAcceptance, Promise, Receipt, SignedInjectedTransaction, }, }; @@ -293,7 +293,7 @@ impl<'a> Mirror<'a> { let message_id = injected_transaction.to_message_id(); let tx_hash = injected_transaction.to_hash().into(); - let result: InjectedTransactionAcceptance = self + let result: TransactionAcceptance = self .api .vara_eth_client() .send_transaction(transaction.into()) @@ -301,14 +301,14 @@ impl<'a> Mirror<'a> { .with_context(|| "failed to send injected transaction")?; match result { - InjectedTransactionAcceptance::Accept => Ok(InjectedMessageResult { + TransactionAcceptance::Accept => Ok(InjectedMessageResult { message_id, tx_hash, reference_block_number, reference_block_hash, promise: None, }), - InjectedTransactionAcceptance::Reject { reason } => { + TransactionAcceptance::Reject { reason } => { Err(anyhow!("injected transaction was rejected: {reason}")) } } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index cbea4aa6809..496c19d5ad2 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -47,7 +47,7 @@ use ethexe_common::{ CodeAndIdUnchecked, PromiseEmissionMode, db::{GlobalsStorageRW, MbStorageRO, OnChainStorageRO}, gear::CodeState, - injected::{CompactPromise, InjectedTransactionAcceptance, Receipt}, + injected::{CompactPromise, TransactionAcceptance, Receipt}, malachite::BlockDecryptionData, network::VerifiedValidatorMessage, }; @@ -864,7 +864,7 @@ impl Service { Some(malachite) => { malachite.receive_transaction(*transaction).into() } - None => InjectedTransactionAcceptance::Reject { + None => TransactionAcceptance::Reject { reason: "no malachite service to handle transaction".into(), }, }; @@ -917,12 +917,12 @@ impl Service { if let Some(malachite) = malachite.as_mut() { let status = malachite.receive_transaction(transaction.clone()); local_acceptance = - Some(InjectedTransactionAcceptance::from(status)); + Some(TransactionAcceptance::from(status)); } match network.as_mut() { Some(network) => match local_acceptance { - Some(acceptance @ InjectedTransactionAcceptance::Accept) => { + Some(acceptance @ TransactionAcceptance::Accept) => { // local consensus handle transaction, no need to wait for other acceptances if let Err(err) = network.broadcast_injected_transaction(transaction) @@ -959,7 +959,7 @@ impl Service { } Err(err) => { let acceptance = - InjectedTransactionAcceptance::Reject { + TransactionAcceptance::Reject { reason: err.to_string(), }; @@ -976,7 +976,7 @@ impl Service { None => { // No network, send local_acceptance to RPC let acceptance = local_acceptance.unwrap_or_else(|| { - InjectedTransactionAcceptance::Reject { + TransactionAcceptance::Reject { reason: "RPC not a validator and do not connect to P2P network".into(), } }); diff --git a/ethexe/service/src/pending_tx.rs b/ethexe/service/src/pending_tx.rs index fb083834958..fb6ed61367f 100644 --- a/ethexe/service/src/pending_tx.rs +++ b/ethexe/service/src/pending_tx.rs @@ -1,7 +1,7 @@ // Copyright (C) Gear Technologies Inc. // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 -use ethexe_common::injected::InjectedTransactionAcceptance; +use ethexe_common::injected::TransactionAcceptance; use std::num::NonZeroUsize; use tokio::sync::oneshot; @@ -10,16 +10,16 @@ use tokio::sync::oneshot; /// Transaction senders waits for acceptance/reject from other validators in /// network. pub(super) struct PendingNetworkInjectedTx { - response_senders: Vec>, + response_senders: Vec>, pending_responses: usize, - last_reject: Option, + last_reject: Option, } impl PendingNetworkInjectedTx { pub(super) fn new( - response_sender: oneshot::Sender, + response_sender: oneshot::Sender, pending_responses: NonZeroUsize, - last_reject: Option, + last_reject: Option, ) -> Self { Self { response_senders: vec![response_sender], @@ -30,24 +30,24 @@ impl PendingNetworkInjectedTx { pub(super) fn add_response_sender( &mut self, - response_sender: oneshot::Sender, + response_sender: oneshot::Sender, ) { self.response_senders.push(response_sender); } pub(super) fn into_response_senders( self, - ) -> Vec> { + ) -> Vec> { self.response_senders } pub(super) fn record_response( &mut self, - acceptance: InjectedTransactionAcceptance, - ) -> Option { + acceptance: TransactionAcceptance, + ) -> Option { match acceptance { - InjectedTransactionAcceptance::Accept => Some(InjectedTransactionAcceptance::Accept), - rejection @ InjectedTransactionAcceptance::Reject { .. } => { + TransactionAcceptance::Accept => Some(TransactionAcceptance::Accept), + rejection @ TransactionAcceptance::Reject { .. } => { // Infallible because in case of `self.pending_responses == 0` returns `Some`. self.pending_responses = self.pending_responses.checked_sub(1).expect("infallible"); self.last_reject = Some(rejection); @@ -67,7 +67,7 @@ impl PendingNetworkInjectedTx { mod tests { use super::*; - fn response_sender() -> oneshot::Sender { + fn response_sender() -> oneshot::Sender { oneshot::channel().0 } @@ -79,9 +79,9 @@ mod tests { None, ); - let acceptance = pending.record_response(InjectedTransactionAcceptance::Accept); + let acceptance = pending.record_response(TransactionAcceptance::Accept); - assert_eq!(acceptance, Some(InjectedTransactionAcceptance::Accept)); + assert_eq!(acceptance, Some(TransactionAcceptance::Accept)); } #[test] @@ -89,24 +89,24 @@ mod tests { let mut pending = PendingNetworkInjectedTx::new( response_sender(), NonZeroUsize::new(2).expect("non-zero"), - Some(InjectedTransactionAcceptance::Reject { + Some(TransactionAcceptance::Reject { reason: "local".into(), }), ); - let acceptance = pending.record_response(InjectedTransactionAcceptance::Reject { + let acceptance = pending.record_response(TransactionAcceptance::Reject { reason: "remote-1".into(), }); assert_eq!(acceptance, None); - let acceptance = pending.record_response(InjectedTransactionAcceptance::Reject { + let acceptance = pending.record_response(TransactionAcceptance::Reject { reason: "remote-2".into(), }); assert_eq!( acceptance, - Some(InjectedTransactionAcceptance::Reject { + Some(TransactionAcceptance::Reject { reason: "remote-2".into() }) ); @@ -120,13 +120,13 @@ mod tests { None, ); - let acceptance = pending.record_response(InjectedTransactionAcceptance::Reject { + let acceptance = pending.record_response(TransactionAcceptance::Reject { reason: "remote".into(), }); assert_eq!( acceptance, - Some(InjectedTransactionAcceptance::Reject { + Some(TransactionAcceptance::Reject { reason: "remote".into() }) ); diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 7dddeaf3a4b..0d859de2aea 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -23,7 +23,7 @@ use ethexe_common::{ }, gear::BatchCommitment, injected::{ - InjectedTransaction, InjectedTransactionAcceptance, Receipt, TransactionHash, + InjectedTransaction, TransactionAcceptance, Receipt, TransactionHash, TransactionPurgedReason, }, mock::*, @@ -1885,7 +1885,7 @@ async fn send_injected_tx() { .send_transaction(signed_tx.clone().into()) .await .expect("rpc server is set"); - assert_eq!(acceptance, InjectedTransactionAcceptance::Accept); + assert_eq!(acceptance, TransactionAcceptance::Accept); // Tx executable validation takes time, so wait for event. node1 diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index fb5b72a7420..cad23677b9c 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -12,7 +12,7 @@ use ethexe_common::{ db::*, events::BlockEvent, injected::{ - InjectedTransactionAcceptance, SignedCompactTxReceipt, Transaction, TransactionHash, + TransactionAcceptance, SignedCompactTxReceipt, Transaction, TransactionHash, }, malachite::SignedBlockDecryptionShares, network::VerifiedValidatorMessage, @@ -50,7 +50,7 @@ pub enum TestingNetworkInjectedEvent { }, OutboundAcceptance { transaction_hash: TransactionHash, - acceptance: InjectedTransactionAcceptance, + acceptance: TransactionAcceptance, }, } From 6daf9ae1c0b36a466d5a2ebd57e262e4a3d99ed1 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 24 Jun 2026 18:24:06 +0300 Subject: [PATCH 35/41] chore: move transactions unshielding to --- ethexe/common/src/db.rs | 3 +- ethexe/common/src/mock.rs | 8 +- ethexe/malachite/service/src/externalities.rs | 372 ++++++++++-------- ethexe/malachite/service/src/lib.rs | 18 +- ethexe/malachite/service/src/mempool.rs | 8 +- ethexe/network/src/injected.rs | 12 +- ethexe/rpc/src/apis/injected/relay.rs | 7 +- ethexe/rpc/src/apis/injected/server.rs | 22 +- ethexe/rpc/src/apis/injected/trait.rs | 4 +- ethexe/rpc/src/lib.rs | 4 +- ethexe/rpc/src/tests.rs | 4 +- ethexe/sdk/src/mirror.rs | 3 +- ethexe/service/src/lib.rs | 23 +- ethexe/service/src/pending_tx.rs | 4 +- ethexe/service/src/tests/mod.rs | 2 +- ethexe/service/src/tests/utils/events.rs | 4 +- 16 files changed, 244 insertions(+), 254 deletions(-) diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index 56b136ec46d..af8ef84079a 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -179,7 +179,6 @@ pub struct CompactMb { pub struct MbMeta { pub computed: bool, pub last_advanced_eb: H256, - pub contains_shielded: bool, } #[auto_impl::auto_impl(&, Box)] @@ -299,7 +298,7 @@ mod tests { #[test] fn ensure_types_unchanged() { const EXPECTED_TYPE_INFO_HASH: &str = - "6a9d4140086d241dd267bc95b0f70e5114721fec1a2071c46dd967c8881eff9c"; + "c543e8c3d27f17bd77d510ce3f1d2b3a286b6444559444eb78807b3c2fd9ffbf"; let types = [ meta_type::(), diff --git a/ethexe/common/src/mock.rs b/ethexe/common/src/mock.rs index 86db2cc6728..183ed86010c 100644 --- a/ethexe/common/src/mock.rs +++ b/ethexe/common/src/mock.rs @@ -12,7 +12,7 @@ use crate::{ BatchCommitment, ChainCommitment, CodeCommitment, Message, MessageType, StateTransition, }, injected::{InjectedTransaction, Promise}, - malachite::{Operation, Operations}, + malachite::Operations, }; use alloc::{collections::BTreeMap, vec}; use gear_core::{ @@ -653,12 +653,6 @@ impl BlockChain { operations_hash, }, ); - db.mutate_mb_meta(mb.hash, |meta| { - meta.contains_shielded = mb - .operations - .iter() - .any(|op| matches!(op, Operation::Shielded(_))); - }); if let Some(computed) = &mb.computed { db.set_mb_program_states(mb.hash, computed.program_states.clone()); db.mutate_mb_meta(mb.hash, |meta| { diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 6e87f0f1311..d86804e3b20 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -44,7 +44,7 @@ use crate::{ quarantine, tx_validity::{TxValidity, TxValidityChecker, eb_touched_programs}, }; -use anyhow::{Result, anyhow, bail}; +use anyhow::{Context, Result, anyhow, bail}; use async_trait::async_trait; use bytes::Bytes; use ethexe_common::{ @@ -67,7 +67,7 @@ use gear_tdec::bls12_381::{ DecryptionShareSimple, SharedSecret, prepare_combine_simple, share_combine_simple, }; use gprimitives::H256; -use gsigner::tdec::TdecKeyStore; +use gsigner::{Address, PublicDecryptionContext, tdec::TdecKeyStore}; use parity_scale_codec::{DecodeAll, Encode}; use std::{ collections::{BTreeMap, HashMap, HashSet, VecDeque}, @@ -76,6 +76,9 @@ use std::{ use tokio::sync::{Notify, mpsc}; use tracing::{debug, error, info, warn}; +/// Type alias for decryption keys provided in [Operation]. +pub(crate) type DecryptionKeys = BTreeMap, SharedSecret>; + /// Inputs the externalities need to satisfy the [`ethexe_malachite_core::Externalities`] /// contract. Constructed by [`crate::MalachiteService::new`] and /// handed to the inner ethexe-malachite-core service inside an [`Arc`]. @@ -182,9 +185,6 @@ impl Externalities for EthexeExternalities { // CompactMb exists, operations are reachable" — holds // unconditionally. let operations_hash = self.db.set_operations(operations.clone()); - let contains_shielded = operations - .iter() - .any(|op| matches!(op, Operation::Shielded(_))); self.db.set_mb_compact_block( mb_hash, CompactMb { @@ -195,7 +195,6 @@ impl Externalities for EthexeExternalities { ); self.db.mutate_mb_meta(mb_hash, |meta| { meta.last_advanced_eb = last_advanced; - meta.contains_shielded = contains_shielded; }); let shielded_transactions = operations @@ -205,54 +204,40 @@ impl Externalities for EthexeExternalities { self.decryption_shares .register_block(mb_hash, shielded_transactions.iter().map(|tx| tx.to_hash())); - let can_speculatively_execute = self.can_speculatively_execute(parent)?; + if let Some(context) = self.tdec_ctx.as_ref() { + // If this node have TDEC context - try provide shares for shielded transaction in this block. + let tdec_ctx = &context.my_context; + + let maybe_my_address = context.contexts.iter().find_map(|(address, participant)| { + (participant.validator_public_key == tdec_ctx.validator_public_key) + .then_some(*address) + }); + match maybe_my_address { + Some(my_address) => self.provide_decryption_shares( + mb_hash, + tdec_ctx, + my_address, + &shielded_transactions, + ), + None => warn!("local TDEC context is absent from validator contexts"), + } + } + + // If decryption keys provided - decrypt shielded transactions and save them to database. + if let Some(decryption_keys) = operations.iter().find_map(|op| match op { + Operation::DecryptionKeys(keys) => Some(keys.clone()), + _ => None, + }) { + self.process_unshielding(mb_hash, &decryption_keys)?; + } + self.try_emit_or_queue( MalachiteEvent::BlockProposal { height: mb.height, mb_hash, - can_speculatively_execute, }, last_advanced, ); - - let Some(context) = self.tdec_ctx.as_ref() else { - return Ok(()); - }; - let decryption_context = &context.my_context; - let Some(local_validator) = context.contexts.iter().find_map(|(address, participant)| { - (participant.validator_public_key == decryption_context.validator_public_key) - .then_some(*address) - }) else { - warn!("local TDEC context is absent from validator contexts"); - return Ok(()); - }; - - let mut shares = Vec::with_capacity(shielded_transactions.len()); - for tx in shielded_transactions { - let Ok(share) = self.tdec_store.create_share( - decryption_context, - &tx.ciphertext.header(), - tx.aad.as_ref(), - ) else { - continue; - }; - let tx_hash = tx.to_hash(); - let outcome = - self.decryption_shares - .insert(mb_hash, tx_hash, local_validator, share.clone()); - debug_assert!(matches!( - outcome, - InsertOutcome::Inserted | InsertOutcome::Duplicate - )); - shares.push(ShieldedTxDecryptionShare { tx_hash, share }); - } - - if !shares.is_empty() { - // Channel receiver is dropped only during shutdown. - let _ = self - .event_tx - .send(Ok(MalachiteEvent::DecryptionShares { mb_hash, shares })); - } Ok(()) } @@ -296,30 +281,6 @@ impl Externalities for EthexeExternalities { // Retain shares belonging to another block self.decryption_shares.retain_block(mb_hash); - if let Some(decryption_keys) = operations.iter().find_map(|op| match op { - Operation::DecryptionKeys(keys) => Some(keys.clone()), - _ => None, - }) { - let UnshieldingOutput { - unshielded: unshielded_with_hashes, - not_unshielded, - } = self.unshield_parent_transactions(compact.parent, &decryption_keys)?; - let unshielded_hash_mapping = unshielded_with_hashes - .iter() - .map(|(tx_hash, tx)| (*tx_hash, tx.data().to_hash())) - .collect(); - let unshielded = unshielded_with_hashes - .into_iter() - .map(|(_, tx)| tx) - .collect(); - self.db.set_mb_unshielded_txs(mb_hash, unshielded); - let _ = self.event_tx.send(Ok(MalachiteEvent::UnshieldingOutput { - mb_hash, - unshielded_hash_mapping, - not_unshielded, - })); - } - let app_cert = CommitCertificate { height: cert.height, mb_hash, @@ -789,59 +750,6 @@ impl Externalities for EthexeExternalities { } impl EthexeExternalities { - fn can_speculatively_execute(&self, parent_mb_hash: H256) -> Result { - if parent_mb_hash.is_zero() { - return Ok(true); - } - - self.db.mb_compact_block(parent_mb_hash).ok_or_else(|| { - anyhow!("can_speculatively_execute: no CompactMb for parent {parent_mb_hash}") - })?; - Ok(!self.db.mb_meta(parent_mb_hash).contains_shielded) - } - - fn unshield_parent_transactions( - &self, - parent_mb_hash: H256, - decryption_keys: &BTreeMap, SharedSecret>, - ) -> Result { - if parent_mb_hash.is_zero() || decryption_keys.is_empty() { - return Ok(UnshieldingOutput::default()); - } - - let compact = self.db.mb_compact_block(parent_mb_hash).ok_or_else(|| { - anyhow!("unshield_parent_transactions: no CompactMb for parent {parent_mb_hash}") - })?; - let operations = self.db.operations(compact.operations_hash).ok_or_else(|| { - anyhow!( - "unshield_parent_transactions: operations blob {} missing for parent {parent_mb_hash}", - compact.operations_hash - ) - })?; - - let mut output = UnshieldingOutput::default(); - for tx in operations.into_iter().filter_map(Operation::into_shielded) { - let tx_hash = tx.data().to_hash(); - match decryption_keys.get(&tx_hash) { - Some(shared_key) => { - match tx.into_verified().try_map(|tx| tx.unshield(shared_key)) { - Ok(injected_tx) => output.unshielded.push((tx_hash, injected_tx)), - Err(_err) => output.not_unshielded.push(PurgedTransaction { - tx_hash: TransactionHash::Right(tx_hash), - reason: TransactionPurgedReason::DecryptionFailed, - }), - } - } - None => output.not_unshielded.push(PurgedTransaction { - tx_hash: TransactionHash::Right(tx_hash), - reason: TransactionPurgedReason::DecryptionFailed, - }), - } - } - - Ok(output) - } - /// True iff `prerequisite.is_zero()` (no prerequisite — genesis /// or pre-advance) or the prerequisite Eth block has been fully /// **prepared** locally. @@ -948,7 +856,7 @@ impl EthexeExternalities { async fn wait_for_shielded_tx_decryption_keys( &self, parent_mb_hash: H256, - ) -> Result, SharedSecret>>> { + ) -> Result> { if parent_mb_hash.is_zero() { return Ok(None); } @@ -985,7 +893,7 @@ impl EthexeExternalities { ); } - let mut keys = BTreeMap::new(); + let mut keys = DecryptionKeys::default(); while !pending.is_empty() { pending.retain(|tx_hash| { let Some(selected) = @@ -1103,6 +1011,114 @@ impl EthexeExternalities { } } + fn provide_decryption_shares( + &self, + mb_hash: H256, + tdec_ctx: &PublicDecryptionContext, + my_address: Address, + transactions: &[&ShieldedTransaction], + ) { + let mut shares = Vec::with_capacity(transactions.len()); + for tx in transactions { + let Ok(share) = + self.tdec_store + .create_share(tdec_ctx, &tx.ciphertext.header(), tx.aad.as_ref()) + else { + continue; + }; + let tx_hash = tx.to_hash(); + let outcome = + self.decryption_shares + .insert(mb_hash, tx_hash, my_address, share.clone()); + debug_assert!(matches!( + outcome, + InsertOutcome::Inserted | InsertOutcome::Duplicate + )); + shares.push(ShieldedTxDecryptionShare { tx_hash, share }); + } + + if !shares.is_empty() { + // Channel receiver is dropped only during shutdown. + let _ = self + .event_tx + .send(Ok(MalachiteEvent::DecryptionShares { mb_hash, shares })); + } + } + + fn process_unshielding(&self, mb_hash: H256, decryption_keys: &DecryptionKeys) -> Result<()> { + let compact = self + .db + .mb_compact_block(mb_hash) + .context("process_unshielding: no compact for {mb_hash}")?; + + let UnshieldingOutput { + unshielded: unshielded_with_hashes, + not_unshielded, + } = self.unshield_parent_transactions(compact.parent, decryption_keys)?; + + let unshielded_hash_mapping = unshielded_with_hashes + .iter() + .map(|(tx_hash, tx)| (*tx_hash, tx.data().to_hash())) + .collect(); + + let unshielded = unshielded_with_hashes + .into_iter() + .map(|(_, tx)| tx) + .collect(); + + self.db.set_mb_unshielded_txs(mb_hash, unshielded); + + let _ = self.event_tx.send(Ok(MalachiteEvent::UnshieldingOutput { + mb_hash, + unshielded_hash_mapping, + not_unshielded, + })); + + Ok(()) + } + + fn unshield_parent_transactions( + &self, + parent_mb_hash: H256, + decryption_keys: &DecryptionKeys, + ) -> Result { + if parent_mb_hash.is_zero() || decryption_keys.is_empty() { + return Ok(UnshieldingOutput::default()); + } + + let compact = self.db.mb_compact_block(parent_mb_hash).ok_or_else(|| { + anyhow!("unshield_parent_transactions: no CompactMb for parent {parent_mb_hash}") + })?; + let operations = self.db.operations(compact.operations_hash).ok_or_else(|| { + anyhow!( + "unshield_parent_transactions: operations blob {} missing for parent {parent_mb_hash}", + compact.operations_hash + ) + })?; + + let mut output = UnshieldingOutput::default(); + for tx in operations.into_iter().filter_map(Operation::into_shielded) { + let tx_hash = tx.data().to_hash(); + match decryption_keys.get(&tx_hash) { + Some(shared_key) => { + match tx.into_verified().try_map(|tx| tx.unshield(shared_key)) { + Ok(injected_tx) => output.unshielded.push((tx_hash, injected_tx)), + Err(_err) => output.not_unshielded.push(PurgedTransaction { + tx_hash: TransactionHash::Right(tx_hash), + reason: TransactionPurgedReason::DecryptionFailed, + }), + } + } + None => output.not_unshielded.push(PurgedTransaction { + tx_hash: TransactionHash::Right(tx_hash), + reason: TransactionPurgedReason::DecryptionFailed, + }), + } + } + + Ok(output) + } + // Candidate EB must be anchored in the quarantine and a strict descendant of the previously advanced EB. fn find_eb_candidate_for_advancing(&self, prev_advanced_eb_hash: H256) -> Option { let head = (*self.chain_head.read().expect("chain_head poisoned"))?; @@ -1303,27 +1319,6 @@ mod tests { } } - fn shielded_operation() -> Operation { - use ethexe_common::{SignedMessage, injected::InjectedTransaction}; - use gprimitives::ActorId; - - let dkg_public_key = gear_tdec::deal::(1, 1, &mut test_rng()).public_key; - let injected = InjectedTransaction { - destination: ActorId::from([1; 32]), - payload: vec![1, 2, 3].try_into().unwrap(), - value: 0, - reference_block: H256::zero(), - salt: vec![7; 32].try_into().unwrap(), - }; - let shielded = injected - .shield(&dkg_public_key, &mut test_rng()) - .expect("test shielding must succeed"); - Operation::Shielded( - SignedMessage::create(ethexe_common::PrivateKey::random(), shielded) - .expect("test signature must be valid"), - ) - } - /// `process_mb_proposal` populates `mb_block`, `mb_meta` (height, /// parent_mb_hash, last_advanced_eb, synced=true) and the /// height index, then emits a `BlockProposal`. @@ -1349,11 +1344,9 @@ mod tests { MalachiteEvent::BlockProposal { height, mb_hash: proposed, - can_speculatively_execute, } => { assert_eq!(height, 1); assert_eq!(proposed, mb_hash); - assert!(can_speculatively_execute); let _ = p; } other => panic!("expected BlockProposal, got {other:?}"), @@ -1364,37 +1357,78 @@ mod tests { } #[tokio::test] - async fn block_proposal_for_parent_with_shielded_tx_disables_speculative_execution() { + async fn process_mb_proposal_unshields_parent_transactions() { + use ethexe_common::{ + SignedMessage, + db::MbStorageRO, + injected::{InjectedTransaction, TransactionHash}, + }; + use gprimitives::ActorId; + let db = Database::memory(); - let (ext, mut rx) = make_externalities(db); + let (mut ext, mut rx) = make_externalities(db.clone()); + let (tdec_ctx, tdec_store, dkg_public_key) = single_validator_tdec_setup(); + ext.tdec_ctx = Some(tdec_ctx); + ext.tdec_store = tdec_store; + + let injected = InjectedTransaction { + destination: ActorId::from([1; 32]), + payload: vec![1, 2, 3].try_into().unwrap(), + value: 0, + reference_block: H256::zero(), + salt: vec![7; 32].try_into().unwrap(), + }; + let shielded = injected + .clone() + .shield(&dkg_public_key, &mut test_rng()) + .expect("test shielding must succeed"); + let shielded_hash = shielded.to_hash(); + let signed_shielded = + SignedMessage::create(ethexe_common::PrivateKey::random(), shielded).unwrap(); let parent_payload = Operations::new(vec![ - shielded_operation(), + Operation::Shielded(signed_shielded), Operation::ProgressTasks, Operation::ProcessQueuesV3 { gas_allowance: 0 }, ]); let parent = wrap(parent_payload, 1, H256::zero()); let parent_hash = parent.hash(); ext.process_mb_proposal(parent_hash, parent).await.unwrap(); + let _ = rx.recv().await.expect("decryption shares").expect("ok"); let _ = rx.recv().await.expect("parent proposal").expect("ok"); - let child_payload = payload(None, 2); + let child_payload = ext + .build_operations(parent_hash) + .await + .expect("single-validator shares should produce decryption keys"); let child = wrap(child_payload, 2, parent_hash); let child_hash = child.hash(); ext.process_mb_proposal(child_hash, child).await.unwrap(); - match rx.try_recv().expect("child event").expect("ok") { - MalachiteEvent::BlockProposal { - height, + let unshielded = db.mb_unshielded_txs(child_hash); + assert_eq!(unshielded.len(), 1); + assert_eq!(unshielded[0].data(), &injected); + + match rx.try_recv().expect("unshielding event").expect("ok") { + MalachiteEvent::UnshieldingOutput { mb_hash, - can_speculatively_execute, + unshielded_hash_mapping, + not_unshielded, } => { - assert_eq!(height, 2); assert_eq!(mb_hash, child_hash); - assert!(!can_speculatively_execute); + assert_eq!( + unshielded_hash_mapping, + vec![(shielded_hash, injected.to_hash())] + ); + assert!(not_unshielded.is_empty()); } - other => panic!("expected BlockProposal, got {other:?}"), + other => panic!("expected UnshieldingOutput, got {other:?}"), } + + assert_eq!( + TransactionHash::Left(unshielded[0].data().to_hash()), + TransactionHash::Left(injected.to_hash()) + ); } /// `process_mb_finalized` reads the [`CompactMb`] + @@ -1686,12 +1720,18 @@ mod tests { let parent = Block::new(H256::zero(), 1, to_payload(parent_payload.encode())); let parent_hash = parent.hash(); ext.process_mb_proposal(parent_hash, parent).await.unwrap(); - let _ = rx.recv().await; // BlockProposal - let shares_event = rx.recv().await.expect("shares event").expect("ok"); - assert!(matches!( - shares_event, - MalachiteEvent::DecryptionShares { .. } - )); + let first = rx.recv().await.expect("first event").expect("ok"); + let second = rx.recv().await.expect("second event").expect("ok"); + assert!( + [&first, &second] + .iter() + .any(|event| matches!(event, MalachiteEvent::BlockProposal { .. })) + ); + assert!( + [&first, &second] + .iter() + .any(|event| matches!(event, MalachiteEvent::DecryptionShares { .. })) + ); let operations = tokio::time::timeout( std::time::Duration::from_millis(50), diff --git a/ethexe/malachite/service/src/lib.rs b/ethexe/malachite/service/src/lib.rs index b4d978ff61f..7ed2bb5af4b 100644 --- a/ethexe/malachite/service/src/lib.rs +++ b/ethexe/malachite/service/src/lib.rs @@ -88,12 +88,7 @@ pub struct CommitCertificate { #[derive(Debug, Clone, PartialEq, Eq)] pub enum MalachiteEvent { /// New sequencer block persisted; `mb_hash` is the Blake2b envelope hash. - BlockProposal { - height: u64, - mb_hash: H256, - /// Whether this MB can be executed before finalization. - can_speculatively_execute: bool, - }, + BlockProposal { height: u64, mb_hash: H256 }, /// BFT-committed block; `globals.latest_finalized_mb_hash` now points at it. BlockFinalized { @@ -127,15 +122,8 @@ pub enum MalachiteEvent { impl std::fmt::Display for MalachiteEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::BlockProposal { - height, - mb_hash, - can_speculatively_execute, - } => { - write!( - f, - "BlockProposal(height: {height}, mb_hash: {mb_hash}, can_speculatively_execute: {can_speculatively_execute})" - ) + Self::BlockProposal { height, mb_hash } => { + write!(f, "BlockProposal(height: {height}, mb_hash: {mb_hash})") } Self::BlockFinalized { cert, diff --git a/ethexe/malachite/service/src/mempool.rs b/ethexe/malachite/service/src/mempool.rs index 38b93dd41eb..686df4778b5 100644 --- a/ethexe/malachite/service/src/mempool.rs +++ b/ethexe/malachite/service/src/mempool.rs @@ -40,8 +40,8 @@ use ethexe_common::{ HashOf, SimpleBlockData, db::{GlobalsStorageRO, InjectedStorageRW, OnChainStorageRO}, injected::{ - InjectedTransaction, TransactionAcceptance, PurgedTransaction, ShieldedTransaction, - SignedInjectedTransaction, SignedShieldedTransaction, Transaction, TransactionHash, + InjectedTransaction, PurgedTransaction, ShieldedTransaction, SignedInjectedTransaction, + SignedShieldedTransaction, Transaction, TransactionAcceptance, TransactionHash, TransactionPurgedReason, TransactionRef, VALIDITY_WINDOW, }, }; @@ -575,8 +575,8 @@ mod tests { BlockHeader, PrivateKey, SignedMessage, SimpleBlockData, db::{BlockMetaStorageRW, GlobalsStorageRW, OnChainStorageRW}, injected::{ - InjectedTransaction, TransactionAcceptance, SignedInjectedTransaction, - SignedShieldedTransaction, + InjectedTransaction, SignedInjectedTransaction, SignedShieldedTransaction, + TransactionAcceptance, }, }; use gprimitives::ActorId; diff --git a/ethexe/network/src/injected.rs b/ethexe/network/src/injected.rs index 3f8bf0c947a..7e18fb8203a 100644 --- a/ethexe/network/src/injected.rs +++ b/ethexe/network/src/injected.rs @@ -8,7 +8,7 @@ use crate::{ }; use ethexe_common::{ Address, - injected::{TransactionAcceptance, Transaction, TransactionHash}, + injected::{Transaction, TransactionAcceptance, TransactionHash}, }; use futures::{FutureExt, StreamExt, future::BoxFuture, stream::FuturesUnordered}; use libp2p::{ @@ -91,11 +91,7 @@ pub enum Event { impl Event { fn unwrap_new_injected_transaction( self, - ) -> ( - PeerId, - Transaction, - oneshot::Sender, - ) { + ) -> (PeerId, Transaction, oneshot::Sender) { match self { Event::InboundTransaction { peer, @@ -106,9 +102,7 @@ impl Event { } } - fn unwrap_injected_transaction_acceptance( - self, - ) -> (TransactionHash, TransactionAcceptance) { + fn unwrap_injected_transaction_acceptance(self) -> (TransactionHash, TransactionAcceptance) { match self { Event::OutboundAcceptance { transaction_hash, diff --git a/ethexe/rpc/src/apis/injected/relay.rs b/ethexe/rpc/src/apis/injected/relay.rs index 0d6c46cded6..2c9cec7568b 100644 --- a/ethexe/rpc/src/apis/injected/relay.rs +++ b/ethexe/rpc/src/apis/injected/relay.rs @@ -7,7 +7,7 @@ //! validator in the current era and returns the first acceptance. use crate::{RpcEvent, errors}; -use ethexe_common::injected::{TransactionAcceptance, Transaction}; +use ethexe_common::injected::{Transaction, TransactionAcceptance}; use jsonrpsee::core::RpcResult; use tokio::sync::{mpsc, oneshot}; @@ -23,10 +23,7 @@ impl TransactionsRelayer { /// Broadcast `transaction` to every validator in the current era, /// returning the first `Accept` observed by the service. - pub async fn relay( - &self, - transaction: Transaction, - ) -> RpcResult { + pub async fn relay(&self, transaction: Transaction) -> RpcResult { let tx_hash = transaction.as_ref().hash(); tracing::trace!(%tx_hash, ?transaction, "Called injected_sendTransaction with vars"); diff --git a/ethexe/rpc/src/apis/injected/server.rs b/ethexe/rpc/src/apis/injected/server.rs index 402fa6ef8f2..b72093c2303 100644 --- a/ethexe/rpc/src/apis/injected/server.rs +++ b/ethexe/rpc/src/apis/injected/server.rs @@ -11,8 +11,8 @@ use ethexe_common::{ HashOf, db::{InjectedStorageRO, TdecStorageRO}, injected::{ - InjectedTransaction, TransactionAcceptance, ShieldedTransaction, - SignedInjectedTransaction, SignedTxReceipt, Transaction, + InjectedTransaction, ShieldedTransaction, SignedInjectedTransaction, SignedTxReceipt, + Transaction, TransactionAcceptance, }, }; use ethexe_db::Database; @@ -42,10 +42,7 @@ impl InjectedServer for InjectedApi { Ok(self.db.shielding_key()) } - async fn send_transaction( - &self, - transaction: Transaction, - ) -> RpcResult { + async fn send_transaction(&self, transaction: Transaction) -> RpcResult { self.send_transaction(transaction).await } @@ -93,10 +90,7 @@ impl InjectedApi { // RPC API implementation. impl InjectedApi { - async fn send_transaction( - &self, - transaction: Transaction, - ) -> RpcResult { + async fn send_transaction(&self, transaction: Transaction) -> RpcResult { self.relayer.relay(transaction).await } @@ -119,11 +113,9 @@ impl InjectedApi { self.manager.cancel_registration(tx_hash); })?; let sink = match acceptance { - TransactionAcceptance::Accept => { - pending.accept().await.inspect_err(|_err| { - self.manager.cancel_registration(tx_hash); - })? - } + TransactionAcceptance::Accept => pending.accept().await.inspect_err(|_err| { + self.manager.cancel_registration(tx_hash); + })?, TransactionAcceptance::Reject { reason } => { self.manager.cancel_registration(tx_hash); return Err(reason.into()); diff --git a/ethexe/rpc/src/apis/injected/trait.rs b/ethexe/rpc/src/apis/injected/trait.rs index e30c925afa8..957ad3a285d 100644 --- a/ethexe/rpc/src/apis/injected/trait.rs +++ b/ethexe/rpc/src/apis/injected/trait.rs @@ -4,8 +4,8 @@ use ethexe_common::{ HashOf, injected::{ - InjectedTransaction, TransactionAcceptance, SignedInjectedTransaction, - SignedTxReceipt, Transaction, + InjectedTransaction, SignedInjectedTransaction, SignedTxReceipt, Transaction, + TransactionAcceptance, }, }; use gear_tdec::bls12_381::DkgPublicKey; diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index 96180dcc14e..dbc4078be0f 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -51,8 +51,8 @@ use apis::{ use ethexe_common::HashOf; #[cfg(feature = "server")] use ethexe_common::injected::{ - InjectedTransaction, TransactionAcceptance, Promise, ShieldedTransaction, - SignedCompactTxReceipt, Transaction, + InjectedTransaction, Promise, ShieldedTransaction, SignedCompactTxReceipt, Transaction, + TransactionAcceptance, }; #[cfg(feature = "server")] use ethexe_db::Database; diff --git a/ethexe/rpc/src/tests.rs b/ethexe/rpc/src/tests.rs index 5eac2d9ec64..61a1a35db1b 100644 --- a/ethexe/rpc/src/tests.rs +++ b/ethexe/rpc/src/tests.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 use crate::{ - CodeClient, InjectedApi, InjectedClient, TransactionAcceptance, RpcConfig, RpcEvent, - RpcServer, RpcService, test_utils::wasm_with_custom_section, + CodeClient, InjectedApi, InjectedClient, RpcConfig, RpcEvent, RpcServer, RpcService, + TransactionAcceptance, test_utils::wasm_with_custom_section, }; use ethexe_common::{ SignedMessage, ValidatorsVec, diff --git a/ethexe/sdk/src/mirror.rs b/ethexe/sdk/src/mirror.rs index c55db1571a1..1331cc9971c 100644 --- a/ethexe/sdk/src/mirror.rs +++ b/ethexe/sdk/src/mirror.rs @@ -9,8 +9,7 @@ use ethexe_common::{ gear::ValueClaim, gear_core::rpc::ReplyInfo, injected::{ - InjectedTransaction, TransactionAcceptance, Promise, Receipt, - SignedInjectedTransaction, + InjectedTransaction, Promise, Receipt, SignedInjectedTransaction, TransactionAcceptance, }, }; use ethexe_ethereum::{ diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 496c19d5ad2..7dc717fc5ea 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -47,7 +47,7 @@ use ethexe_common::{ CodeAndIdUnchecked, PromiseEmissionMode, db::{GlobalsStorageRW, MbStorageRO, OnChainStorageRO}, gear::CodeState, - injected::{CompactPromise, TransactionAcceptance, Receipt}, + injected::{CompactPromise, Receipt, TransactionAcceptance}, malachite::BlockDecryptionData, network::VerifiedValidatorMessage, }; @@ -916,8 +916,7 @@ impl Service { if let Some(malachite) = malachite.as_mut() { let status = malachite.receive_transaction(transaction.clone()); - local_acceptance = - Some(TransactionAcceptance::from(status)); + local_acceptance = Some(TransactionAcceptance::from(status)); } match network.as_mut() { @@ -958,10 +957,9 @@ impl Service { network_injected_txs.insert(tx_hash, pending); } Err(err) => { - let acceptance = - TransactionAcceptance::Reject { - reason: err.to_string(), - }; + let acceptance = TransactionAcceptance::Reject { + reason: err.to_string(), + }; if let Err(err) = response_sender.send(acceptance) { tracing::error!( @@ -1002,15 +1000,10 @@ impl Service { } }, Event::Malachite(event) => match event { - MalachiteEvent::BlockProposal { - height, - mb_hash, - can_speculatively_execute, - } => { + MalachiteEvent::BlockProposal { height, mb_hash } => { tracing::info!( height, mb_hash = %mb_hash, - can_speculatively_execute, "Malachite: BlockProposal", ); // Validators are interested in this MB's @@ -1018,9 +1011,7 @@ impl Service { // service's `PromiseEmissionMode` can still // force the policy to `Enabled` regardless. - if can_speculatively_execute { - compute.compute_mb(mb_hash, ethexe_common::PromisePolicy::Enabled); - } + compute.compute_mb(mb_hash, ethexe_common::PromisePolicy::Enabled); } MalachiteEvent::BlockFinalized { cert, diff --git a/ethexe/service/src/pending_tx.rs b/ethexe/service/src/pending_tx.rs index fb6ed61367f..bd5a505c026 100644 --- a/ethexe/service/src/pending_tx.rs +++ b/ethexe/service/src/pending_tx.rs @@ -35,9 +35,7 @@ impl PendingNetworkInjectedTx { self.response_senders.push(response_sender); } - pub(super) fn into_response_senders( - self, - ) -> Vec> { + pub(super) fn into_response_senders(self) -> Vec> { self.response_senders } diff --git a/ethexe/service/src/tests/mod.rs b/ethexe/service/src/tests/mod.rs index 0d859de2aea..b0a91907a59 100644 --- a/ethexe/service/src/tests/mod.rs +++ b/ethexe/service/src/tests/mod.rs @@ -23,7 +23,7 @@ use ethexe_common::{ }, gear::BatchCommitment, injected::{ - InjectedTransaction, TransactionAcceptance, Receipt, TransactionHash, + InjectedTransaction, Receipt, TransactionAcceptance, TransactionHash, TransactionPurgedReason, }, mock::*, diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index cad23677b9c..98eea752ebf 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -11,9 +11,7 @@ use ethexe_common::{ Address, SimpleBlockData, db::*, events::BlockEvent, - injected::{ - TransactionAcceptance, SignedCompactTxReceipt, Transaction, TransactionHash, - }, + injected::{SignedCompactTxReceipt, Transaction, TransactionAcceptance, TransactionHash}, malachite::SignedBlockDecryptionShares, network::VerifiedValidatorMessage, }; From 1f8755d5deb823af6d29871c11f360502cb9aa03 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 24 Jun 2026 19:52:57 +0300 Subject: [PATCH 36/41] chore: remove remnants of votings extensions from malachite --- Cargo.lock | 1 - ethexe/malachite/core/src/app.rs | 52 ++--------- ethexe/malachite/core/src/codec.rs | 91 ++++--------------- ethexe/malachite/core/src/externalities.rs | 11 +-- .../malachite/core/tests/multi_validators.rs | 12 +-- ethexe/malachite/service/Cargo.toml | 1 - ethexe/malachite/service/src/externalities.rs | 18 +--- .../service/tests/restart_resilience.rs | 13 +-- 8 files changed, 42 insertions(+), 157 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92ae6dc035e..0fa5ed207f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5755,7 +5755,6 @@ dependencies = [ "alloy", "anyhow", "async-trait", - "bytes", "derive_more 2.1.1", "ethexe-common", "ethexe-db", diff --git a/ethexe/malachite/core/src/app.rs b/ethexe/malachite/core/src/app.rs index 8c7a133824b..0be82a1aec7 100644 --- a/ethexe/malachite/core/src/app.rs +++ b/ethexe/malachite/core/src/app.rs @@ -52,7 +52,7 @@ use malachitebft_app_channel::{ }, }, }; -use malachitebft_core_types::{Height as _, VoteExtensions}; +use malachitebft_core_types::Height as _; use parity_scale_codec::{Decode, Encode}; use std::{ops::RangeInclusive, sync::Arc}; use tracing::{error, info}; @@ -209,7 +209,7 @@ where // Finalized (commit + cascade). AppMsg::Finalized { certificate, - extensions, + extensions: _, evidence, reply, } => { @@ -221,7 +221,7 @@ where evidence = ?evidence, "Finalized" ); - let next = match self.process_finalized(certificate, extensions).await { + let next = match self.process_finalized(certificate).await { Ok(()) => { let h = self.state.current_height; Next::Start( @@ -467,13 +467,12 @@ where async fn process_finalized( &mut self, certificate: EngineCert, - extensions: VoteExtensions, ) -> Result<(), FinalizationError> { let (block_bytes, _cert) = self .state .commit(certificate.clone()) .map_err(FinalizationError::NonFatal)?; - self.ingest_finalized(certificate, block_bytes, extensions) + self.ingest_finalized(certificate, block_bytes) .await .context("ingest finalized") .map_err(FinalizationError::Fatal) @@ -669,21 +668,11 @@ where /// [`Store::cascade_finalize`] silently no-ops on an unsaved /// ancestor (the `finalize_chain` walk returns `None`), and the /// `errors_tx` channel surfaces the contract breach upstream. - async fn ingest_finalized( - &self, - cert: EngineCert, - block_bytes: Vec, - extensions: VoteExtensions, - ) -> Result<()> { + async fn ingest_finalized(&self, cert: EngineCert, block_bytes: Vec) -> Result<()> { let block = Block::decode(&mut &block_bytes[..]) .map_err(|e| anyhow!("decoding Block at finalize: {e}"))?; let block_hash = block.hash(); let height = cert.height.as_u64(); - let finalized_extensions: Vec<_> = extensions - .extensions - .into_iter() - .map(|(address, extension)| (address, extension.message)) - .collect(); let app_cert = CommitCertificate { height, @@ -727,12 +716,7 @@ where .store .cascade_finalize(vec![block_hash], |hash, cert| { let ext = Arc::clone(&self.externalities); - let extensions = if hash == block_hash { - finalized_extensions.clone() - } else { - Vec::new() - }; - async move { ext.process_mb_finalized(hash, cert, extensions).await } + async move { ext.process_mb_finalized(hash, cert).await } }) .await?; Ok(()) @@ -804,12 +788,7 @@ mod tests { async fn process_mb_proposal(&self, _: H256, _: Block) -> Result<()> { Ok(()) } - async fn process_mb_finalized( - &self, - _: H256, - _: CommitCertificate, - _: Vec<(Address, Bytes)>, - ) -> Result<()> { + async fn process_mb_finalized(&self, _: H256, _: CommitCertificate) -> Result<()> { Ok(()) } async fn build_block_above(&self, _: H256) -> Result { @@ -992,12 +971,7 @@ mod tests { async fn process_mb_proposal(&self, _: H256, _: Block) -> Result<()> { Ok(()) } - async fn process_mb_finalized( - &self, - _: H256, - _: CommitCertificate, - _: Vec<(Address, Bytes)>, - ) -> Result<()> { + async fn process_mb_finalized(&self, _: H256, _: CommitCertificate) -> Result<()> { Err(anyhow!("application: finalize-side store write failed")) } async fn build_block_above(&self, _: H256) -> Result { @@ -1104,10 +1078,7 @@ mod tests { commit_signatures: Vec::new(), }; - match handler - .process_finalized(cert, VoteExtensions::default()) - .await - { + match handler.process_finalized(cert).await { Err(FinalizationError::Fatal(_)) => { // Expected: app::run propagates the error and the // service tears down rather than silently moving on. @@ -1189,10 +1160,7 @@ mod tests { value_id, commit_signatures: Vec::new(), }; - match handler - .process_finalized(cert, VoteExtensions::default()) - .await - { + match handler.process_finalized(cert).await { Ok(()) => {} Err(FinalizationError::Fatal(e)) => panic!("Fatal: {e:?}"), Err(FinalizationError::NonFatal(e)) => panic!("NonFatal: {e:?}"), diff --git a/ethexe/malachite/core/src/codec.rs b/ethexe/malachite/core/src/codec.rs index 1a83b9651be..859f82ddcc2 100644 --- a/ethexe/malachite/core/src/codec.rs +++ b/ethexe/malachite/core/src/codec.rs @@ -25,8 +25,8 @@ use malachitebft_codec::{Codec, HasEncodedLen}; use malachitebft_core_consensus::{LivenessMsg, ProposedValue, SignedConsensusMsg}; use malachitebft_core_types::{ CommitCertificate, CommitSignature, NilOrVal, PolkaCertificate, PolkaSignature, Round, - RoundCertificate, RoundCertificateType, RoundSignature, SignedExtension, SignedMessage, - SignedProposal, SignedVote, ValidatorProof, Validity, VoteType, + RoundCertificate, RoundCertificateType, RoundSignature, SignedProposal, SignedVote, + ValidatorProof, Validity, VoteType, }; use malachitebft_engine::util::streaming::{StreamContent, StreamMessage}; use malachitebft_sync::{ @@ -228,57 +228,19 @@ struct RawSignedMessage { signature: RawSignature, } -#[derive(Encode, Decode)] -struct RawSignedVote { - message: Vec, - signature: RawSignature, - extension: Option, -} - -#[derive(Encode, Decode)] -struct RawSignedExtension { - message: Vec, - signature: RawSignature, -} - -impl From> for RawSignedExtension { - fn from(value: SignedExtension) -> Self { - Self { - message: value.message.to_vec(), - signature: RawSignature::from(&value.signature), - } - } -} - -impl TryFrom for SignedExtension { - type Error = CodecError; - - fn try_from(value: RawSignedExtension) -> Result { - Ok(SignedMessage::new( - Bytes::from(value.message), - Signature::try_from(value.signature)?, - )) - } -} - #[derive(Encode, Decode)] enum RawSignedConsensusMsg { - Vote(RawSignedVote), + Vote(RawSignedMessage), Proposal(RawSignedMessage), } impl From> for RawSignedConsensusMsg { fn from(value: SignedConsensusMsg) -> Self { match value { - SignedConsensusMsg::Vote(vote) => { - let mut message = vote.message; - let extension = message.extension.take().map(RawSignedExtension::from); - Self::Vote(RawSignedVote { - message: message.to_sign_bytes().to_vec(), - signature: RawSignature::from(&vote.signature), - extension, - }) - } + SignedConsensusMsg::Vote(vote) => Self::Vote(RawSignedMessage { + message: vote.message.to_sign_bytes().to_vec(), + signature: RawSignature::from(&vote.signature), + }), SignedConsensusMsg::Proposal(proposal) => Self::Proposal(RawSignedMessage { message: proposal.message.to_sign_bytes().to_vec(), signature: RawSignature::from(&proposal.signature), @@ -291,14 +253,10 @@ impl TryFrom for SignedConsensusMsg { type Error = CodecError; fn try_from(value: RawSignedConsensusMsg) -> Result { match value { - RawSignedConsensusMsg::Vote(raw) => { - let mut message = Vote::from_sign_bytes(&raw.message)?; - message.extension = raw.extension.map(SignedExtension::try_from).transpose()?; - Ok(SignedConsensusMsg::Vote(SignedVote { - message, - signature: Signature::try_from(raw.signature)?, - })) - } + RawSignedConsensusMsg::Vote(raw) => Ok(SignedConsensusMsg::Vote(SignedVote { + message: Vote::from_sign_bytes(&raw.message)?, + signature: Signature::try_from(raw.signature)?, + })), RawSignedConsensusMsg::Proposal(raw) => { Ok(SignedConsensusMsg::Proposal(SignedProposal { message: Proposal::from_sign_bytes(&raw.message)?, @@ -586,7 +544,7 @@ struct RawRoundCertificate { #[derive(Encode, Decode)] enum RawLivenessMsg { - Vote(RawSignedVote), + Vote(RawSignedMessage), PolkaCertificate(RawPolkaCertificate), SkipRoundCertificate(RawRoundCertificate), } @@ -594,15 +552,10 @@ enum RawLivenessMsg { impl From> for RawLivenessMsg { fn from(value: LivenessMsg) -> Self { match value { - LivenessMsg::Vote(vote) => { - let mut message = vote.message; - let extension = message.extension.take().map(RawSignedExtension::from); - Self::Vote(RawSignedVote { - message: message.to_sign_bytes().to_vec(), - signature: RawSignature::from(&vote.signature), - extension, - }) - } + LivenessMsg::Vote(vote) => Self::Vote(RawSignedMessage { + message: vote.message.to_sign_bytes().to_vec(), + signature: RawSignature::from(&vote.signature), + }), LivenessMsg::PolkaCertificate(polka) => Self::PolkaCertificate(RawPolkaCertificate { height: polka.height.as_u64(), round: round_to_i64(polka.round), @@ -641,14 +594,10 @@ impl TryFrom for LivenessMsg { type Error = CodecError; fn try_from(value: RawLivenessMsg) -> Result { Ok(match value { - RawLivenessMsg::Vote(raw) => { - let mut message = Vote::from_sign_bytes(&raw.message)?; - message.extension = raw.extension.map(SignedExtension::try_from).transpose()?; - LivenessMsg::Vote(SignedVote { - message, - signature: Signature::try_from(raw.signature)?, - }) - } + RawLivenessMsg::Vote(raw) => LivenessMsg::Vote(SignedVote { + message: Vote::from_sign_bytes(&raw.message)?, + signature: Signature::try_from(raw.signature)?, + }), RawLivenessMsg::PolkaCertificate(cert) => { let mut polka_signatures = Vec::with_capacity(cert.polka_signatures.len()); for s in cert.polka_signatures { diff --git a/ethexe/malachite/core/src/externalities.rs b/ethexe/malachite/core/src/externalities.rs index 4d02ef323cc..1908ec5766f 100644 --- a/ethexe/malachite/core/src/externalities.rs +++ b/ethexe/malachite/core/src/externalities.rs @@ -3,11 +3,9 @@ //! Application callbacks the service makes to the outside world. +use crate::types::{Block, BlockPayload, CommitCertificate, H256}; use anyhow::Result; use async_trait::async_trait; -use bytes::Bytes; - -use crate::types::{Address, Block, BlockPayload, CommitCertificate, H256}; /// Application-side callbacks the consensus service requires. /// @@ -54,12 +52,7 @@ pub trait Externalities: Send + Sync + 'static { /// `cert` is the BFT commit certificate for the height of /// `mb_hash`. The application typically forwards `cert` to /// downstream layers (on-chain commits, light clients, etc.). - async fn process_mb_finalized( - &self, - mb_hash: H256, - cert: CommitCertificate, - extensions: Vec<(Address, Bytes)>, - ) -> Result<()>; + async fn process_mb_finalized(&self, mb_hash: H256, cert: CommitCertificate) -> Result<()>; /// Build a fresh block payload whose parent has hash /// `parent_mb_hash`. Called only when this node has been elected diff --git a/ethexe/malachite/core/tests/multi_validators.rs b/ethexe/malachite/core/tests/multi_validators.rs index 0a464e60741..e1a744dd89b 100644 --- a/ethexe/malachite/core/tests/multi_validators.rs +++ b/ethexe/malachite/core/tests/multi_validators.rs @@ -35,10 +35,9 @@ fn init_tracing() { use anyhow::Result; use async_trait::async_trait; -use bytes::Bytes; use ethexe_malachite_core::{ - Address, Block, BlockPayload, CommitCertificate, Externalities, H256, MalachiteConfig, - MalachiteService, Multiaddr, NodeRole, ValidatorEntry, libp2p_peer_id, + Block, BlockPayload, CommitCertificate, Externalities, H256, MalachiteConfig, MalachiteService, + Multiaddr, NodeRole, ValidatorEntry, libp2p_peer_id, }; use proptest::prelude::*; use tempfile::TempDir; @@ -125,12 +124,7 @@ impl Externalities for TestExt { Ok(()) } - async fn process_mb_finalized( - &self, - hash: H256, - cert: CommitCertificate, - _: Vec<(Address, Bytes)>, - ) -> Result<()> { + async fn process_mb_finalized(&self, hash: H256, cert: CommitCertificate) -> Result<()> { let mut s = self.state.lock().unwrap(); if cert.block_hash != hash { s.violations diff --git a/ethexe/malachite/service/Cargo.toml b/ethexe/malachite/service/Cargo.toml index d6c4ecc725f..cbd76a75ccf 100644 --- a/ethexe/malachite/service/Cargo.toml +++ b/ethexe/malachite/service/Cargo.toml @@ -12,7 +12,6 @@ repository.workspace = true alloy = { workspace = true, features = ["eips"] } anyhow.workspace = true async-trait.workspace = true -bytes.workspace = true derive_more.workspace = true futures.workspace = true parity-scale-codec.workspace = true diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index d86804e3b20..b7dced13535 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -46,7 +46,6 @@ use crate::{ }; use anyhow::{Context, Result, anyhow, bail}; use async_trait::async_trait; -use bytes::Bytes; use ethexe_common::{ HashOf, MAX_TOUCHED_PROGRAMS_PER_MB, SimpleBlockData, VerifiedData, db::{CompactMb, GlobalsStorageRO, GlobalsStorageRW, MbStorageRO, MbStorageRW}, @@ -60,9 +59,7 @@ use ethexe_common::{ }, }; use ethexe_db::Database; -use ethexe_malachite_core::{ - Address as MalachiteAddress, Block, BlockPayload, Externalities, MAX_BLOCK_PAYLOAD_BYTES, -}; +use ethexe_malachite_core::{Block, BlockPayload, Externalities, MAX_BLOCK_PAYLOAD_BYTES}; use gear_tdec::bls12_381::{ DecryptionShareSimple, SharedSecret, prepare_combine_simple, share_combine_simple, }; @@ -245,7 +242,6 @@ impl Externalities for EthexeExternalities { &self, mb_hash: H256, cert: ethexe_malachite_core::CommitCertificate, - _extensions: Vec<(MalachiteAddress, Bytes)>, ) -> Result<()> { let compact = self.db.mb_compact_block(mb_hash).ok_or_else(|| { anyhow!( @@ -1445,7 +1441,7 @@ mod tests { let mb_hash = block.hash(); ext.process_mb_proposal(mb_hash, block).await.unwrap(); let _ = rx.recv().await; // BlockProposal - ext.process_mb_finalized(mb_hash, fake_cert(1), Vec::new()) + ext.process_mb_finalized(mb_hash, fake_cert(1)) .await .unwrap(); assert_eq!(db.globals().latest_finalized_mb_hash, mb_hash); @@ -1483,7 +1479,7 @@ mod tests { let mb_hash = block.hash(); ext_a.process_mb_proposal(mb_hash, block).await.unwrap(); ext_a - .process_mb_finalized(mb_hash, fake_cert(i), Vec::new()) + .process_mb_finalized(mb_hash, fake_cert(i)) .await .unwrap(); chain.push((mb_hash, p)); @@ -1515,10 +1511,7 @@ mod tests { let mb4 = block4.hash(); ext_b.process_mb_proposal(mb4, block4).await.unwrap(); let _ = rx_b.recv().await; // proposal - ext_b - .process_mb_finalized(mb4, fake_cert(4), Vec::new()) - .await - .unwrap(); + ext_b.process_mb_finalized(mb4, fake_cert(4)).await.unwrap(); assert_eq!(db.mb_compact_block(mb4).unwrap().parent, last_pre); assert_eq!(db.globals().latest_finalized_mb_hash, mb4); } @@ -1544,7 +1537,7 @@ mod tests { let block = wrap(p.clone(), height, parent); let mb_hash = block.hash(); ext.process_mb_proposal(mb_hash, block).await.unwrap(); - ext.process_mb_finalized(mb_hash, fake_cert(height), Vec::new()) + ext.process_mb_finalized(mb_hash, fake_cert(height)) .await .unwrap(); chain.push(mb_hash); @@ -1865,7 +1858,6 @@ mod tests { block_hash: mb_hash, signatures: vec![], }, - Vec::new(), ) .await .unwrap(); diff --git a/ethexe/malachite/service/tests/restart_resilience.rs b/ethexe/malachite/service/tests/restart_resilience.rs index 8b7199339db..ca96768ad8d 100644 --- a/ethexe/malachite/service/tests/restart_resilience.rs +++ b/ethexe/malachite/service/tests/restart_resilience.rs @@ -142,14 +142,6 @@ fn build_tdec_setup(pub_key: gsigner::schemes::secp256k1::PublicKey) -> Validato } } -fn free_tcp_port() -> u16 { - std::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)) - .expect("bind ephemeral test port") - .local_addr() - .expect("read ephemeral test port") - .port() -} - /// Build the MalachiteConfig used by the resilience tests: /// quarantine-off (so the producer can advance immediately on each /// new chain head), ephemeral listen port, no persistent peers, @@ -254,10 +246,9 @@ async fn single_validator_finalizes_and_recovers_after_restart() { let (signer, pub_key) = build_signer(home.path()); let tdec_setup = build_tdec_setup(pub_key); - let listen_port = free_tcp_port(); // ---- first run ------------------------------------------------- let mut svc = MalachiteService::new( - build_config(home.path(), listen_port, pub_key), + build_config(home.path(), 30_001, pub_key), db.clone(), signer.clone(), Some(pub_key), @@ -300,7 +291,7 @@ async fn single_validator_finalizes_and_recovers_after_restart() { // ---- second run on the SAME home dir + DB ---------------------- let mut svc2 = MalachiteService::new( - build_config(home.path(), listen_port, pub_key), + build_config(home.path(), 30_001, pub_key), db.clone(), signer, Some(pub_key), From cb37441dc018bbec5139274264588b598bb85c4e Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Fri, 26 Jun 2026 15:03:46 +0300 Subject: [PATCH 37/41] feat: add decryption share verification --- Cargo.lock | 20 +- .../service/src/decryption_shares.rs | 213 ++++++++++++++---- ethexe/malachite/service/src/externalities.rs | 33 ++- 3 files changed, 191 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0fa5ed207f3..9129be0029a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5544,7 +5544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6233,7 +6233,7 @@ dependencies = [ [[package]] name = "ferveo-gear-common" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#76a41689d406724dad41dcff1cc62b383150fa31" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#f10902a443a305ff240a00b7d43d117696becf1d" dependencies = [ "ark-ec 0.5.0", "ark-serialize 0.5.0", @@ -6250,7 +6250,7 @@ dependencies = [ [[package]] name = "ferveo-gear-tdec" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#76a41689d406724dad41dcff1cc62b383150fa31" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#f10902a443a305ff240a00b7d43d117696becf1d" dependencies = [ "ark-bls12-381 0.5.0", "ark-ec 0.5.0", @@ -9672,7 +9672,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -15632,7 +15632,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -15645,7 +15645,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -15748,7 +15748,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]] @@ -15769,7 +15769,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 1.0.5", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -18880,7 +18880,7 @@ dependencies = [ [[package]] name = "subproductdomain-gear" version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#76a41689d406724dad41dcff1cc62b383150fa31" +source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#f10902a443a305ff240a00b7d43d117696becf1d" dependencies = [ "anyhow", "ark-ec 0.5.0", @@ -19356,7 +19356,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/ethexe/malachite/service/src/decryption_shares.rs b/ethexe/malachite/service/src/decryption_shares.rs index 45b4707e426..e7387479a8a 100644 --- a/ethexe/malachite/service/src/decryption_shares.rs +++ b/ethexe/malachite/service/src/decryption_shares.rs @@ -5,7 +5,7 @@ use ethexe_common::{Address, HashOf, injected::ShieldedTransaction}; use gprimitives::H256; -use gsigner::DecryptionShare; +use gsigner::{DecryptionShare, PublicDecryptionContext}; use std::{collections::HashMap, sync::Mutex}; use tokio::sync::Notify; @@ -17,13 +17,12 @@ pub(crate) enum InsertOutcome { Inserted, Duplicate, Equivocation, + InvalidShare, UnknownBlock, UnknownTransaction, } /// Decryption shares grouped by MB, shielded transaction, and validator. -/// -/// Shares are verified before reaching this store. pub(crate) struct DecryptionSharesStore { inner: Mutex>, changed: Notify, @@ -57,15 +56,24 @@ impl DecryptionSharesStore { .or_insert(transactions); } - /// Insert a share whose transaction membership and cryptographic proof - /// have already been checked. + /// Insert a share after checking transaction membership and cryptographic proof. pub(crate) fn insert( &self, mb_hash: H256, tx_hash: ShieldedTxHash, validator: Address, + validator_context: &PublicDecryptionContext, + transaction: &ShieldedTransaction, share: DecryptionShare, ) -> InsertOutcome { + if !share.verify( + &validator_context.blinded_key_share.blinded_key_share, + &validator_context.validator_public_key.encryption_key, + &transaction.ciphertext, + ) { + return InsertOutcome::InvalidShare; + } + let mut blocks = self.inner.lock().expect("decryption shares poisoned"); let Some(block) = blocks.get_mut(&mb_hash) else { return InsertOutcome::UnknownBlock; @@ -73,7 +81,6 @@ impl DecryptionSharesStore { let Some(shares) = block.get_mut(&tx_hash) else { return InsertOutcome::UnknownTransaction; }; - let outcome = match shares.get(&validator) { Some(existing) if existing == &share => InsertOutcome::Duplicate, Some(_) => InsertOutcome::Equivocation, @@ -137,24 +144,49 @@ impl DecryptionSharesStore { #[cfg(test)] mod tests { use super::*; + use ethexe_common::injected::InjectedTransaction; use gear_tdec::{bls12_381::E, rand_utils::Rng}; + use gprimitives::ActorId; - fn shares() -> (DecryptionShare, DecryptionShare) { + struct ShareFixture { + transaction: ShieldedTransaction, + tx_hash: ShieldedTxHash, + first_context: PublicDecryptionContext, + second_context: PublicDecryptionContext, + first_share: DecryptionShare, + second_share: DecryptionShare, + } + + fn share_fixture() -> ShareFixture { let mut rng = gear_tdec::rand_utils::test_rng(); let dealer = gear_tdec::deal::(3, 2, &mut rng); - let plaintext = rng.r#gen::<[u8; 32]>(); - let ciphertext = - gear_tdec::encrypt_raw::(&plaintext, b"aad", &dealer.public_key, &mut rng) - .expect("encryption succeeds"); - let header = ciphertext.header(); - ( - dealer.private_contexts[0] - .create_share(&header, b"aad") - .expect("share creation succeeds"), - dealer.private_contexts[1] - .create_share(&header, b"aad") - .expect("share creation succeeds"), - ) + let transaction = InjectedTransaction { + destination: ActorId::from([1; 32]), + payload: rng.r#gen::<[u8; 32]>().to_vec().try_into().unwrap(), + value: 0, + reference_block: H256::random(), + salt: rng.r#gen::<[u8; 32]>().to_vec().try_into().unwrap(), + } + .shield(&dealer.public_key, &mut rng) + .expect("shielding succeeds"); + let tx_hash = transaction.to_hash(); + let header = transaction.ciphertext.header(); + let aad = transaction.aad.as_ref(); + let first_share = dealer.private_contexts[0] + .create_share(&header, aad) + .expect("share creation succeeds"); + let second_share = dealer.private_contexts[1] + .create_share(&header, aad) + .expect("share creation succeeds"); + + ShareFixture { + transaction, + tx_hash, + first_context: dealer.private_contexts[0].public_decryption_contexts[0].clone(), + second_context: dealer.private_contexts[1].public_decryption_contexts[1].clone(), + first_share, + second_share, + } } fn random_tx_hash() -> ShieldedTxHash { @@ -169,23 +201,39 @@ mod tests { async fn insertion_is_idempotent_and_notifies() { let store = DecryptionSharesStore::new(); let mb_hash = H256::random(); - let tx_hash = random_tx_hash(); - let (share, _) = shares(); - store.register_block(mb_hash, [tx_hash]); + let fixture = share_fixture(); + store.register_block(mb_hash, [fixture.tx_hash]); assert_eq!( - store.insert(mb_hash, tx_hash, validator(1), share.clone()), + store.insert( + mb_hash, + fixture.tx_hash, + validator(1), + &fixture.first_context, + &fixture.transaction, + fixture.first_share.clone() + ), InsertOutcome::Inserted ); tokio::time::timeout(std::time::Duration::from_millis(10), store.notified()) .await .expect("insert notification is retained"); assert_eq!( - store.insert(mb_hash, tx_hash, validator(1), share), + store.insert( + mb_hash, + fixture.tx_hash, + validator(1), + &fixture.first_context, + &fixture.transaction, + fixture.first_share + ), InsertOutcome::Duplicate ); assert_eq!( - store.threshold_shares(mb_hash, tx_hash, 1).unwrap().len(), + store + .threshold_shares(mb_hash, fixture.tx_hash, 1) + .unwrap() + .len(), 1 ); } @@ -194,45 +242,101 @@ mod tests { fn threshold_query_is_deterministic_and_limited() { let store = DecryptionSharesStore::new(); let mb_hash = H256::random(); - let tx_hash = random_tx_hash(); - let (first_share, second_share) = shares(); - store.register_block(mb_hash, [tx_hash]); - store.insert(mb_hash, tx_hash, validator(2), second_share); + let fixture = share_fixture(); + store.register_block(mb_hash, [fixture.tx_hash]); + store.insert( + mb_hash, + fixture.tx_hash, + validator(2), + &fixture.second_context, + &fixture.transaction, + fixture.second_share, + ); - assert!(store.threshold_shares(mb_hash, tx_hash, 2).is_none()); + assert!( + store + .threshold_shares(mb_hash, fixture.tx_hash, 2) + .is_none() + ); - store.insert(mb_hash, tx_hash, validator(1), first_share); + store.insert( + mb_hash, + fixture.tx_hash, + validator(1), + &fixture.first_context, + &fixture.transaction, + fixture.first_share, + ); let shares = store - .threshold_shares(mb_hash, tx_hash, 1) + .threshold_shares(mb_hash, fixture.tx_hash, 1) .expect("threshold reached"); assert_eq!(shares.len(), 1); assert_eq!(shares[0].0, validator(1)); } #[test] - fn rejects_unknown_entries_and_equivocation() { + fn rejects_unknown_entries_and_invalid_shares() { let store = DecryptionSharesStore::new(); let mb_hash = H256::random(); - let tx_hash = random_tx_hash(); + let fixture = share_fixture(); let other_tx_hash = random_tx_hash(); - let (share, conflicting_share) = shares(); assert_eq!( - store.insert(mb_hash, tx_hash, validator(1), share.clone()), + store.insert( + mb_hash, + fixture.tx_hash, + validator(1), + &fixture.first_context, + &fixture.transaction, + fixture.first_share.clone() + ), InsertOutcome::UnknownBlock ); - store.register_block(mb_hash, [tx_hash]); + store.register_block(mb_hash, [fixture.tx_hash]); assert_eq!( - store.insert(mb_hash, other_tx_hash, validator(1), share.clone()), + store.insert( + mb_hash, + other_tx_hash, + validator(1), + &fixture.first_context, + &fixture.transaction, + fixture.first_share.clone() + ), InsertOutcome::UnknownTransaction ); assert_eq!( - store.insert(mb_hash, tx_hash, validator(1), share), + store.insert( + mb_hash, + fixture.tx_hash, + validator(1), + &fixture.first_context, + &fixture.transaction, + fixture.first_share + ), InsertOutcome::Inserted ); assert_eq!( - store.insert(mb_hash, tx_hash, validator(1), conflicting_share), - InsertOutcome::Equivocation + store.insert( + mb_hash, + fixture.tx_hash, + validator(2), + &fixture.first_context, + &fixture.transaction, + fixture.second_share + ), + InsertOutcome::InvalidShare + ); + assert!( + store + .threshold_shares(mb_hash, fixture.tx_hash, 2) + .is_none() + ); + assert_eq!( + store + .threshold_shares(mb_hash, fixture.tx_hash, 1) + .unwrap() + .len(), + 1 ); } @@ -241,19 +345,32 @@ mod tests { let store = DecryptionSharesStore::new(); let finalized = H256::random(); let sibling = H256::random(); - let tx_hash = random_tx_hash(); - let (share, _) = shares(); - store.register_block(finalized, [tx_hash]); - store.register_block(sibling, [tx_hash]); + let fixture = share_fixture(); + store.register_block(finalized, [fixture.tx_hash]); + store.register_block(sibling, [fixture.tx_hash]); assert_eq!( - store.insert(sibling, tx_hash, validator(1), share.clone()), + store.insert( + sibling, + fixture.tx_hash, + validator(1), + &fixture.first_context, + &fixture.transaction, + fixture.first_share.clone() + ), InsertOutcome::Inserted ); store.retain_block(finalized); assert_eq!( - store.insert(sibling, tx_hash, validator(1), share), + store.insert( + sibling, + fixture.tx_hash, + validator(1), + &fixture.first_context, + &fixture.transaction, + fixture.first_share + ), InsertOutcome::UnknownBlock ); } diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index b7dced13535..901712b69fa 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -970,27 +970,21 @@ impl EthexeExternalities { ); continue; }; - if !message_share.share.verify( - &participant_context.blinded_key_share.blinded_key_share, - &participant_context.validator_public_key.encryption_key, - &transaction.ciphertext, - ) { - debug!( - %sender, - mb_hash = %data.mb_hash, - tx_hash = %message_share.tx_hash.inner(), - "ignoring invalid decryption share", - ); - continue; - } - match self.decryption_shares.insert( data.mb_hash, message_share.tx_hash, sender, + participant_context, + transaction, message_share.share.clone(), ) { InsertOutcome::Inserted | InsertOutcome::Duplicate => {} + InsertOutcome::InvalidShare => debug!( + %sender, + mb_hash = %data.mb_hash, + tx_hash = %message_share.tx_hash.inner(), + "ignoring invalid decryption share", + ), InsertOutcome::Equivocation => warn!( %sender, mb_hash = %data.mb_hash, @@ -1023,9 +1017,14 @@ impl EthexeExternalities { continue; }; let tx_hash = tx.to_hash(); - let outcome = - self.decryption_shares - .insert(mb_hash, tx_hash, my_address, share.clone()); + let outcome = self.decryption_shares.insert( + mb_hash, + tx_hash, + my_address, + tdec_ctx, + tx, + share.clone(), + ); debug_assert!(matches!( outcome, InsertOutcome::Inserted | InsertOutcome::Duplicate From fc23d4f78c7b40897c32d4628f4d0e408ae2a5f2 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Mon, 29 Jun 2026 17:55:11 +0400 Subject: [PATCH 38/41] chote: switch again to voting extension --- ethexe/malachite/core/src/app.rs | 86 ++++++++- ethexe/malachite/core/src/codec.rs | 127 +++++++++++- ethexe/malachite/core/src/context.rs | 32 +++- ethexe/malachite/core/src/externalities.rs | 16 +- ethexe/malachite/core/src/lib.rs | 5 +- ethexe/malachite/core/src/types.rs | 15 ++ ethexe/malachite/service/src/externalities.rs | 180 +++++++++--------- ethexe/malachite/service/src/service.rs | 8 - ethexe/malachite/service/src/types.rs | 12 -- .../service/tests/restart_resilience.rs | 3 - ethexe/network/src/gossipsub.rs | 15 +- ethexe/network/src/lib.rs | 16 +- ethexe/network/src/validator/topic.rs | 58 ------ ethexe/service/src/lib.rs | 26 --- ethexe/service/src/tests/utils/events.rs | 3 - 15 files changed, 348 insertions(+), 254 deletions(-) diff --git a/ethexe/malachite/core/src/app.rs b/ethexe/malachite/core/src/app.rs index 0dcda2fef76..f4b7c96a695 100644 --- a/ethexe/malachite/core/src/app.rs +++ b/ethexe/malachite/core/src/app.rs @@ -35,9 +35,9 @@ use crate::{ state::State, store::BlockEntry, streaming::ProposalParts, - types::{Address, Block, CommitCertificate, H256}, + types::{Address, Block, CommitCertificate, EthexeVoteExtension, H256}, }; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Context as _, Result, anyhow, ensure}; use bytes::Bytes; use ethexe_common::Acceptance; use malachitebft_app_channel::{ @@ -195,14 +195,39 @@ where } // Vote extensions. - AppMsg::ExtendVote { reply, .. } => { + AppMsg::ExtendVote { + height, + round, + value_id, + reply, + } => { + let extension = self + .process_extend_vote(height, round, value_id) + .await + .unwrap_or_else(|e| { + error!(%height, %round, ?e, "ExtendVote: process failed"); + None + }); reply - .send(self.process_extend_vote()) + .send(extension) .map_err(|e| anyhow!("failed to send ExtendVote reply: {e:?}"))?; } - AppMsg::VerifyVoteExtension { reply, .. } => { + AppMsg::VerifyVoteExtension { + height, + round, + value_id, + extension, + reply, + } => { + let result = self + .process_verify_vote_extension(height, round, value_id, &extension) + .await + .unwrap_or_else(|e| { + warn!(%height, %round, ?e, "VerifyVoteExtension: process failed"); + Err(VoteExtensionError::InvalidVoteExtension) + }); reply - .send(self.process_verify_vote_extension()) + .send(result) .map_err(|e| anyhow!("failed to send VerifyVoteExtension reply: {e:?}"))?; } @@ -426,12 +451,53 @@ where Ok(locally) } - fn process_extend_vote(&self) -> Option { - None + async fn process_extend_vote( + &self, + height: Height, + _round: Round, + value_id: ValueId, + ) -> Result> { + let mb_hash = self.mb_hash_for_value(height, &value_id)?; + self.externalities.extend_vote(mb_hash).await } - fn process_verify_vote_extension(&self) -> Result<(), VoteExtensionError> { - Ok(()) + async fn process_verify_vote_extension( + &self, + height: Height, + _round: Round, + value_id: ValueId, + extension: &EthexeVoteExtension, + ) -> Result> { + let mb_hash = self.mb_hash_for_value(height, &value_id)?; + Ok( + match self + .externalities + .verify_vote_extension(mb_hash, extension) + .await? + { + Acceptance::Accepted(()) => Ok(()), + Acceptance::Rejected(reason) => { + debug!(%reason, %mb_hash, "rejecting vote extension"); + Err(VoteExtensionError::InvalidVoteExtension) + } + }, + ) + } + + fn mb_hash_for_value(&self, height: Height, value_id: &ValueId) -> Result { + let proposed = self + .state + .store + .get_undecided_proposal_by_value_id(value_id)? + .context("vote extension refers to an unknown proposal")?; + ensure!( + proposed.height == height, + "vote extension value belongs to height {}, not {height}", + proposed.height + ); + let block = Block::decode(&mut &proposed.value.block_bytes[..]) + .context("decoding vote extension block")?; + Ok(block.hash()) } // TODO: #5475 add per-peer token-bucket rate limit before `ingest_proposal_part` diff --git a/ethexe/malachite/core/src/codec.rs b/ethexe/malachite/core/src/codec.rs index 859f82ddcc2..e053297a9ac 100644 --- a/ethexe/malachite/core/src/codec.rs +++ b/ethexe/malachite/core/src/codec.rs @@ -25,8 +25,8 @@ use malachitebft_codec::{Codec, HasEncodedLen}; use malachitebft_core_consensus::{LivenessMsg, ProposedValue, SignedConsensusMsg}; use malachitebft_core_types::{ CommitCertificate, CommitSignature, NilOrVal, PolkaCertificate, PolkaSignature, Round, - RoundCertificate, RoundCertificateType, RoundSignature, SignedProposal, SignedVote, - ValidatorProof, Validity, VoteType, + RoundCertificate, RoundCertificateType, RoundSignature, SignedMessage, SignedProposal, + SignedVote, ValidatorProof, Validity, VoteType, }; use malachitebft_engine::util::streaming::{StreamContent, StreamMessage}; use malachitebft_sync::{ @@ -36,7 +36,7 @@ use malachitebft_sync::{ use crate::{ context::{Height, MalachiteCtx, Proposal, ProposalPart, Value, ValueId, Vote}, signing::{Signature, signature_from_vec, signature_to_vec}, - types::Address, + types::{Address, EthexeVoteExtension}, }; /// SCALE codec for malachite wire types. Zero-sized handle. @@ -228,18 +228,46 @@ struct RawSignedMessage { signature: RawSignature, } +#[derive(Encode)] +struct RawSignedVote { + message: Vec, + signature: RawSignature, + extension: Option<(EthexeVoteExtension, RawSignature)>, +} + +impl Decode for RawSignedVote { + fn decode(input: &mut I) -> Result { + let message = Vec::::decode(input)?; + let signature = RawSignature::decode(input)?; + let extension = match input.remaining_len()? { + // Votes persisted before vote-extension support end after the base signature. + Some(0) => None, + _ => Option::decode(input)?, + }; + Ok(Self { + message, + signature, + extension, + }) + } +} + #[derive(Encode, Decode)] enum RawSignedConsensusMsg { - Vote(RawSignedMessage), + Vote(RawSignedVote), Proposal(RawSignedMessage), } impl From> for RawSignedConsensusMsg { fn from(value: SignedConsensusMsg) -> Self { match value { - SignedConsensusMsg::Vote(vote) => Self::Vote(RawSignedMessage { + SignedConsensusMsg::Vote(vote) => Self::Vote(RawSignedVote { message: vote.message.to_sign_bytes().to_vec(), signature: RawSignature::from(&vote.signature), + extension: vote + .message + .extension + .map(|extension| (extension.message, RawSignature::from(&extension.signature))), }), SignedConsensusMsg::Proposal(proposal) => Self::Proposal(RawSignedMessage { message: proposal.message.to_sign_bytes().to_vec(), @@ -253,10 +281,20 @@ impl TryFrom for SignedConsensusMsg { type Error = CodecError; fn try_from(value: RawSignedConsensusMsg) -> Result { match value { - RawSignedConsensusMsg::Vote(raw) => Ok(SignedConsensusMsg::Vote(SignedVote { - message: Vote::from_sign_bytes(&raw.message)?, - signature: Signature::try_from(raw.signature)?, - })), + RawSignedConsensusMsg::Vote(raw) => { + let mut message = Vote::from_sign_bytes(&raw.message)?; + message.extension = match raw.extension { + Some((extension, signature)) => Some(SignedMessage::new( + extension, + Signature::try_from(signature)?, + )), + None => None, + }; + Ok(SignedConsensusMsg::Vote(SignedVote { + message, + signature: Signature::try_from(raw.signature)?, + })) + } RawSignedConsensusMsg::Proposal(raw) => { Ok(SignedConsensusMsg::Proposal(SignedProposal { message: Proposal::from_sign_bytes(&raw.message)?, @@ -792,6 +830,11 @@ mod tests { use crate::signing::{MalachiteSigner, private_key_from_bytes}; use proptest::prelude::*; + #[derive(Encode)] + enum LegacyRawSignedConsensusMsg { + Vote(RawSignedMessage), + } + #[test] fn value_round_trip() { let v = Value::new(b"hello".to_vec()); @@ -800,6 +843,72 @@ mod tests { assert_eq!(v, back); } + #[test] + fn signed_vote_round_trip_preserves_extension() { + let mut bytes = [0u8; 32]; + bytes[31] = 9; + let signer = MalachiteSigner::new(private_key_from_bytes(&bytes).unwrap()); + let address = Address::from_public_key(&signer.public_key()); + let extension = EthexeVoteExtension { + sender: address.0, + shares: Vec::new(), + }; + let mut vote = Vote::new_precommit( + Height::new(3), + Round::new(1), + NilOrVal::Val(ValueId([7; 32])), + address, + ); + vote.extension = Some(SignedMessage::new( + extension.clone(), + signer.sign(&extension.encode()), + )); + let message = SignedConsensusMsg::Vote(SignedVote::new( + vote.clone(), + signer.sign(&vote.to_sign_bytes()), + )); + + let codec = ScaleCodec; + let encoded = + >>::encode(&codec, &message) + .unwrap(); + let decoded = + >>::decode(&codec, encoded) + .unwrap(); + + assert_eq!(decoded, message); + } + + #[test] + fn decodes_legacy_vote_without_extension() { + let mut bytes = [0u8; 32]; + bytes[31] = 10; + let signer = MalachiteSigner::new(private_key_from_bytes(&bytes).unwrap()); + let address = Address::from_public_key(&signer.public_key()); + let vote = Vote::new_precommit( + Height::new(3), + Round::new(1), + NilOrVal::Val(ValueId([8; 32])), + address, + ); + let legacy = LegacyRawSignedConsensusMsg::Vote(RawSignedMessage { + message: vote.to_sign_bytes().to_vec(), + signature: RawSignature::from(&signer.sign(&vote.to_sign_bytes())), + }); + + let decoded = >>::decode( + &ScaleCodec, + legacy.encode().into(), + ) + .unwrap(); + + let SignedConsensusMsg::Vote(decoded) = decoded else { + panic!("expected vote") + }; + assert_eq!(decoded.message, vote); + assert!(decoded.message.extension.is_none()); + } + #[test] fn liveness_polka_cert_round_trip_preserves_signatures() { let mut bytes = [0u8; 32]; diff --git a/ethexe/malachite/core/src/context.rs b/ethexe/malachite/core/src/context.rs index 85b11a35b0f..9d018dece5a 100644 --- a/ethexe/malachite/core/src/context.rs +++ b/ethexe/malachite/core/src/context.rs @@ -37,7 +37,7 @@ pub use malachitebft_test::Height; use crate::{ signing::{MalachiteSigner, PublicKey, Signature, signature_from_vec, signature_to_vec}, - types::Address, + types::{Address, EthexeVoteExtension}, }; // Address — adopt the foreign trait via our local newtype. @@ -567,7 +567,7 @@ impl Context for MalachiteCtx { type ValidatorSet = ValidatorSet; type Value = Value; type Vote = Vote; - type Extension = Bytes; + type Extension = EthexeVoteExtension; type SigningScheme = K256; type Timeouts = LinearTimeouts; @@ -679,20 +679,22 @@ impl SigningProvider for MalachiteSigner { async fn sign_vote_extension( &self, - extension: Bytes, + extension: EthexeVoteExtension, ) -> Result, SigningError> { - let signature = self.sign(extension.as_ref()); + let signature = self.sign(&extension.encode()); Ok(SignedMessage::new(extension, signature)) } async fn verify_signed_vote_extension( &self, - extension: &Bytes, + extension: &EthexeVoteExtension, signature: &Signature, public_key: &PublicKey, ) -> Result { + let sender = Address::from_public_key(public_key); Ok(VerificationResult::from_bool( - public_key.verify(extension.as_ref(), signature).is_ok(), + sender.0 == extension.sender + && public_key.verify(&extension.encode(), signature).is_ok(), )) } } @@ -867,6 +869,24 @@ mod tests { assert!(signer.verify(&bytes, &sig, &pk)); } + #[tokio::test] + async fn vote_extension_sender_must_match_signer() { + let (pk, signer) = mk_keypair(7); + let (other_pk, _) = mk_keypair(8); + let extension = EthexeVoteExtension { + sender: Address::from_public_key(&other_pk).0, + shares: Vec::new(), + }; + let signature = signer.sign(&extension.encode()); + + let result = signer + .verify_signed_vote_extension(&extension, &signature, &pk) + .await + .unwrap(); + + assert!(result.is_invalid()); + } + #[test] fn proposal_signature_round_trip() { let (pk, signer) = mk_keypair(8); diff --git a/ethexe/malachite/core/src/externalities.rs b/ethexe/malachite/core/src/externalities.rs index ad52163c86c..1f59b308155 100644 --- a/ethexe/malachite/core/src/externalities.rs +++ b/ethexe/malachite/core/src/externalities.rs @@ -3,7 +3,7 @@ //! Application callbacks the service makes to the outside world. -use crate::types::{Block, BlockPayload, CommitCertificate, H256}; +use crate::types::{Block, BlockPayload, CommitCertificate, EthexeVoteExtension, H256}; use anyhow::Result; use async_trait::async_trait; use ethexe_common::Acceptance; @@ -23,6 +23,20 @@ use ethexe_common::Acceptance; /// only after the parent has been finalized. #[async_trait] pub trait Externalities: Send + Sync + 'static { + /// Build this validator's extension for a precommit on `mb_hash`. + async fn extend_vote(&self, _mb_hash: H256) -> Result> { + Ok(None) + } + + /// Validate and ingest an extension attached to a precommit on `mb_hash`. + async fn verify_vote_extension( + &self, + _mb_hash: H256, + _extension: &EthexeVoteExtension, + ) -> Result> { + Ok(Acceptance::Accepted(())) + } + /// Persist `block` indexed by `mb_hash`; called exactly once per hash /// at proposal-assembly time. async fn process_mb_proposal(&self, mb_hash: H256, block: Block) -> Result<()>; diff --git a/ethexe/malachite/core/src/lib.rs b/ethexe/malachite/core/src/lib.rs index 4b0c9c15dbd..30e701d6886 100644 --- a/ethexe/malachite/core/src/lib.rs +++ b/ethexe/malachite/core/src/lib.rs @@ -97,7 +97,10 @@ pub use crate::{ libp2p_keypair_from, libp2p_peer_id, private_key_from_bytes, private_key_from_gsigner, public_key_from_gsigner, }, - types::{Address, Block, BlockPayload, CommitCertificate, H256, MAX_BLOCK_PAYLOAD_BYTES}, + types::{ + Address, Block, BlockPayload, CommitCertificate, EthexeVoteExtension, H256, + MAX_BLOCK_PAYLOAD_BYTES, + }, }; /// Re-exported libp2p PeerId — used by integration tests / operators diff --git a/ethexe/malachite/core/src/types.rs b/ethexe/malachite/core/src/types.rs index 44c6fd40260..736b916ad13 100644 --- a/ethexe/malachite/core/src/types.rs +++ b/ethexe/malachite/core/src/types.rs @@ -22,6 +22,21 @@ pub type BlockPayload = LimitedVec; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct Address(pub gsigner::schemes::secp256k1::Address); +/// Decryption shares attached to a Malachite precommit. +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] +pub struct EthexeVoteExtension { + /// Validator that created and signed this extension. + pub sender: gsigner::Address, + /// Shares for shielded transactions in the voted block. + pub shares: Vec, +} + +impl malachitebft_core_types::Extension for EthexeVoteExtension { + fn size_bytes(&self) -> usize { + self.encoded_size() + } +} + impl Display for Address { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "0x{}", hex::encode(self.0.0)) diff --git a/ethexe/malachite/service/src/externalities.rs b/ethexe/malachite/service/src/externalities.rs index 7ce8234eb5a..00f5b6e7f01 100644 --- a/ethexe/malachite/service/src/externalities.rs +++ b/ethexe/malachite/service/src/externalities.rs @@ -56,13 +56,12 @@ use ethexe_common::{ InjectedTransaction, MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB, PurgedTransaction, ShieldedTransaction, Transaction, TransactionHash, TransactionPurgedReason, }, - malachite::{ - MalachiteTdecContext, Operation, Operations, ShieldedTxDecryptionShare, - SignedBlockDecryptionShares, - }, + malachite::{MalachiteTdecContext, Operation, Operations, ShieldedTxDecryptionShare}, }; use ethexe_db::Database; -use ethexe_malachite_core::{Block, BlockPayload, Externalities, MAX_BLOCK_PAYLOAD_BYTES}; +use ethexe_malachite_core::{ + Block, BlockPayload, EthexeVoteExtension, Externalities, MAX_BLOCK_PAYLOAD_BYTES, +}; use gear_tdec::bls12_381::{ DecryptionShareSimple, SharedSecret, prepare_combine_simple, share_combine_simple, }; @@ -133,6 +132,51 @@ struct UnshieldingOutput { #[async_trait] impl Externalities for EthexeExternalities { + async fn extend_vote(&self, mb_hash: H256) -> Result> { + let Some(context) = self.tdec_ctx.as_ref() else { + return Ok(None); + }; + let compact = self + .db + .mb_compact_block(mb_hash) + .with_context(|| format!("vote extension refers to unknown MB {mb_hash}"))?; + let operations = self + .db + .operations(compact.operations_hash) + .with_context(|| format!("operations for MB {mb_hash} are missing"))?; + let transactions = operations + .iter() + .filter_map(|op| op.as_shielded().map(|signed| signed.data())) + .collect::>(); + if transactions.is_empty() { + return Ok(None); + } + + let my_address = context + .contexts + .iter() + .find_map(|(address, participant)| { + (participant.validator_public_key == context.my_context.validator_public_key) + .then_some(*address) + }) + .context("local TDEC context is absent from validator contexts")?; + let shares = + self.provide_decryption_shares(mb_hash, &context.my_context, my_address, &transactions); + + Ok(Some(EthexeVoteExtension { + sender: my_address, + shares, + })) + } + + async fn verify_vote_extension( + &self, + mb_hash: H256, + extension: &EthexeVoteExtension, + ) -> Result> { + Ok(self.receive_decryption_shares(mb_hash, extension.sender, &extension.shares)) + } + async fn process_mb_proposal(&self, mb_hash: H256, mb: Block) -> Result<()> { let operations = Operations::decode_all(&mut mb.payload.as_ref()) .map_err(|e| anyhow!("decoding Operations from block payload bytes: {e}"))?; @@ -172,25 +216,6 @@ impl Externalities for EthexeExternalities { self.decryption_shares .register_block(mb_hash, shielded_transactions.iter().map(|tx| tx.to_hash())); - if let Some(context) = self.tdec_ctx.as_ref() { - // If this node have TDEC context - try provide shares for shielded transaction in this block. - let tdec_ctx = &context.my_context; - - let maybe_my_address = context.contexts.iter().find_map(|(address, participant)| { - (participant.validator_public_key == tdec_ctx.validator_public_key) - .then_some(*address) - }); - match maybe_my_address { - Some(my_address) => self.provide_decryption_shares( - mb_hash, - tdec_ctx, - my_address, - &shielded_transactions, - ), - None => warn!("local TDEC context is absent from validator contexts"), - } - } - // If decryption keys provided - decrypt shielded transactions and save them to database. if let Some(decryption_keys) = operations.iter().find_map(|op| match op { Operation::DecryptionKeys(keys) => Some(keys.clone()), @@ -775,31 +800,28 @@ impl EthexeExternalities { Ok(Some(keys)) } - pub(crate) fn receive_decryption_shares(&self, signed: SignedBlockDecryptionShares) { + fn receive_decryption_shares( + &self, + mb_hash: H256, + sender: Address, + shares: &[ShieldedTxDecryptionShare], + ) -> Acceptance<(), String> { let Some(context) = self.tdec_ctx.as_ref() else { - debug!("ignoring decryption shares without local TDEC context"); - return; + return Acceptance::Rejected("local TDEC context is unavailable".into()); }; - let sender = signed.address(); - let data = signed.data(); - let Some(compact) = self.db.mb_compact_block(data.mb_hash) else { - debug!(%sender, mb_hash = %data.mb_hash, "ignoring shares for unknown MB"); - return; + let Some(compact) = self.db.mb_compact_block(mb_hash) else { + return Acceptance::Rejected(format!("unknown MB {mb_hash}")); }; let Some(operations) = self.db.operations(compact.operations_hash) else { - warn!( - %sender, - mb_hash = %data.mb_hash, - operations_hash = %compact.operations_hash, - "ignoring decryption shares: MB operations are missing", - ); - return; + return Acceptance::Rejected(format!( + "operations {} for MB {mb_hash} are missing", + compact.operations_hash + )); }; let Some(participant_context) = context.contexts.get(&sender) else { - debug!(%sender, "ignoring decryption shares from unknown TDEC participant"); - return; + return Acceptance::Rejected(format!("unknown TDEC participant {sender}")); }; let transactions = operations .iter() @@ -807,18 +829,22 @@ impl EthexeExternalities { .map(|tx| (tx.to_hash(), tx)) .collect::>(); - for message_share in &data.shares { + let mut seen = HashSet::with_capacity(shares.len()); + for message_share in shares { + if !seen.insert(message_share.tx_hash) { + return Acceptance::Rejected(format!( + "duplicate decryption share for transaction {}", + message_share.tx_hash.inner() + )); + } let Some(transaction) = transactions.get(&message_share.tx_hash) else { - debug!( - %sender, - mb_hash = %data.mb_hash, - tx_hash = %message_share.tx_hash.inner(), - "ignoring decryption share for transaction outside MB", - ); - continue; + return Acceptance::Rejected(format!( + "decryption share for transaction {} outside MB {mb_hash}", + message_share.tx_hash.inner() + )); }; match self.decryption_shares.insert( - data.mb_hash, + mb_hash, message_share.tx_hash, sender, participant_context, @@ -826,26 +852,18 @@ impl EthexeExternalities { message_share.share.clone(), ) { InsertOutcome::Inserted | InsertOutcome::Duplicate => {} - InsertOutcome::InvalidShare => debug!( - %sender, - mb_hash = %data.mb_hash, - tx_hash = %message_share.tx_hash.inner(), - "ignoring invalid decryption share", - ), - InsertOutcome::Equivocation => warn!( - %sender, - mb_hash = %data.mb_hash, - tx_hash = %message_share.tx_hash.inner(), - "conflicting valid decryption share from the same participant", - ), - InsertOutcome::UnknownBlock | InsertOutcome::UnknownTransaction => debug!( - %sender, - mb_hash = %data.mb_hash, - tx_hash = %message_share.tx_hash.inner(), - "decryption-share storage rejected unknown MB or transaction", - ), + InsertOutcome::InvalidShare => { + return Acceptance::Rejected("invalid decryption share".into()); + } + InsertOutcome::Equivocation => { + return Acceptance::Rejected("conflicting decryption share".into()); + } + InsertOutcome::UnknownBlock | InsertOutcome::UnknownTransaction => { + return Acceptance::Rejected("unknown MB or transaction".into()); + } } } + Acceptance::Accepted(()) } fn provide_decryption_shares( @@ -854,7 +872,7 @@ impl EthexeExternalities { tdec_ctx: &PublicDecryptionContext, my_address: Address, transactions: &[&ShieldedTransaction], - ) { + ) -> Vec { let mut shares = Vec::with_capacity(transactions.len()); for tx in transactions { let Ok(share) = @@ -879,12 +897,7 @@ impl EthexeExternalities { shares.push(ShieldedTxDecryptionShare { tx_hash, share }); } - if !shares.is_empty() { - // Channel receiver is dropped only during shutdown. - let _ = self - .event_tx - .send(Ok(MalachiteEvent::DecryptionShares { mb_hash, shares })); - } + shares } fn process_unshielding(&self, mb_hash: H256, decryption_keys: &DecryptionKeys) -> Result<()> { @@ -1245,8 +1258,8 @@ mod tests { let parent = wrap(parent_payload, 1, H256::zero()); let parent_hash = parent.hash(); ext.process_mb_proposal(parent_hash, parent).await.unwrap(); - let _ = rx.recv().await.expect("decryption shares").expect("ok"); let _ = rx.recv().await.expect("parent proposal").expect("ok"); + assert!(ext.extend_vote(parent_hash).await.unwrap().is_some()); let child_payload = ext .build_operations(parent_hash) @@ -1569,18 +1582,9 @@ mod tests { let parent = Block::new(H256::zero(), 1, to_payload(parent_payload.encode())); let parent_hash = parent.hash(); ext.process_mb_proposal(parent_hash, parent).await.unwrap(); - let first = rx.recv().await.expect("first event").expect("ok"); - let second = rx.recv().await.expect("second event").expect("ok"); - assert!( - [&first, &second] - .iter() - .any(|event| matches!(event, MalachiteEvent::BlockProposal { .. })) - ); - assert!( - [&first, &second] - .iter() - .any(|event| matches!(event, MalachiteEvent::DecryptionShares { .. })) - ); + let event = rx.recv().await.expect("block event").expect("ok"); + assert!(matches!(event, MalachiteEvent::BlockProposal { .. })); + assert!(ext.extend_vote(parent_hash).await.unwrap().is_some()); let operations = tokio::time::timeout( std::time::Duration::from_millis(50), diff --git a/ethexe/malachite/service/src/service.rs b/ethexe/malachite/service/src/service.rs index 917377e48de..68b824f0353 100644 --- a/ethexe/malachite/service/src/service.rs +++ b/ethexe/malachite/service/src/service.rs @@ -17,7 +17,6 @@ use ethexe_common::{ Address, SimpleBlockData, db::{ConfigStorageRO, OnChainStorageRO}, injected::Transaction, - malachite::SignedBlockDecryptionShares, }; use ethexe_malachite_core::MalachiteCore; use futures::{Stream, stream::FusedStream}; @@ -157,13 +156,6 @@ impl MalachiteService { } } - /// Handle signed decryption shares for [ShieldedTransaction]. - /// - /// [ShieldedTransaction]: ethexe_common::injected::ShieldedTransaction - pub fn receive_decryption_shares(&self, signed_shares: SignedBlockDecryptionShares) { - self.externalities.receive_decryption_shares(signed_shares); - } - /// Push the on-chain validators for `head`'s era into the engine, /// if the era moved. Skips on missing DB data or unknown pub keys /// (wait-and-retry: the next `BlockSynced` re-evaluates). diff --git a/ethexe/malachite/service/src/types.rs b/ethexe/malachite/service/src/types.rs index 31dc246a2ae..29666239420 100644 --- a/ethexe/malachite/service/src/types.rs +++ b/ethexe/malachite/service/src/types.rs @@ -4,7 +4,6 @@ use ethexe_common::{ HashOf, SimpleBlockData, injected::{InjectedTransaction, PurgedTransaction, ShieldedTransaction}, - malachite::ShieldedTxDecryptionShare, }; use gprimitives::H256; use tokio::sync::{Notify, RwLock}; @@ -59,12 +58,6 @@ pub enum MalachiteEvent { unshielded_hash_mapping: Vec<(HashOf, HashOf)>, not_unshielded: Vec, }, - - /// Decryption shares for shielded transactions in an MB. - DecryptionShares { - mb_hash: H256, - shares: Vec, - }, } impl std::fmt::Display for MalachiteEvent { @@ -104,11 +97,6 @@ impl std::fmt::Display for MalachiteEvent { unshielded_hash_mapping.len(), not_unshielded.len(), ), - Self::DecryptionShares { mb_hash, shares } => write!( - f, - "DecryptionShares(mb_hash: {mb_hash}, shares_len: {})", - shares.len(), - ), } } } diff --git a/ethexe/malachite/service/tests/restart_resilience.rs b/ethexe/malachite/service/tests/restart_resilience.rs index f39f777b92d..ad6dadff9b6 100644 --- a/ethexe/malachite/service/tests/restart_resilience.rs +++ b/ethexe/malachite/service/tests/restart_resilience.rs @@ -226,9 +226,6 @@ async fn collect_until_finalized( Ok(Some(Ok(MalachiteEvent::PurgedTransactions { .. }))) => { // ignore } - Ok(Some(Ok(MalachiteEvent::DecryptionShares { .. }))) => { - // ignore - } Ok(Some(Ok(MalachiteEvent::UnshieldingOutput { .. }))) => { // ignore } diff --git a/ethexe/network/src/gossipsub.rs b/ethexe/network/src/gossipsub.rs index f6865ed66e7..72b79e177d7 100644 --- a/ethexe/network/src/gossipsub.rs +++ b/ethexe/network/src/gossipsub.rs @@ -8,10 +8,7 @@ use crate::{ peer_score, }; use anyhow::anyhow; -use ethexe_common::{ - Address, injected::SignedCompactTxReceipt, malachite::SignedBlockDecryptionShares, - network::SignedValidatorMessage, -}; +use ethexe_common::{Address, injected::SignedCompactTxReceipt, network::SignedValidatorMessage}; use libp2p::{ core::{Endpoint, transport::PortUse}, gossipsub, @@ -35,7 +32,6 @@ pub enum Message { // TODO: rename to `Validators` Commitments(SignedValidatorMessage), TxReceipt(SignedCompactTxReceipt), - DecryptionShares(SignedBlockDecryptionShares), } impl Message { @@ -43,7 +39,6 @@ impl Message { match self { Message::Commitments(_) => behaviour.commitments_topic.hash(), Message::TxReceipt(_) => behaviour.tx_receipts_topic.hash(), - Message::DecryptionShares(_) => behaviour.decryption_shares_topic.hash(), } } @@ -51,7 +46,6 @@ impl Message { match self { Message::Commitments(message) => message.encode(), Message::TxReceipt(message) => message.encode(), - Message::DecryptionShares(message) => message.encode(), } } } @@ -104,7 +98,6 @@ pub(crate) struct Behaviour { message_queue: VecDeque, commitments_topic: IdentTopic, tx_receipts_topic: IdentTopic, - decryption_shares_topic: IdentTopic, metrics: Arc, } @@ -118,7 +111,6 @@ impl Behaviour { ) -> anyhow::Result { let commitments_topic = Self::topic_with_router("commitments", router_address); let tx_receipts_topic = Self::topic_with_router("receipts", router_address); - let decryption_shares_topic = Self::topic_with_router("decryption_shares", router_address); let inner = ConfigBuilder::default() // dedup messages @@ -143,7 +135,6 @@ impl Behaviour { inner.subscribe(&commitments_topic)?; inner.subscribe(&tx_receipts_topic)?; - inner.subscribe(&decryption_shares_topic)?; Ok(Self { inner, @@ -151,7 +142,6 @@ impl Behaviour { message_queue: VecDeque::new(), commitments_topic, tx_receipts_topic, - decryption_shares_topic, metrics, }) } @@ -186,9 +176,6 @@ impl Behaviour { SignedValidatorMessage::decode(&mut &data[..]).map(Message::Commitments) } else if topic == self.tx_receipts_topic.hash() { SignedCompactTxReceipt::decode(&mut &data[..]).map(Message::TxReceipt) - } else if topic == self.decryption_shares_topic.hash() { - SignedBlockDecryptionShares::decode(&mut &data[..]) - .map(Message::DecryptionShares) } else { unreachable!("topic we never subscribed to: {topic:?}"); }; diff --git a/ethexe/network/src/lib.rs b/ethexe/network/src/lib.rs index ad31033866d..612e3e7d0b1 100644 --- a/ethexe/network/src/lib.rs +++ b/ethexe/network/src/lib.rs @@ -8,7 +8,7 @@ //! //! - peer management and connection caps; //! - Kademlia-backed validator discovery; -//! - gossipsub topics for validator messages, public promises, and decryption shares; +//! - gossipsub topics for validator messages and public promises; //! - request/response database synchronization; //! - private injected-transaction delivery to validators; //! - peer scoring and temporary peer blocking. @@ -45,7 +45,6 @@ use ethexe_common::{ db::ConfigStorageRO, ecdsa::PublicKey, injected::{SignedCompactTxReceipt, Transaction}, - malachite::SignedBlockDecryptionShares, network::{SignedValidatorMessage, VerifiedValidatorMessage}, }; use ethexe_db::Database; @@ -93,8 +92,6 @@ pub enum NetworkEvent { ValidatorMessage(VerifiedValidatorMessage), /// A public promise observed on the promise gossipsub topic. TxReceiptMessage(SignedCompactTxReceipt), - /// Validator-signed decryption shares for a Malachite block. - DecryptionShares(SignedBlockDecryptionShares), /// Validator discovery learned or refreshed the network identity of the /// given validator address. ValidatorIdentityUpdated(Address), @@ -546,12 +543,6 @@ impl NetworkService { self.validator_topic.verify_receipt(source, receipt); (acceptance, receipt.map(NetworkEvent::TxReceiptMessage)) } - gossipsub::Message::DecryptionShares(message) => { - let (acceptance, message) = self - .validator_topic - .verify_decryption_message(source, message); - (acceptance, message.map(NetworkEvent::DecryptionShares)) - } }) } gossipsub::Event::PublishFailure { @@ -655,11 +646,6 @@ impl NetworkService { pub fn publish_tx_receipt(&mut self, receipt: SignedCompactTxReceipt) { self.swarm.behaviour_mut().gossipsub.publish(receipt) } - - /// Publish validator-signed decryption shares for a Malachite block. - pub fn publish_decryption_shares(&mut self, message: SignedBlockDecryptionShares) { - self.swarm.behaviour_mut().gossipsub.publish(message) - } } impl Drop for NetworkService { diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index e38a94a5189..586cfeffdf5 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -11,7 +11,6 @@ use crate::{ use ethexe_common::{ Address, injected::{SignedCompactTxReceipt, TransactionHash}, - malachite::SignedBlockDecryptionShares, network::VerifiedValidatorMessage, }; use lru::LruCache; @@ -304,26 +303,6 @@ impl ValidatorTopic { } } - /// Admit a signed decryption-share message from a known validator. - /// - /// Block relevance and share correctness are intentionally left to the - /// future decryption-share handler. - pub fn verify_decryption_message( - &self, - source: PeerId, - message: SignedBlockDecryptionShares, - ) -> (MessageAcceptance, Option) { - if self.snapshot.contains(message.address()) { - (MessageAcceptance::Accept, Some(message)) - } else { - log::trace!( - "ignore decryption shares from unknown validator {} via {source}", - message.address() - ); - (MessageAcceptance::Ignore, None) - } - } - /// Retrieve the next verified message that is ready for further processing. pub(crate) fn next_message(&mut self) -> Option { self.verified_messages.pop_front() @@ -338,11 +317,9 @@ mod tests { consensus::BatchCommitmentValidationRequest, ecdsa::PublicKey, injected::{Promise, Receipt}, - malachite::BlockDecryptionData, mock::Mock, network::{SignedValidatorMessage, ValidatorMessage}, }; - use gprimitives::H256; use gsigner::secp256k1::{Secp256k1SignerExt, Signer}; use nonempty::{NonEmpty, nonempty}; @@ -400,22 +377,6 @@ mod tests { .into() } - fn signed_decryption_message( - signer: &Signer, - public_key: PublicKey, - ) -> SignedBlockDecryptionShares { - signer - .signed_message( - public_key, - BlockDecryptionData { - mb_hash: H256::random(), - shares: Vec::new(), - }, - None, - ) - .unwrap() - } - /// Buckets a message era can fall into relative to the snapshot era. #[derive(Debug, Clone, Copy)] enum EraRelation { @@ -810,23 +771,4 @@ mod tests { assert_matches!(acceptance, MessageAcceptance::Accept); assert_eq!(returned_receipt, Some(receipt)); } - - #[test] - fn verify_decryption_message_checks_validator_membership() { - let (pubkey, signer) = signer_with_pubkey(); - let message = signed_decryption_message(&signer, pubkey); - let peer_id = PeerId::random(); - - let unknown_topic = new_topic(nonempty![Address::default()]); - let (acceptance, returned) = - unknown_topic.verify_decryption_message(peer_id, message.clone()); - assert_matches!(acceptance, MessageAcceptance::Ignore); - assert_eq!(returned, None); - - let validator_topic = new_topic(nonempty![message.address()]); - let (acceptance, returned) = - validator_topic.verify_decryption_message(peer_id, message.clone()); - assert_matches!(acceptance, MessageAcceptance::Accept); - assert_eq!(returned, Some(message)); - } } diff --git a/ethexe/service/src/lib.rs b/ethexe/service/src/lib.rs index 64ef05955a9..b657c09181e 100644 --- a/ethexe/service/src/lib.rs +++ b/ethexe/service/src/lib.rs @@ -48,7 +48,6 @@ use ethexe_common::{ db::{GlobalsStorageRW, MbStorageRO}, gear::CodeState, injected::{CompactPromise, Receipt, TransactionAcceptance}, - malachite::BlockDecryptionData, network::VerifiedValidatorMessage, }; use ethexe_compute::{ComputeEvent, ComputeService}; @@ -882,10 +881,6 @@ impl Service { rpc.receive_tx_receipt(receipt); } } - NetworkEvent::DecryptionShares(message) => { - // Just route shares to malachite service. - malachite.receive_decryption_shares(message); - } NetworkEvent::ValidatorIdentityUpdated(_) | NetworkEvent::PeerBlocked(_) | NetworkEvent::PeerConnected(_) => {} @@ -1037,27 +1032,6 @@ impl Service { } }); } - MalachiteEvent::DecryptionShares { mb_hash, shares } => { - let Some(pub_key) = validator_pub_key else { - // Validator key not found, can not sign shares. - continue; - }; - - let data = BlockDecryptionData { mb_hash, shares }; - match signer.signed_message(pub_key, data, None) { - Ok(message) => { - if let Some(network) = network.as_mut() { - network.publish_decryption_shares(message); - } - } - Err(err) => { - tracing::error!( - %mb_hash, - "failed to sign decryption shares: {err}" - ); - } - } - } MalachiteEvent::UnshieldingOutput { mb_hash, unshielded_hash_mapping, diff --git a/ethexe/service/src/tests/utils/events.rs b/ethexe/service/src/tests/utils/events.rs index 98eea752ebf..2d1d042c9c3 100644 --- a/ethexe/service/src/tests/utils/events.rs +++ b/ethexe/service/src/tests/utils/events.rs @@ -12,7 +12,6 @@ use ethexe_common::{ db::*, events::BlockEvent, injected::{SignedCompactTxReceipt, Transaction, TransactionAcceptance, TransactionHash}, - malachite::SignedBlockDecryptionShares, network::VerifiedValidatorMessage, }; use ethexe_compute::ComputeEvent; @@ -78,7 +77,6 @@ impl TestingNetworkInjectedEvent { pub enum TestingNetworkEvent { ValidatorMessage(VerifiedValidatorMessage), TxReceiptMessage(SignedCompactTxReceipt), - DecryptionShares(SignedBlockDecryptionShares), ValidatorIdentityUpdated(Address), InjectedTransaction(TestingNetworkInjectedEvent), PeerBlocked(PeerId), @@ -90,7 +88,6 @@ impl TestingNetworkEvent { match event { NetworkEvent::ValidatorMessage(message) => Self::ValidatorMessage(message.clone()), NetworkEvent::TxReceiptMessage(message) => Self::TxReceiptMessage(message.clone()), - NetworkEvent::DecryptionShares(message) => Self::DecryptionShares(message.clone()), NetworkEvent::ValidatorIdentityUpdated(address) => { Self::ValidatorIdentityUpdated(*address) } From 84034792f2f29df6c1d7a13443e65051d8012ce6 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Tue, 30 Jun 2026 20:21:02 +0400 Subject: [PATCH 39/41] chore: update ferveo-* deps to release 0.7.0 --- Cargo.lock | 43 +++++++++++++------------ Cargo.toml | 4 +-- ethexe/malachite/service/src/mempool.rs | 2 +- protocol/gsigner/Cargo.toml | 11 ++----- 4 files changed, 27 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 917d4eae8b5..9c7e33f9e91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -946,7 +946,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -957,7 +957,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3358,7 +3358,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]] @@ -4194,7 +4194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.114", ] [[package]] @@ -5544,7 +5544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6233,8 +6233,9 @@ dependencies = [ [[package]] name = "ferveo-gear-common" -version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#f10902a443a305ff240a00b7d43d117696becf1d" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7221252bf540ec9de204fc7f3da82bfbf13bdec2d3ad8f44d58aab8cace39ae3" dependencies = [ "ark-ec 0.5.0", "ark-serialize 0.5.0", @@ -6250,8 +6251,9 @@ dependencies = [ [[package]] name = "ferveo-gear-tdec" -version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#f10902a443a305ff240a00b7d43d117696becf1d" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76505fcc520805d312c411d54318856b32fd6dbd2caa5e9e6130eeb927aab6c9" dependencies = [ "ark-bls12-381 0.5.0", "ark-ec 0.5.0", @@ -9673,7 +9675,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -12211,7 +12213,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", @@ -12223,7 +12225,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]] @@ -14581,7 +14583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck 0.5.0", - "itertools 0.11.0", + "itertools 0.14.0", "log", "multimap 0.10.1", "once_cell", @@ -14627,7 +14629,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.114", @@ -15646,7 +15648,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -15770,7 +15772,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 1.0.5", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -18880,8 +18882,9 @@ dependencies = [ [[package]] name = "subproductdomain-gear" -version = "0.5.0" -source = "git+https://github.com/gear-tech/ferveo-nucypher.git?branch=more-codec#f10902a443a305ff240a00b7d43d117696becf1d" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f01cef670a1b7a6ed0e5eb516d5b0b60b85ca3c7e6c8fe438e1ec667995c4d6" dependencies = [ "anyhow", "ark-ec 0.5.0", @@ -19357,7 +19360,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -21339,7 +21342,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 c1b508c047c..b320c388d57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -268,9 +268,7 @@ metrics = "0.24.0" metrics-derive = "0.1" metrics-exporter-prometheus = { version = "0.16.0", default-features = false } -# gear-tdec = { package = "ferveo-gear-tdec", version = "0.5.0"} -gear-tdec = { package = "ferveo-gear-tdec", git = "https://github.com/gear-tech/ferveo-nucypher.git", branch = "more-codec"} - +gear-tdec = { package = "ferveo-gear-tdec", version = "0.7.0" } # Published deps # # https://github.com/gear-tech/gear-dlmalloc/tree/0.2.1 diff --git a/ethexe/malachite/service/src/mempool.rs b/ethexe/malachite/service/src/mempool.rs index 53290a9a244..7ef305c0d44 100644 --- a/ethexe/malachite/service/src/mempool.rs +++ b/ethexe/malachite/service/src/mempool.rs @@ -206,7 +206,7 @@ impl InjectedTxMempool { } } - /// Delegates call to [Inner::len]. + /// Delegates call to `Inner::len`. pub async fn len(&self) -> usize { self.inner.read().await.len() } diff --git a/protocol/gsigner/Cargo.toml b/protocol/gsigner/Cargo.toml index 1559ac2253b..9f7fd4db5d2 100644 --- a/protocol/gsigner/Cargo.toml +++ b/protocol/gsigner/Cargo.toml @@ -32,7 +32,7 @@ k256 = { version = "0.13.4", default-features = false, features = [ ], optional = true } nacl = { workspace = true, optional = true } dirs = { workspace = true, optional = true } -ferveo-common = { package = "ferveo-gear-common", git = "https://github.com/gear-tech/ferveo-nucypher.git", branch = "more-codec", features = [ +ferveo-common = { package = "ferveo-gear-common", version = "0.7.0", features = [ "ark-serde-hex", ], optional = true } gear-tdec = { workspace = true, optional = true } @@ -75,14 +75,7 @@ serde_json = { workspace = true, features = ["std"] } tempfile = { workspace = true } [features] -default = [ - "std", - "secp256k1", - "sr25519", - "ed25519", - "codec", - "keyring", -] +default = ["std", "secp256k1", "sr25519", "ed25519", "codec", "keyring"] std = [ "dep:anyhow", "dep:rand", From cc35201b88d076fe40701cbd39e937de5e2515a2 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 1 Jul 2026 13:25:26 +0400 Subject: [PATCH 40/41] chore: remove feature from ethexe-common | bump gear-tdec to v0.8.0 --- Cargo.lock | 19 +++--- Cargo.toml | 4 +- ethexe/common/Cargo.toml | 7 +-- ethexe/common/src/db.rs | 14 ++--- ethexe/common/src/injected.rs | 26 -------- ethexe/common/src/malachite.rs | 58 +++++++---------- protocol/gsigner/Cargo.toml | 7 +-- protocol/gsigner/src/lib.rs | 22 +------ protocol/gsigner/src/tdec/mod.rs | 24 +++++++ .../gsigner/src/{tdec.rs => tdec/store.rs} | 63 ++----------------- protocol/gsigner/src/tdec/tests.rs | 37 +++++++++++ 11 files changed, 111 insertions(+), 170 deletions(-) create mode 100644 protocol/gsigner/src/tdec/mod.rs rename protocol/gsigner/src/{tdec.rs => tdec/store.rs} (82%) create mode 100644 protocol/gsigner/src/tdec/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 9c7e33f9e91..5281a885e3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6233,27 +6233,26 @@ dependencies = [ [[package]] name = "ferveo-gear-common" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7221252bf540ec9de204fc7f3da82bfbf13bdec2d3ad8f44d58aab8cace39ae3" +checksum = "30a17e50cd62653ecf51f3123b4b9950e6873aa2fa6c7266e04a6000fa9ff941" dependencies = [ "ark-ec 0.5.0", "ark-serialize 0.5.0", "ark-std 0.5.0", - "bincode", "const-hex", "generic-array 0.14.7", "parity-scale-codec", "rand 0.8.5", "serde", - "thiserror 1.0.69", + "thiserror 2.0.17", ] [[package]] name = "ferveo-gear-tdec" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76505fcc520805d312c411d54318856b32fd6dbd2caa5e9e6130eeb927aab6c9" +checksum = "8eedc637ff20af20c9aea6b9d82b8740311ed634309739cbd886809932314ad8" dependencies = [ "ark-bls12-381 0.5.0", "ark-ec 0.5.0", @@ -6261,7 +6260,6 @@ dependencies = [ "ark-poly 0.5.0", "ark-serialize 0.5.0", "ark-std 0.5.0", - "bincode", "chacha20poly1305", "const-hex", "ferveo-gear-common", @@ -6272,7 +6270,7 @@ dependencies = [ "serde", "sha2 0.10.9", "subproductdomain-gear", - "thiserror 1.0.69", + "thiserror 2.0.17", "zeroize", ] @@ -8566,7 +8564,6 @@ dependencies = [ "colored", "derive_more 2.1.1", "dirs", - "ferveo-gear-common", "ferveo-gear-tdec", "gear-workspace-hack", "gprimitives", @@ -18882,9 +18879,9 @@ dependencies = [ [[package]] name = "subproductdomain-gear" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f01cef670a1b7a6ed0e5eb516d5b0b60b85ca3c7e6c8fe438e1ec667995c4d6" +checksum = "cfba387009bc87f6ba69c9461d62da844f7c81904d319e7348a59b15fdd535dc" dependencies = [ "anyhow", "ark-ec 0.5.0", diff --git a/Cargo.toml b/Cargo.toml index b320c388d57..969e177df6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -268,7 +268,7 @@ metrics = "0.24.0" metrics-derive = "0.1" metrics-exporter-prometheus = { version = "0.16.0", default-features = false } -gear-tdec = { package = "ferveo-gear-tdec", version = "0.7.0" } +gear-tdec = { package = "ferveo-gear-tdec", version = "0.8.0", default-features = false} # Published deps # # https://github.com/gear-tech/gear-dlmalloc/tree/0.2.1 @@ -726,4 +726,4 @@ sc-executor-wasmtime = { path = "substrate/runtime-executor/wasmtime" } sc-mixnet = { path = "substrate/sc-mixnet" } sp-runtime-interface-proc-macro = { path = "substrate/sp-runtime-interface-proc-macro" } sp-wasm-interface = { path = "substrate/sp-wasm-interface" } -substrate-wasm-builder = { path = "substrate/substrate-wasm-builder" } \ No newline at end of file +substrate-wasm-builder = { path = "substrate/substrate-wasm-builder" } diff --git a/ethexe/common/Cargo.toml b/ethexe/common/Cargo.toml index b9d30046e42..a528b5aa8b5 100644 --- a/ethexe/common/Cargo.toml +++ b/ethexe/common/Cargo.toml @@ -25,6 +25,7 @@ gsigner = { workspace = true, default-features = false, features = [ "secp256k1", "codec", "serde", + "tdec", ] } sha3.workspace = true k256 = { version = "0.13.4", features = ["ecdsa"], default-features = false } @@ -34,8 +35,8 @@ ark-ec.workspace = true # optional dependencies serde = { workspace = true, optional = true } -gear-tdec = { workspace = true, optional = true, features = ["serde-hex"]} -ark-serialize = { workspace = true, optional = true } +gear-tdec = { workspace = true, features = ["serde-hex", "parity-codec", "bls12_381"]} +ark-serialize = { workspace = true } # mock deps itertools = { workspace = true, optional = true } @@ -62,7 +63,5 @@ std = [ "alloy-primitives/std", "gsigner/std", "gsigner/keyring", - "shielded" ] -shielded = ["gsigner/tdec", "dep:gear-tdec", "dep:ark-serialize"] mock = ["std", "itertools/use_std", "tap", "proptest"] diff --git a/ethexe/common/src/db.rs b/ethexe/common/src/db.rs index af8ef84079a..6c20cb4d4d4 100644 --- a/ethexe/common/src/db.rs +++ b/ethexe/common/src/db.rs @@ -3,14 +3,15 @@ //! Common db types and traits. -#[cfg(feature = "shielded")] -use crate::injected::{ShieldedTransaction, SignedShieldedTransaction, SignedTxReceipt}; use crate::{ Address, BlockHeader, CodeBlobInfo, Digest, HashOf, ProgramStates, ProtocolTimelines, Schedule, SimpleBlockData, ValidatorsVec, events::BlockEvent, gear::StateTransition, - injected::{InjectedTransaction, Promise, SignedInjectedTransaction}, + injected::{ + InjectedTransaction, Promise, ShieldedTransaction, SignedInjectedTransaction, + SignedShieldedTransaction, SignedTxReceipt, + }, malachite::Operations, }; use alloc::{ @@ -21,7 +22,6 @@ use gear_core::{ code::{CodeMetadata, InstrumentedCode}, ids::{ActorId, CodeId}, }; -#[cfg(feature = "shielded")] use gear_tdec::bls12_381::DkgPublicKey; use gprimitives::H256; use gsigner::VerifiedData; @@ -120,7 +120,6 @@ pub trait InjectedStorageRO { hash: HashOf, ) -> Option; - #[cfg(feature = "shielded")] /// Returns the shielded transaction by its hash. fn shielded_transaction( &self, @@ -130,7 +129,6 @@ pub trait InjectedStorageRO { /// Returns the promise by its transaction hash. fn promise(&self, hash: HashOf) -> Option; - #[cfg(feature = "shielded")] /// Returns the receipt by its transaction hash. fn receipt(&self, hash: HashOf) -> Option; } @@ -139,22 +137,18 @@ pub trait InjectedStorageRO { pub trait InjectedStorageRW: InjectedStorageRO { fn set_injected_transaction(&self, tx: SignedInjectedTransaction); - #[cfg(feature = "shielded")] fn set_shielded_transaction(&self, tx: SignedShieldedTransaction); fn set_promise(&self, promise: &Promise); - #[cfg(feature = "shielded")] fn set_receipt(&self, receipt: &SignedTxReceipt); } -#[cfg(feature = "shielded")] #[auto_impl::auto_impl(&)] pub trait TdecStorageRO { fn shielding_key(&self) -> Option; } -#[cfg(feature = "shielded")] #[auto_impl::auto_impl(&)] pub trait TdecStorageRW: TdecStorageRO { fn set_shielding_key(&self, key: DkgPublicKey); diff --git a/ethexe/common/src/injected.rs b/ethexe/common/src/injected.rs index 9c33a1785f3..6162e66140d 100644 --- a/ethexe/common/src/injected.rs +++ b/ethexe/common/src/injected.rs @@ -6,11 +6,9 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; -#[cfg(feature = "shielded")] use ark_serialize::CanonicalSerialize; use core::hash::Hash; use gear_core::{limited::LimitedVec, rpc::ReplyInfo}; -#[cfg(feature = "shielded")] use gear_tdec::{ Result as TdecResult, bls12_381::{Ciphertext, DkgPublicKey, SharedSecret}, @@ -132,7 +130,6 @@ impl InjectedTransaction { MessageId::new(self.to_hash().inner().0) } - #[cfg(feature = "shielded")] pub fn shield( self, public_key: &DkgPublicKey, @@ -251,7 +248,6 @@ impl PromiseKind for CompactPromise { /// **Important**: `Receipt` and `Receipt` have the same /// digest. So it helps to reuses the producer's signature to construct the full /// version from compact. -#[cfg(feature = "shielded")] #[derive( Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::IsVariant, derive_more::Unwrap, )] @@ -262,7 +258,6 @@ pub enum Receipt

{ Purged(PurgedTransaction), } -#[cfg(feature = "shielded")] impl Receipt

{ pub fn tx_hash(&self) -> TransactionHash { match self { @@ -272,7 +267,6 @@ impl Receipt

{ } } -#[cfg(feature = "shielded")] impl ToDigest for Receipt

{ fn update_hasher(&self, hasher: &mut sha3::Keccak256) { match self { @@ -290,7 +284,6 @@ impl ToDigest for Receipt

{ /// Signed [Receipt] with a [Promise] generic. /// End RPC user always receives this object. -#[cfg(feature = "shielded")] #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::From, derive_more::Deref)] #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", serde(transparent))] @@ -298,7 +291,6 @@ pub struct SignedTxReceipt(pub SignedMessage>); /// Signed [Receipt] with a [CompactPromise] generic. /// It is used as a lightweight transfer type -#[cfg(feature = "shielded")] #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Deref, derive_more::From)] pub struct SignedCompactTxReceipt(SignedMessage>); @@ -307,14 +299,12 @@ pub struct SignedCompactTxReceipt(SignedMessage>); /// to full version. /// [Pending](Self::Pending) means that receipt contains a promise and requires the /// full promise body to restore receipt. -#[cfg(feature = "shielded")] #[derive(Debug, PartialEq, Eq, derive_more::From)] pub enum UpgradedReceipt { Pending(UnfilledPromiseReceipt), Ready(SignedTxReceipt), } -#[cfg(feature = "shielded")] impl SignedCompactTxReceipt { /// Upgrades the compact receipt to its full version ([SignedTxReceipt]). pub fn upgrade(self) -> UpgradedReceipt { @@ -342,13 +332,11 @@ pub struct UnfilledPromiseReceipt(#[deref] CompactPromise, Signature, Address); /// The result of [try_fill_with](UnfilledPromiseReceipt::try_fill_with) function. /// [Filled](Self::Filled) means the successful result. /// [HashesMismatch](Self::HashesMismatch) means that raw promise body and stored compact are not the same promise. -#[cfg(feature = "shielded")] pub enum TryFillPromiseResult { Filled(SignedTxReceipt), HashesMismatch(UnfilledPromiseReceipt), } -#[cfg(feature = "shielded")] impl UnfilledPromiseReceipt { pub fn try_fill_with(self, promise: Promise) -> TryFillPromiseResult { if self.0 != promise.to_compact() { @@ -363,7 +351,6 @@ impl UnfilledPromiseReceipt { } /// Represents the reason why transaction was not included. -#[cfg(feature = "shielded")] #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Display)] #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[display("Injected transaction wasn't executed: tx_hash={tx_hash}, reason={reason}")] @@ -374,7 +361,6 @@ pub struct PurgedTransaction { pub reason: TransactionPurgedReason, } -#[cfg(feature = "shielded")] impl ToDigest for PurgedTransaction { fn update_hasher(&self, hasher: &mut sha3::Keccak256) { let Self { tx_hash, reason } = self; @@ -413,7 +399,6 @@ impl TransactionPurgedReason { } } -#[cfg(feature = "shielded")] #[cfg_attr(feature = "serde", derive(Hash))] #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TypeInfo)] pub struct ShieldedFields { @@ -422,7 +407,6 @@ pub struct ShieldedFields { pub payload: LimitedVec, } -#[cfg(feature = "shielded")] impl ToDigest for ShieldedFields { fn update_hasher(&self, hasher: &mut sha3::Keccak256) { let Self { @@ -436,7 +420,6 @@ impl ToDigest for ShieldedFields { } } -#[cfg(feature = "shielded")] #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(Hash))] #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] @@ -455,7 +438,6 @@ pub struct ShieldedTransaction { pub salt: LimitedVec, } -#[cfg(feature = "shielded")] impl ShieldedTransaction { fn append_compressed_point(buffer: &mut Vec, point: &P) { point @@ -490,17 +472,14 @@ impl ShieldedTransaction { } } -#[cfg(feature = "shielded")] pub type SignedShieldedTransaction = SignedMessage; -#[cfg(feature = "shielded")] impl ToDigest for ShieldedTransaction { fn update_hasher(&self, hasher: &mut sha3::Keccak256) { hasher.update(self.to_hashable_bytes()); } } -#[cfg(feature = "shielded")] impl ShieldedTransaction { /// Decrypts [Ciphertext] with provided [SharedSecret]. /// Returns initial [InjectedTransaction]. @@ -522,7 +501,6 @@ impl ShieldedTransaction { } } -#[cfg(feature = "shielded")] #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] #[derive(Debug, Clone, Encode, Decode, Eq, PartialEq, derive_more::From)] #[allow(clippy::large_enum_variant)] @@ -531,11 +509,9 @@ pub enum Transaction { Shielded(SignedShieldedTransaction), } -#[cfg(feature = "shielded")] /// Type alias over [EitherHashOf]. pub type TransactionHash = EitherHashOf; -#[cfg(feature = "shielded")] impl Transaction { pub fn as_ref(&self) -> TransactionRef<'_> { match self { @@ -559,14 +535,12 @@ impl Transaction { /// This type must be used to transform [Operation] type into [Option]. /// /// [Operation]: crate::malachite::Operation -#[cfg(feature = "shielded")] #[derive(Clone, Copy)] pub enum TransactionRef<'op> { Injected(&'op SignedInjectedTransaction), Shielded(&'op SignedShieldedTransaction), } -#[cfg(feature = "shielded")] impl<'t> TransactionRef<'t> { pub fn hash(&self) -> TransactionHash { match self { diff --git a/ethexe/common/src/malachite.rs b/ethexe/common/src/malachite.rs index 3bd9602068b..8dab6c459d9 100644 --- a/ethexe/common/src/malachite.rs +++ b/ethexe/common/src/malachite.rs @@ -27,26 +27,18 @@ //! `ethexe-malachite`) so `ethexe-processor` can accept them without //! depending on the consensus layer. -#[cfg(all(feature = "shielded", feature = "std"))] -use std::num::NonZeroUsize; - -use crate::{Address, injected::SignedInjectedTransaction}; -use alloc::vec::Vec; +use crate::{ + Address, HashOf, ToDigest, + injected::{ShieldedTransaction, SignedInjectedTransaction, SignedShieldedTransaction}, +}; +use alloc::{collections::BTreeMap, vec::Vec}; use derive_more::{Deref, DerefMut, IntoIterator}; +use gear_tdec::bls12_381::SharedSecret; use gprimitives::H256; +use gsigner::{DecryptionShare, SignedMessage}; use parity_scale_codec::{Decode, Encode}; -#[cfg(feature = "shielded")] -use { - crate::{ - HashOf, ToDigest, - injected::{ShieldedTransaction, SignedShieldedTransaction}, - }, - gear_tdec::bls12_381::SharedSecret, - gsigner::{DecryptionShare, SignedMessage}, - sha3::{Digest as _, Keccak256}, - std::collections::BTreeMap, -}; -#[cfg(all(feature = "shielded", feature = "std"))] +use sha3::{Digest as _, Keccak256}; +#[cfg(feature = "std")] use {gsigner::PublicDecryptionContext, std::collections::HashMap}; #[cfg(feature = "std")] @@ -59,31 +51,37 @@ use serde::{Deserialize, Serialize}; #[allow(clippy::large_enum_variant)] pub enum Operation { /// Pin executor's view to a quarantine-passed Ethereum block. - AdvanceTillEthereumBlock { block_hash: H256 } = 0, + AdvanceTillEthereumBlock { + block_hash: H256, + } = 0, /// Progress scheduled tasks (mailbox/waitlist/reservation cleanup). ProgressTasks = 1, /// Execute queued message within `gas_allowance`. - ProcessQueues { gas_allowance: u64 } = 2, + ProcessQueues { + gas_allowance: u64, + } = 2, /// User-submitted transaction from the mempool. Injected(SignedInjectedTransaction) = 3, /// Execute queued messages within `gas_allowance`. /// V2 - changes mailbox validity, from one week to 15 minutes - ProcessQueuesV2 { gas_allowance: u64 } = 4, + ProcessQueuesV2 { + gas_allowance: u64, + } = 4, /// Execute queued messages within `gas_allowance`. /// V3 - auto-replies to Sails event destinations without mailboxing and /// emits Ethereum event destinations via transition messages. - ProcessQueuesV3 { gas_allowance: u64 } = 5, + ProcessQueuesV3 { + gas_allowance: u64, + } = 5, /// User-submitted shielded transaction from mempool. - #[cfg(feature = "shielded")] Shielded(SignedShieldedTransaction) = 6, - #[cfg(feature = "shielded")] DecryptionKeys(BTreeMap, SharedSecret>) = 7, } @@ -102,7 +100,6 @@ impl Operation { } /// Returns `Some` if `Self` contains shielded transaction. - #[cfg(feature = "shielded")] pub fn as_shielded(&self) -> Option<&SignedShieldedTransaction> { match self { Self::Shielded(tx) => Some(tx), @@ -110,7 +107,6 @@ impl Operation { } } - #[cfg(feature = "shielded")] pub fn into_shielded(self) -> Option { match self { Self::Shielded(tx) => Some(tx), @@ -145,11 +141,9 @@ impl Decode for Operation { 5 => Ok(Operation::ProcessQueuesV3 { gas_allowance: u64::decode(input)?, }), - #[cfg(feature = "shielded")] 6 => Ok(Operation::Shielded(SignedShieldedTransaction::decode( input, )?)), - #[cfg(feature = "shielded")] 7 => Ok(Operation::DecryptionKeys( as Decode>::decode(input)?, )), @@ -168,9 +162,7 @@ impl Encode for Operation { Operation::Injected(signed_tx) => signed_tx.encode_to(dest), Operation::ProcessQueuesV2 { gas_allowance } => gas_allowance.encode_to(dest), Operation::ProcessQueuesV3 { gas_allowance } => gas_allowance.encode_to(dest), - #[cfg(feature = "shielded")] Operation::Shielded(shielded_tx) => shielded_tx.encode_to(dest), - #[cfg(feature = "shielded")] Operation::DecryptionKeys(keys) => keys.encode_to(dest), } } @@ -193,11 +185,11 @@ impl Operations { } /// Validator's context for shielded transactions decryption. -#[cfg(all(feature = "shielded", feature = "std"))] +#[cfg(feature = "std")] #[derive(Debug, Clone)] pub struct MalachiteTdecContext { /// Minimal number of decryption shares required to decrypt transaction. - pub threshold: NonZeroUsize, + pub threshold: core::num::NonZeroUsize, /// Current validator's public decryption context. /// Private data stored in [TdecKeyStore]. /// @@ -212,7 +204,6 @@ pub struct MalachiteTdecContext { /// Holds [`DecryptionShare`] over [`ShieldedTransaction`]. /// /// [ShieldedTransaction]: crate::injected::ShieldedTransaction -#[cfg(feature = "shielded")] #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct ShieldedTxDecryptionShare { @@ -221,7 +212,6 @@ pub struct ShieldedTxDecryptionShare { pub share: DecryptionShare, } -#[cfg(feature = "shielded")] #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct BlockDecryptionData { @@ -231,7 +221,6 @@ pub struct BlockDecryptionData { pub shares: Vec, } -#[cfg(feature = "shielded")] impl ToDigest for BlockDecryptionData { fn update_hasher(&self, hasher: &mut Keccak256) { hasher.update(self.encode()); @@ -239,7 +228,6 @@ impl ToDigest for BlockDecryptionData { } /// Validator-signed decryption shares for one Malachite block. -#[cfg(feature = "shielded")] pub type SignedBlockDecryptionShares = SignedMessage; #[cfg(test)] diff --git a/protocol/gsigner/Cargo.toml b/protocol/gsigner/Cargo.toml index 9f7fd4db5d2..0362a7fe66b 100644 --- a/protocol/gsigner/Cargo.toml +++ b/protocol/gsigner/Cargo.toml @@ -32,10 +32,7 @@ k256 = { version = "0.13.4", default-features = false, features = [ ], optional = true } nacl = { workspace = true, optional = true } dirs = { workspace = true, optional = true } -ferveo-common = { package = "ferveo-gear-common", version = "0.7.0", features = [ - "ark-serde-hex", -], optional = true } -gear-tdec = { workspace = true, optional = true } +gear-tdec = { workspace = true, optional = true, features = ["bls12_381"] } parity-scale-codec = { workspace = true, default-features = false, features = [ "derive", ], optional = true } @@ -103,7 +100,7 @@ codec = ["dep:parity-scale-codec", "dep:scale-info"] keyring = ["std", "serde", "dep:nacl"] serde = ["dep:serde"] peer-id = ["dep:libp2p-identity"] -tdec = ["std", "keyring", "serde", "dep:ferveo-common", "dep:gear-tdec"] +tdec = ["serde", "dep:gear-tdec"] [package.metadata.cargo-shear] # we need it for applying full_crypto feature diff --git a/protocol/gsigner/src/lib.rs b/protocol/gsigner/src/lib.rs index c2e86652ff7..ece8258c6ee 100644 --- a/protocol/gsigner/src/lib.rs +++ b/protocol/gsigner/src/lib.rs @@ -52,12 +52,7 @@ pub mod scheme; pub mod schemes; #[cfg(all(feature = "std", feature = "keyring", feature = "serde"))] pub mod signer; -#[cfg(all( - feature = "std", - feature = "keyring", - feature = "serde", - feature = "tdec" -))] +#[cfg(feature = "tdec")] pub mod tdec; pub mod utils; @@ -91,19 +86,8 @@ pub use scheme::KeystoreOps; pub use signer::Signer; #[cfg(all(feature = "std", feature = "keyring"))] pub use storage::{FilesystemBackend, MemoryBackend, StorageBackend, StorageError, StorageResult}; -#[cfg(all( - feature = "std", - feature = "keyring", - feature = "serde", - feature = "tdec" -))] -pub use { - crate::tdec::{ - BlindedKeyShare, PublicDecryptionContext, TdecDecryptionKey, TdecKeyEntry, TdecKeyStore, - TdecKeypair, TdecPublicKey, - }, - gear_tdec::bls12_381::{CiphertextHeader, DecryptionShareSimple as DecryptionShare}, -}; +#[cfg(feature = "tdec")] +pub use {crate::tdec::*, gear_tdec::bls12_381::DecryptionShareSimple as DecryptionShare}; #[cfg(feature = "secp256k1")] pub use schemes::secp256k1::{ diff --git a/protocol/gsigner/src/tdec/mod.rs b/protocol/gsigner/src/tdec/mod.rs new file mode 100644 index 00000000000..797658afd28 --- /dev/null +++ b/protocol/gsigner/src/tdec/mod.rs @@ -0,0 +1,24 @@ +// Copyright (C) Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +//! Threshold-decryption key storage. +//! +//! This module stores validator threshold-decryption private material separately +//! from signing schemes. It intentionally does not implement [`crate::CryptoScheme`]: +//! these keys create decryption shares, not signatures. + +pub type Bls12_381 = gear_tdec::bls12_381::E; +pub type TdecPublicKey = gear_tdec::keypair_common::PublicKey; +pub type TdecKeypair = gear_tdec::keypair_common::Keypair; +pub type TdecDecryptionKey = gear_tdec::DomainPoint; +pub type BlindedKeyShare = gear_tdec::BlindedKeyShare; +pub type PublicDecryptionContext = gear_tdec::PublicDecryptionContextSimple; + +#[cfg(all(feature = "std", feature = "keyring", feature = "serde"))] +pub mod store; + +#[cfg(all(feature = "std", feature = "keyring", feature = "serde"))] +pub use store::{TdecKeyEntry, TdecKeyStore}; + +#[cfg(all(test, feature = "std", feature = "keyring", feature = "serde"))] +mod tests; diff --git a/protocol/gsigner/src/tdec.rs b/protocol/gsigner/src/tdec/store.rs similarity index 82% rename from protocol/gsigner/src/tdec.rs rename to protocol/gsigner/src/tdec/store.rs index ba2b2bcf4f1..f2c498cd2c6 100644 --- a/protocol/gsigner/src/tdec.rs +++ b/protocol/gsigner/src/tdec/store.rs @@ -1,20 +1,13 @@ -// Copyright (C) Gear Technologies Inc. -// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 - -//! Threshold-decryption key storage. -//! -//! This module stores validator threshold-decryption private material separately -//! from signing schemes. It intentionally does not implement [`crate::CryptoScheme`]: -//! these keys create decryption shares, not signatures. - +use super::{ + BlindedKeyShare, PublicDecryptionContext, TdecDecryptionKey, TdecKeypair, TdecPublicKey, +}; use crate::{ error::{Result, SignerError}, keyring::{self, KeystoreEntry}, }; -use ferveo_common::{Keypair, PublicKey, from_bytes, to_bytes}; use gear_tdec::{ - DomainPoint, PublicDecryptionContextSimple, - bls12_381::{CiphertextHeader, DecryptionShareSimple as DecryptionShare, E}, + bls12_381::{CiphertextHeader, DecryptionShareSimple as DecryptionShare}, + keypair_common::{from_bytes, to_bytes}, }; use hex::ToHex; use serde::{Deserialize, Serialize}; @@ -25,12 +18,6 @@ use std::{ }; use tempfile::TempDir; -pub type TdecPublicKey = PublicKey; -pub type TdecKeypair = Keypair; -pub type TdecDecryptionKey = DomainPoint; -pub type BlindedKeyShare = gear_tdec::BlindedKeyShare; -pub type PublicDecryptionContext = PublicDecryptionContextSimple; - const NAMESPACE_TDEC: &str = "tdec"; /// JSON keyring entry for one validator threshold-decryption key. @@ -307,43 +294,3 @@ fn decode_decryption_key(encoded: &str) -> Result { let bytes = hex::decode(encoded)?; from_bytes(&bytes).map_err(|err| SignerError::InvalidKey(err.to_string())) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn imports_and_gets_validator_decryption_key_by_public_key() { - let mut rng = gear_tdec::rand_utils::test_rng(); - let keypair = TdecKeypair::new(&mut rng); - let store = TdecKeyStore::memory(); - - let public_key = store.import_keypair(keypair).unwrap(); - assert!(store.has_key(&public_key).unwrap()); - assert_eq!( - store.validator_decryption_key(&public_key).unwrap(), - keypair.decryption_key - ); - } - - #[test] - fn creates_decryption_share_from_public_context() { - let mut rng = gear_tdec::rand_utils::test_rng(); - let dealer = gear_tdec::deal::(3, 2, &mut rng); - let context = dealer.private_contexts[0].clone(); - let public_context = context.public_decryption_contexts[context.index].clone(); - let ciphertext = - gear_tdec::encrypt_raw::(b"hello", b"aad", &dealer.public_key, &mut rng).unwrap(); - let header = ciphertext.header(); - let store = TdecKeyStore::memory(); - store - .import_decryption_key(context.validator_decryption_key) - .unwrap(); - - let expected = context.create_share(&header, b"aad").unwrap(); - let actual = store - .create_share(&public_context, &header, b"aad") - .unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/protocol/gsigner/src/tdec/tests.rs b/protocol/gsigner/src/tdec/tests.rs new file mode 100644 index 00000000000..bb7ad3d599d --- /dev/null +++ b/protocol/gsigner/src/tdec/tests.rs @@ -0,0 +1,37 @@ +pub use crate::*; + +#[test] +fn imports_and_gets_validator_decryption_key_by_public_key() { + let mut rng = gear_tdec::rand_utils::test_rng(); + let keypair = TdecKeypair::new(&mut rng); + let store = TdecKeyStore::memory(); + + let public_key = store.import_keypair(keypair).unwrap(); + assert!(store.has_key(&public_key).unwrap()); + assert_eq!( + store.validator_decryption_key(&public_key).unwrap(), + keypair.decryption_key + ); +} + +#[test] +fn creates_decryption_share_from_public_context() { + let mut rng = gear_tdec::rand_utils::test_rng(); + let dealer = gear_tdec::deal::(3, 2, &mut rng); + let context = dealer.private_contexts[0].clone(); + let public_context = context.public_decryption_contexts[context.index].clone(); + let ciphertext = + gear_tdec::encrypt_raw::(b"hello", b"aad", &dealer.public_key, &mut rng) + .unwrap(); + let header = ciphertext.header(); + let store = TdecKeyStore::memory(); + store + .import_decryption_key(context.validator_decryption_key) + .unwrap(); + + let expected = context.create_share(&header, b"aad").unwrap(); + let actual = store + .create_share(&public_context, &header, b"aad") + .unwrap(); + assert_eq!(actual, expected); +} From c136f107c34368b9355f30a91592d6299dd80773 Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmin Date: Wed, 1 Jul 2026 14:58:01 +0400 Subject: [PATCH 41/41] fix: update license headers --- protocol/gsigner/src/tdec/store.rs | 3 +++ protocol/gsigner/src/tdec/tests.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/protocol/gsigner/src/tdec/store.rs b/protocol/gsigner/src/tdec/store.rs index f2c498cd2c6..057b23c807a 100644 --- a/protocol/gsigner/src/tdec/store.rs +++ b/protocol/gsigner/src/tdec/store.rs @@ -1,3 +1,6 @@ +// Copyright (C) Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + use super::{ BlindedKeyShare, PublicDecryptionContext, TdecDecryptionKey, TdecKeypair, TdecPublicKey, }; diff --git a/protocol/gsigner/src/tdec/tests.rs b/protocol/gsigner/src/tdec/tests.rs index bb7ad3d599d..6d0f4c4dce5 100644 --- a/protocol/gsigner/src/tdec/tests.rs +++ b/protocol/gsigner/src/tdec/tests.rs @@ -1,3 +1,6 @@ +// Copyright (C) Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + pub use crate::*; #[test]