Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 12 additions & 3 deletions ethexe/cli/src/commands/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ use clap::{Parser, Subcommand};
use ethexe_common::{
Address, BlockHeader, SimpleBlockData,
gear_core::{ids::prelude::CodeIdExt, limited::LimitedVec, rpc::ReplyInfo},
injected::{AddressedInjectedTransaction, InjectedTransaction, MAX_INJECTED_TX_PAYLOAD_SIZE},
injected::{
AddressedInjectedTransaction, InjectedTransaction, MAX_INJECTED_TX_PAYLOAD_SIZE, TxReceipt,
},
};
use ethexe_ethereum::{Ethereum, EthereumBuilder, mirror::ClaimInfo, router::CodeValidationResult};
use ethexe_rpc::{InjectedClient, ProgramClient};
Expand Down Expand Up @@ -1126,12 +1128,19 @@ impl TxCommand {
|| "failed to send injected transaction to Vara.eth RPC",
)?;

let promise = subscription
let receipt = subscription
.next()
.await
.ok_or_else(|| anyhow!("no promise received from subscription"))?
.with_context(|| "failed to receive transaction promise")?
.into_data();
.data()
.clone();
let promise = match receipt {
TxReceipt::Promise(promise) => promise,
TxReceipt::Error(err) => {
return Err(anyhow!("injected transaction failed: {err:?}"));
}
};
let ReplyInfo {
payload,
value,
Expand Down
1 change: 1 addition & 0 deletions ethexe/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
sha3.workspace = true
k256 = { version = "0.13.4", features = ["ecdsa"], default-features = false }
nonempty.workspace = true
thiserror.workspace = true

Check failure on line 31 in ethexe/common/Cargo.toml

View workflow job for this annotation

GitHub Actions / check / unused-deps

shear/unused_dependency

unused dependency `thiserror` (remove this dependency)

# mock deps
itertools = { workspace = true, optional = true }
Expand Down
120 changes: 119 additions & 1 deletion ethexe/common/src/injected.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

use crate::{Address, HashOf, ToDigest, ecdsa::SignedMessage};
use alloc::string::{String, ToString};
use anyhow::bail;
use core::hash::Hash;
use gear_core::{limited::LimitedVec, rpc::ReplyInfo};
use gprimitives::{ActorId, H256, MessageId};
Expand Down Expand Up @@ -222,7 +223,124 @@
SignedMessage::try_from_parts(promise, *self.0.signature(), self.0.address())
}
}
/// Encoding and decoding of `LimitedVec<u8, N>` as hex string.

/// Receipt for [InjectedTransaction].
///
/// This type is a generic over Promise type is purpose to allow transfer
/// [CompactPromise] between validators and send full [Promise] only to end-user.
#[derive(
Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::IsVariant, derive_more::Unwrap,
)]
#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
pub enum TxReceipt<P = Promise> {
Promise(P),
Error(TransactionError),
}

impl TxReceipt<Promise> {
/// Returns the transaction hash the receipt belongs to
pub fn tx_hash(&self) -> HashOf<InjectedTransaction> {
match self {
Self::Promise(promise) => promise.tx_hash,
Self::Error(err) => err.tx_hash,
}
}
}

impl TxReceipt<CompactPromise> {
/// Returns the transaction hash the receipt belongs to
pub fn tx_hash(&self) -> HashOf<InjectedTransaction> {
match self {
Self::Promise(promise) => promise.tx_hash,
Self::Error(err) => err.tx_hash,
}
}
}

/// Signed wrapper on top of [TxReceipt].
#[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<P: ToDigest = Promise>(SignedMessage<TxReceipt<P>>);

// TODO: these implementations are not clear anough, redesign
impl SignedTxReceipt<CompactPromise> {
pub fn try_map_promise(&self, promise: Promise) -> Result<SignedTxReceipt, &'static str> {
let mapped_receipt = match self.0.data() {
TxReceipt::Promise(_) => TxReceipt::Promise(promise),
TxReceipt::Error(_) => return Err("Expected for this variant a promise, not error"),
};

let (address, signature) = (self.0.address(), *self.0.signature());
SignedMessage::try_from_parts(mapped_receipt, signature, address).map(Into::into)
}

