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
173 changes: 171 additions & 2 deletions ethexe/common/src/injected.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
use core::hash::Hash;
use gear_core::{limited::LimitedVec, rpc::ReplyInfo};
use gprimitives::{ActorId, H256, MessageId};
use gsigner::{PrivateKey, secp256k1::signature::SignResult};
use gsigner::{PrivateKey, Signature, secp256k1::signature::SignResult};
use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sha3::{Digest, Keccak256};
Expand Down Expand Up @@ -222,7 +222,141 @@
SignedMessage::try_from_parts(promise, *self.0.signature(), self.0.address())
}
}
/// Encoding and decoding of `LimitedVec<u8, N>` as hex string.

/// Receipt for [InjectedTransaction].
#[derive(
Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::IsVariant, derive_more::Unwrap,
)]
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
pub enum TxReceipt {
Promise(Promise),
Error(TransactionError),
}

#[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 SignedFullTxReceipt(SignedMessage<TxReceipt>);

impl TxReceipt {
/// 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,
}
}

pub fn as_compact(&self) -> CompactTxReceipt {
match self {
Self::Promise(promise) => CompactTxReceipt::Promise(promise.to_compact()),
// Clone is cheap here, because the error is just hash + byte.
Self::Error(err) => CompactTxReceipt::Error(err.clone()),
}
}
}

impl ToDigest for TxReceipt {
fn update_hasher(&self, hasher: &mut sha3::Keccak256) {
self.as_compact().update_hasher(hasher);
}
}

#[derive(
Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::IsVariant, derive_more::Unwrap,
)]
pub enum CompactTxReceipt {
Promise(CompactPromise),
Error(TransactionError),
}

#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Deref, derive_more::From)]
pub struct SignedCompactTxReceipt(SignedMessage<CompactTxReceipt>);

impl CompactTxReceipt {
/// 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 ToDigest for CompactTxReceipt {
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);
}
}
}
}

impl SignedCompactTxReceipt {
pub fn as_promise_with_signature(&self) -> Option<(&CompactPromise, &Signature, Address)> {
let CompactTxReceipt::Promise(compact) = self.0.data() else {
return None;
};
Some((compact, self.0.signature(), self.0.address()))
}

pub fn as_full_receipt_error(&self) -> Option<SignedFullTxReceipt> {
let CompactTxReceipt::Error(error) = self.0.data() else {
return None;
};

let (signature, address) = (*self.0.signature(), self.0.address());
let message = unsafe {
SignedMessage::from_parts_unchecked(TxReceipt::Error(error.clone()), signature, address)
};
Some(message.into())
}
}

/// 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) {
let Self { tx_hash, reason } = self;
hasher.update(tx_hash.inner().0);
hasher.update([reason.variant_index()]);
}
}

/// Reason why transaction was not executed in chain.
#[repr(u8)]
#[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 344 in ethexe/common/src/injected.rs

View workflow job for this annotation

GitHub Actions / check / typos

"oudated" should be "outdated".
Outdated = 1,

// 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 = 2,
}

impl TransactionErrorReason {
pub fn variant_index(&self) -> u8 {
unsafe { (self as *const TransactionErrorReason).cast::<u8>().read() }
}
}

/// 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 Expand Up @@ -330,4 +464,39 @@
compact_signed_promise.signature().clone()
);
}

#[test]
fn tx_receipt_has_the_same_hash_for_promise() {
let pk = PrivateKey::random();
let promise = Promise::mock(());
let compact_promise = promise.to_compact();

let receipt_promise = TxReceipt::Promise(promise);
let receipt_compact_promise = CompactTxReceipt::Promise(compact_promise);
assert_eq!(
receipt_promise.to_digest(),
receipt_compact_promise.to_digest()
);

let signed_receipt = SignedMessage::create(pk.clone(), receipt_promise).unwrap();
let signed_compact_receipt = SignedMessage::create(pk, receipt_compact_promise).unwrap();

assert_eq!(
*signed_receipt.signature(),
*signed_compact_receipt.signature()
);
assert_eq!(signed_receipt.address(), signed_compact_receipt.address());
}

#[test]
fn tx_receipt_has_the_same_hash_for_error() {
let error = TransactionError {
tx_hash: unsafe { HashOf::new(H256::random()) },
reason: TransactionErrorReason::Outdated,
};
let receipt1 = TxReceipt::Error(error.clone());
let receipt2 = CompactTxReceipt::Error(error);

assert_eq!(receipt1.to_digest(), receipt2.to_digest());
}
}
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::{Promise, SignedCompactTxReceipt, SignedInjectedTransaction},
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(SignedCompactTxReceipt),
// #[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
Loading
Loading