pub fn try_map_error(self) -> anyhow::Result<SignedTxReceipt> {
let address = self.0.address();
let (receipt, signature) = self.0.into_parts();

match receipt {
TxReceipt::Promise(_) => bail!(
"SignedTxReceipt<CompactPromise>::map_error expecting to be call on error variant"
),
// TODO: optimize me
TxReceipt::Error(err) => {
Ok(
SignedMessage::try_from_parts(TxReceipt::Error(err), signature, address)
.map(Into::into)
.expect("Infallible"),
)
}
}
}
}

impl<P: ToDigest> ToDigest for TxReceipt<P> {
fn update_hasher(&self, hasher: &mut sha3::Keccak256) {
match self {
Self::Promise(promise) => {
hasher.update([0]);
hasher.update(promise.to_digest().0);
}
Self::Error(err) => {
hasher.update([1]);
hasher.update(err.to_digest().0);
}
}
}
}

/// Represents the reason why [InjectedTransaction] was not included.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
pub struct TransactionError {
pub tx_hash: HashOf<InjectedTransaction>,
pub reason: TransactionErrorReason,
}

impl ToDigest for TransactionError {
fn update_hasher(&self, _hasher: &mut sha3::Keccak256) {
todo!()
}
}

// TODO: think about creating the general error for `TxValidity` and `ErrorReason`

/// Reason why transaction was not executed in chain.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, derive_more::Display)]
#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
pub enum TransactionErrorReason {
/// Transaction is outdated and can not be included.
#[display("Transaction is oudated")]

Check warning on line 334 in ethexe/common/src/injected.rs

View workflow job for this annotation

GitHub Actions / check / typos

"oudated" should be "outdated".
Outdated,

// Important: Keep it in the end of enum.
// In future we will support non zero value injected txs.
#[display("Transaction's value must be zero")]
NonZeroValue,
}

/// Encoding and decoding of [LimitedVec<u8, N>] as hex string.
#[cfg(feature = "std")]
mod serde_hex {
pub fn serialize<S, const N: usize>(
Expand Down
6 changes: 2 additions & 4 deletions ethexe/consensus/src/announces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -714,17 +714,15 @@ pub fn accept_announce(db: &impl DBAnnouncesExt, announce: Announce) -> Result<A
let tx_checker = TxValidityChecker::new_for_announce(db, block, announce.parent)?;

for tx in announce.injected_transactions.iter() {
let validity_status = tx_checker.check_tx_validity(tx)?;

match validity_status {
match tx_checker.check_tx_validity(tx)? {
TxValidity::Valid => {
db.set_injected_transaction(tx.clone());
}

validity => {
tracing::trace!(
announce = ?announce.to_hash(),
"announce contains invalid transition with status {validity_status:?}, rejecting announce."
"announce contains invalid transition with status {validity:?}, rejecting announce."
);

return Ok(AnnounceStatus::Rejected {
Expand Down
12 changes: 7 additions & 5 deletions ethexe/consensus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
//! - `ethexe-network` delivers producer announces, validation requests
//! and replies, fetched announces and network-forwarded injected
//! transactions. Outgoing network messages leave as
//! [`ConsensusEvent::PublishMessage`], [`ConsensusEvent::PublishPromise`]
//! [`ConsensusEvent::PublishMessage`], [`ConsensusEvent::PublishTxReceipt`]
//! and [`ConsensusEvent::RequestAnnounces`].
//! - `ethexe-ethereum` is reached only from [`ValidatorService`], through
//! the [`BatchCommitter`] trait, to submit aggregated batch
Expand Down Expand Up @@ -92,8 +92,8 @@
//! |--------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
//! | [`AnnounceAccepted`](ConsensusEvent::AnnounceAccepted) / [`AnnounceRejected`](ConsensusEvent::AnnounceRejected) | Informational result of validating a received producer announce. |
//! | [`ComputeAnnounce`](ConsensusEvent::ComputeAnnounce) | The outer service must hand this announce to `ethexe-compute`, with the given `PromisePolicy`. |
//! | [`PublishMessage`](ConsensusEvent::PublishMessage) | Signed validator-to-validator message to gossip over the network. |
//! | [`PublishPromise`](ConsensusEvent::PublishPromise) | Signed promise to gossip over the network and deliver to RPC subscribers. |
//! | [`PublishMessage`](ConsensusEvent::PublishMessage) | Signed validator-to-validator message to gossip over the network. |
//! | [`PublishTxReceipt`](ConsensusEvent::PublishTxReceipt) | Signed transaction receipt to gossip over the network and deliver to RPC subscribers. |
//! | [`RequestAnnounces`](ConsensusEvent::RequestAnnounces) | Ask the network to fetch announces we are missing. |
//! | [`CommitmentSubmitted`](ConsensusEvent::CommitmentSubmitted) | Informational: a batch was successfully submitted to the Router contract. |
//! | [`Warning`](ConsensusEvent::Warning) | Informational: a non-fatal anomaly (unexpected input, bad reply, etc.) was detected. |
Expand Down Expand Up @@ -203,7 +203,7 @@ use anyhow::Result;
use ethexe_common::{
Announce, Digest, HashOf, PromisePolicy, SimpleBlockData,
consensus::{BatchCommitmentValidationReply, VerifiedAnnounce, VerifiedValidationRequest},
injected::{Promise, SignedCompactPromise, SignedInjectedTransaction},
injected::{CompactPromise, Promise, SignedInjectedTransaction, SignedTxReceipt},
network::{AnnouncesRequest, AnnouncesResponse, SignedValidatorMessage},
};
use futures::{Stream, stream::FusedStream};
Expand Down Expand Up @@ -287,7 +287,9 @@ pub enum ConsensusEvent {
#[from]
PublishMessage(SignedValidatorMessage),
#[from]
PublishPromise(SignedCompactPromise),
PublishTxReceipt(SignedTxReceipt<CompactPromise>),
// #[from]
// PublishTransactionResult(SignedTransactionResult),
/// Outer service have to request announces
#[from]
RequestAnnounces(AnnouncesRequest),
Expand Down
21 changes: 10 additions & 11 deletions ethexe/consensus/src/tx_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ use ethexe_common::{
Announce, HashOf, ProgramStates, SimpleBlockData,
db::{AnnounceStorageRO, GlobalsStorageRO, OnChainStorageRO},
gear::INJECTED_MESSAGE_PANIC_GAS_CHARGE_THRESHOLD,
injected::{InjectedTransaction, SignedInjectedTransaction, VALIDITY_WINDOW},
injected::{
InjectedTransaction, SignedInjectedTransaction, TransactionErrorReason, VALIDITY_WINDOW,
},
};
use ethexe_runtime_common::state::Storage;
use gprimitives::H256;
Expand All @@ -38,20 +40,17 @@ pub enum TxValidity {
Valid,
/// Transaction was already include into one of previous [`VALIDITY_WINDOW`] announces.
Duplicate,
/// Transaction is outdated and should be remove from pool.
Outdated,
/// Transaction's reference block not on current branch.
/// Keep tx in pool in case of reorg.
NotOnCurrentBranch,
/// Transaction's destination [`gprimitives::ActorId`] not found.
UnknownDestination,
/// Transaction's destination [`gprimitives::ActorId`] not initialized.
UninitializedDestination,
// TODO: #5083 support non zero value transactions.
/// Transaction with non zero value is not supported for now.
NonZeroValue,
/// Transaction's destination contract has insufficient balance for injected messages.
InsufficientBalanceForInjectedMessages,
/// Transaction must be remove from pool because of [TransactionErrorReason].
MustRemove(TransactionErrorReason),
}

pub struct TxValidityChecker<DB> {
Expand Down Expand Up @@ -102,11 +101,11 @@ impl<DB: OnChainStorageRO + AnnounceStorageRO + GlobalsStorageRO + Storage> TxVa
let reference_block = tx.data().reference_block;

if tx.data().value != 0 {
return Ok(TxValidity::NonZeroValue);
return Ok(TxValidity::MustRemove(TransactionErrorReason::NonZeroValue));
}

if !self.is_reference_block_within_validity_window(reference_block)? {
return Ok(TxValidity::Outdated);
return Ok(TxValidity::MustRemove(TransactionErrorReason::Outdated));
}

if !self.is_reference_block_on_current_branch(reference_block)? {
Expand Down Expand Up @@ -337,7 +336,7 @@ mod tests {
for block in chain.blocks.iter().take(VALIDITY_WINDOW as usize) {
let tx = mock_tx(block.hash);
assert_eq!(
TxValidity::Outdated,
TxValidity::MustRemove(TransactionErrorReason::Outdated),
tx_checker.check_tx_validity(&tx).unwrap()
);
}
Expand Down Expand Up @@ -414,7 +413,7 @@ mod tests {
TxValidityChecker::new_for_announce(db, chain_head, announce_hash).unwrap();

assert_eq!(
TxValidity::NonZeroValue,
TxValidity::MustRemove(TransactionErrorReason::NonZeroValue),
tx_checker.check_tx_validity(&signed_tx(tx)).unwrap()
);
}
Expand All @@ -432,7 +431,7 @@ mod tests {
TxValidityChecker::new_for_announce(db, chain_head, announce_hash).unwrap();

assert_eq!(
TxValidity::Outdated,
TxValidity::MustRemove(TransactionErrorReason::Outdated),
tx_checker.check_tx_validity(&signed_tx(tx)).unwrap()
);
}
Expand Down
33 changes: 22 additions & 11 deletions ethexe/consensus/src/validator/producer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ use super::{
use crate::{
ConsensusEvent,
announces::{self, DBAnnouncesExt},
validator::DefaultProcessing,
validator::{DefaultProcessing, tx_pool::PoolDelta},
};
use anyhow::{Context as _, Result, anyhow};
use derive_more::{Debug, Display};
use ethexe_common::{
Announce, HashOf, PromisePolicy, SimpleBlockData, ValidatorsVec,
db::BlockMetaStorageRO,
gear::BatchCommitment,
injected::{Promise, SignedCompactPromise},
injected::{Promise, TxReceipt},
network::ValidatorMessage,
};
use ethexe_service_utils::Timer;
Expand Down Expand Up @@ -117,16 +117,14 @@ impl StateHandler for Producer {
State::WaitingAnnounceComputed(expected) if *expected == announce_hash => {
let tx_hash = promise.tx_hash;

let signed_promise =
self.ctx
.core
.signer
.signed_message(self.ctx.core.pub_key, promise, None)?;
let compact_signed_promise =
SignedCompactPromise::from_signed_promise(&signed_promise);
let signed_receipt = self.ctx.core.signer.signed_message(
self.ctx.core.pub_key,
TxReceipt::Promise(promise.to_compact()),
None,
)?;

self.ctx
.output(ConsensusEvent::PublishPromise(compact_signed_promise));
.output(ConsensusEvent::PublishTxReceipt(signed_receipt.into()));

tracing::trace!("consensus sign promise for transaction-hash={tx_hash}");
Ok(self.into())
Expand Down Expand Up @@ -204,12 +202,25 @@ impl Producer {
self.ctx.core.commitment_delay_limit,
)?;

let injected_transactions = self
let PoolDelta {
selected: injected_transactions,
removed,
} = self
.ctx
.core
.injected_pool
.select_for_announce(self.block, parent)?;

for err in removed.into_iter() {
let signed_receipt = self.ctx.core.signer.signed_message(
self.ctx.core.pub_key,
TxReceipt::Error(err),
None,
)?;
self.ctx
.output(ConsensusEvent::PublishTxReceipt(signed_receipt.into()));
}

let announce = Announce {
block_hash: self.block.hash,
parent,
Expand Down
Loading
Loading