Skip to main content

ethexe_common/
injected.rs

1// Copyright (C) Gear Technologies Inc.
2// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
3
4use crate::{Address, HashOf, ToDigest, ecdsa::SignedMessage};
5use alloc::string::{String, ToString};
6use core::hash::Hash;
7use gear_core::{limited::LimitedVec, rpc::ReplyInfo};
8use gprimitives::{ActorId, H256, MessageId};
9use gsigner::Signature;
10use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
11use scale_info::TypeInfo;
12use sha3::{Digest, Keccak256};
13
14/// Recent block hashes window size used to check transaction mortality.
15pub const VALIDITY_WINDOW: u8 = 32;
16
17/// Maximum size of single injected transaction payload.
18///
19/// Limited by the maximum injected transactions size per MB.
20/// Currently is 126 KiB.
21pub const MAX_INJECTED_TX_PAYLOAD_SIZE: usize = 126 * 1024;
22
23/// Maximum size of injected transaction salt.
24pub const MAX_INJECTED_TX_SALT_SIZE: usize = 32;
25
26/// Maximum cumulative SCALE-encoded size of [`SignedInjectedTransaction`]s
27/// that a single MB may carry. 127 KiB leaves ~1 KiB of headroom over the
28/// per-tx [`MAX_INJECTED_TX_PAYLOAD_SIZE`] for signature and other
29/// envelope bytes, so at least one tx of the maximum payload size is
30/// always admissible.
31pub const MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB: usize = 127 * 1024;
32
33#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
34#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)]
35pub enum InjectedTransactionAcceptance {
36    Accept,
37    Reject { reason: String },
38}
39
40impl<E: ToString> From<Result<(), E>> for InjectedTransactionAcceptance {
41    fn from(value: Result<(), E>) -> Self {
42        match value {
43            Ok(()) => Self::Accept,
44            Err(err) => Self::Reject {
45                reason: err.to_string(),
46            },
47        }
48    }
49}
50
51pub type SignedInjectedTransaction = SignedMessage<InjectedTransaction>;
52
53#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
54#[cfg_attr(feature = "serde", derive(Hash))]
55#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
56pub struct AddressedInjectedTransaction {
57    /// Address of validator the transaction intended for
58    pub recipient: Address,
59    pub tx: SignedInjectedTransaction,
60}
61
62/// IMPORTANT: message id == tx hash.
63#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
64#[cfg_attr(feature = "serde", derive(Hash))]
65#[derive(Debug, Clone, Encode, Decode, MaxEncodedLen, TypeInfo, PartialEq, Eq)]
66pub struct InjectedTransaction {
67    /// Destination program inside `Vara.eth`.
68    pub destination: ActorId,
69    /// Payload of the message.
70    #[cfg_attr(feature = "std", serde(with = "serde_hex"))]
71    pub payload: LimitedVec<u8, MAX_INJECTED_TX_PAYLOAD_SIZE>,
72    /// Value attached to the message.
73    /// NOTE: at this moment will be zero.
74    pub value: u128,
75    /// Reference block number.
76    pub reference_block: H256,
77    /// Arbitrary bytes to allow multiple synonymous
78    /// transactions to be sent simultaneously.
79    /// NOTE: this is also a salt for MessageId generation.
80    #[cfg_attr(feature = "std", serde(with = "serde_hex"))]
81    pub salt: LimitedVec<u8, MAX_INJECTED_TX_SALT_SIZE>,
82}
83
84// Destination + payload_hash + value + ref_block + salt_hash
85const INJECTED_TX_HASHABLE_SIZE: usize = size_of::<ActorId>()
86    + size_of::<H256>()
87    + size_of::<u128>()
88    + size_of::<H256>()
89    + size_of::<H256>();
90
91impl InjectedTransaction {
92    /// Helper function that returns bytes of [InjectedTransaction]
93    /// that will be hashed by blake2b256 or keccak256.
94    fn to_hashable_bytes(&self) -> [u8; INJECTED_TX_HASHABLE_SIZE] {
95        let Self {
96            destination,
97            payload,
98            value,
99            reference_block,
100            salt,
101        } = self;
102
103        let mut hashable_bytes = [0u8; INJECTED_TX_HASHABLE_SIZE];
104        let mut offset = 0;
105
106        let mut append = |slice: &[u8]| {
107            let next_offset = offset + slice.len();
108            hashable_bytes[offset..next_offset].copy_from_slice(slice);
109            offset = next_offset;
110        };
111
112        append(destination.as_ref());
113        append(gear_core::utils::hash(payload).as_ref());
114        append(value.to_be_bytes().as_ref());
115        append(reference_block.0.as_ref());
116        append(gear_core::utils::hash(salt).as_ref());
117
118        hashable_bytes
119    }
120
121    /// Returns the hash of [`InjectedTransaction`].
122    pub fn to_hash(&self) -> HashOf<InjectedTransaction> {
123        let hashable_bytes = self.to_hashable_bytes();
124        unsafe { HashOf::new(gear_core::utils::hash(hashable_bytes.as_ref()).into()) }
125    }
126
127    /// Creates [`MessageId`] from [`InjectedTransaction`].
128    pub fn to_message_id(&self) -> MessageId {
129        MessageId::new(self.to_hash().inner().0)
130    }
131}
132
133impl ToDigest for InjectedTransaction {
134    fn update_hasher(&self, hasher: &mut Keccak256) {
135        let hashable_bytes = self.to_hashable_bytes();
136        hasher.update(hashable_bytes);
137    }
138}
139
140/// [`Promise`] represents the guaranteed reply for [`InjectedTransaction`].
141///
142/// Note: Validator must ensure the validity of the promise, because of it can be slashed for
143/// providing an invalid promise.
144#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
145#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq, Hash)]
146pub struct Promise {
147    /// Hash of the injected transaction this reply corresponds to.
148    pub tx_hash: HashOf<InjectedTransaction>,
149    /// Reply data for injected message.
150    pub reply: ReplyInfo,
151}
152
153impl Promise {
154    /// Calculates the `blake2b` hash from promise's reply.
155    pub fn reply_hash(&self) -> HashOf<ReplyInfo> {
156        // Safety by implementation
157        unsafe { HashOf::new(self.reply.to_hash()) }
158    }
159
160    /// Converts promise to its compact version.
161    pub fn to_compact(&self) -> CompactPromise {
162        CompactPromise {
163            tx_hash: self.tx_hash,
164            reply_hash: self.reply_hash(),
165        }
166    }
167}
168
169impl ToDigest for Promise {
170    fn update_hasher(&self, hasher: &mut sha3::Keccak256) {
171        self.to_compact().update_hasher(hasher);
172    }
173}
174
175/// The hashes of [`Promise`] parts.
176#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
177pub struct CompactPromise {
178    pub tx_hash: HashOf<InjectedTransaction>,
179    pub reply_hash: HashOf<ReplyInfo>,
180}
181
182impl ToDigest for CompactPromise {
183    fn update_hasher(&self, hasher: &mut sha3::Keccak256) {
184        let Self {
185            tx_hash,
186            reply_hash,
187        } = self;
188
189        hasher.update(tx_hash.inner());
190        hasher.update(reply_hash.inner());
191    }
192}
193
194mod sealed {
195    pub trait Sealed {}
196
197    impl Sealed for super::Promise {}
198    impl Sealed for super::CompactPromise {}
199}
200
201pub trait PromiseKind: sealed::Sealed {
202    fn tx_hash(&self) -> HashOf<InjectedTransaction>;
203}
204
205impl PromiseKind for Promise {
206    fn tx_hash(&self) -> HashOf<InjectedTransaction> {
207        self.tx_hash
208    }
209}
210
211impl PromiseKind for CompactPromise {
212    fn tx_hash(&self) -> HashOf<InjectedTransaction> {
213        self.tx_hash
214    }
215}
216
217/// Receipt for [InjectedTransaction].
218///
219/// This type generic over promise type in purpose to support both
220/// [CompactPromise] and [Promise].
221///
222/// [CompactPromise] generic uses only for transport purposes. End user
223/// always receives the full version.
224///
225/// **Important**: `Receipt<CompactPromise>` and `Receipt<Promise>` have the same
226///     digest. So it helps to reuses the producer's signature to construct the full
227///     version from compact.
228#[derive(
229    Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::IsVariant, derive_more::Unwrap,
230)]
231#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
232pub enum Receipt<P> {
233    Promise(P),
234    /// No promise, transaction wasn't executed.
235    Purged(PurgedTransaction),
236}
237
238impl<P: PromiseKind> Receipt<P> {
239    pub fn tx_hash(&self) -> HashOf<InjectedTransaction> {
240        match self {
241            Self::Promise(promise) => promise.tx_hash(),
242            Self::Purged(purged) => purged.tx_hash,
243        }
244    }
245}
246
247impl<P: ToDigest> ToDigest for Receipt<P> {
248    fn update_hasher(&self, hasher: &mut sha3::Keccak256) {
249        match self {
250            Self::Promise(promise) => {
251                hasher.update([0]);
252                promise.update_hasher(hasher);
253            }
254            Self::Purged(err) => {
255                hasher.update([1]);
256                err.update_hasher(hasher);
257            }
258        }
259    }
260}
261
262/// Signed [Receipt] with a [Promise] generic.
263/// End RPC user always receives this object.
264#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::From, derive_more::Deref)]
265#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
266#[cfg_attr(feature = "std", serde(transparent))]
267pub struct SignedTxReceipt(SignedMessage<Receipt<Promise>>);
268
269/// Signed [Receipt] with a [CompactPromise] generic.
270/// It is used as a lightweight transfer type
271#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Deref, derive_more::From)]
272pub struct SignedCompactTxReceipt(SignedMessage<Receipt<CompactPromise>>);
273
274/// The result of [upgrade](SignedCompactTxReceipt::upgrade) function.
275/// [Ready](Self::Ready) means that receipt contains an error and was upgraded
276/// to full version.
277/// [Pending](Self::Pending) means that receipt contains a promise and requires the
278/// full promise body to restore receipt.
279#[derive(Debug, PartialEq, Eq, derive_more::From)]
280pub enum UpgradedReceipt {
281    Pending(UnfilledPromiseReceipt),
282    Ready(SignedTxReceipt),
283}
284
285impl SignedCompactTxReceipt {
286    /// Upgrades the compact receipt to its full version ([SignedTxReceipt]).
287    pub fn upgrade(self) -> UpgradedReceipt {
288        let (receipt, signature, address) = self.0.into_parts_full();
289
290        match receipt {
291            Receipt::Promise(compact) => {
292                UpgradedReceipt::Pending(UnfilledPromiseReceipt(compact, signature, address))
293            }
294            Receipt::Purged(purged) => UpgradedReceipt::Ready(unsafe {
295                // SAFETY: Receipt::Purged has the same digest representation for both
296                // Promise and CompactPromise generics, so the signature remains valid.
297                SignedMessage::from_parts_unchecked(Receipt::Purged(purged), signature, address)
298                    .into()
299            }),
300        }
301    }
302}
303
304/// Intermediate type between receipt's states [SignedCompactTxReceipt] and [SignedTxReceipt].
305/// Use method [try_fill_with](Self::try_fill_with) to build the [SignedTxReceipt].
306#[derive(Debug, Clone, PartialEq, Eq, derive_more::Deref)]
307pub struct UnfilledPromiseReceipt(#[deref] CompactPromise, Signature, Address);
308
309/// The result of [try_fill_with](UnfilledPromiseReceipt::try_fill_with) function.
310/// [Filled](Self::Filled) means the successful result.
311/// [HashesMismatch](Self::HashesMismatch) means that raw promise body and stored compact are not the same promise.
312pub enum TryFillPromiseResult {
313    Filled(SignedTxReceipt),
314    HashesMismatch(UnfilledPromiseReceipt),
315}
316
317impl UnfilledPromiseReceipt {
318    pub fn try_fill_with(self, promise: Promise) -> TryFillPromiseResult {
319        if self.0 != promise.to_compact() {
320            return TryFillPromiseResult::HashesMismatch(self);
321        }
322        let Self(.., signature, address) = self;
323        TryFillPromiseResult::Filled(unsafe {
324            SignedMessage::from_parts_unchecked(Receipt::Promise(promise), signature, address)
325                .into()
326        })
327    }
328}
329
330/// Represents the reason why [InjectedTransaction] was not included.
331#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Display)]
332#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
333#[display("Injected transaction wasn't executed: tx_hash={tx_hash}, reason={reason}")]
334pub struct PurgedTransaction {
335    pub tx_hash: HashOf<InjectedTransaction>,
336    pub reason: TransactionPurgedReason,
337}
338
339impl ToDigest for PurgedTransaction {
340    fn update_hasher(&self, hasher: &mut sha3::Keccak256) {
341        let Self { tx_hash, reason } = self;
342        hasher.update(tx_hash.inner().0);
343        hasher.update([reason.variant_index()]);
344    }
345}
346
347/// Reason why transaction was not executed in chain.
348#[repr(u8)]
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, derive_more::Display)]
350#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
351pub enum TransactionPurgedReason {
352    /// The transaction references an outdated block and cannot be included.
353    #[display("transaction reference block is outdated")]
354    Outdated = 1,
355    /// The transaction references a block that is not known locally.
356    #[display("transaction reference block is unknown")]
357    UnknownReferenceBlock = 2,
358
359    /// The transaction has a non-zero value, which is not supported yet.
360    ///
361    /// Note: keep this variant at the end of the enum. The `u8::MAX`
362    /// discriminant intentionally leaves values `3..=254` available for
363    /// future purge reasons, including non-zero-value injected transactions.
364    #[display("transaction value must be zero")]
365    NonZeroValue = u8::MAX,
366}
367
368impl TransactionPurgedReason {
369    pub fn variant_index(&self) -> u8 {
370        *self as u8
371    }
372}
373
374/// Encoding and decoding of [LimitedVec<u8, N>] as hex string.
375#[cfg(feature = "std")]
376mod serde_hex {
377    pub fn serialize<S, const N: usize>(
378        data: &super::LimitedVec<u8, N>,
379        serializer: S,
380    ) -> Result<S::Ok, S::Error>
381    where
382        S: serde::Serializer,
383    {
384        alloy_primitives::hex::serialize(data.to_vec(), serializer)
385    }
386
387    pub fn deserialize<'de, D, const N: usize>(
388        deserializer: D,
389    ) -> Result<super::LimitedVec<u8, N>, D::Error>
390    where
391        D: serde::Deserializer<'de>,
392    {
393        let vec: Vec<u8> = alloy_primitives::hex::deserialize(deserializer)?;
394        super::LimitedVec::<u8, N>::try_from(vec)
395            .map_err(|_| serde::de::Error::custom("LimitedVec deserialization overflow"))
396    }
397}
398
399#[cfg(all(test, feature = "mock"))]
400mod tests {
401    use gsigner::PrivateKey;
402
403    use super::*;
404    use crate::mock::Mock;
405
406    #[test]
407    fn signed_message_and_injected_transactions() {
408        const RPC_INPUT: &str = r#"{
409            "data": {
410                "destination": "0xede8c947f1ce1a5add6c26c2db01ad1dcd377c72",
411                "payload": "0x",
412                "value": 0,
413                "reference_block": "0xb03574ea84ef2acbdbc8c04f8afb73c9d59f2fbd3bf82f37dcb2aa390372b702",
414                "salt": "0x6c6db263a31830e072ea7f083e6a818df3074119be6eee60601a5f2f668db508"
415            },
416            "signature": "0x030a25167f5b18aba302c16226a1f5e590bba1adf5c49430040518416d3caac41d7f5b8c5df142d3c6db2a8e36ca0ca3f42640441d980c54b0847ada2580000f1b",
417            "address": "0xfb2f65ffad2971b699097990ab7a1d4ac35bd0ff"
418        }"#;
419
420        let signed_tx: SignedInjectedTransaction =
421            serde_json::from_str(RPC_INPUT).expect("failed to deserialize SignedMessage");
422
423        // AKA tx_hash
424        assert_eq!(
425            hex::encode(signed_tx.data().to_message_id()),
426            "70ab92fb3161d1feefbd4793ed1217574e71c802d4d8af01648863d3ba7e37c1"
427        );
428
429        assert_eq!(
430            hex::encode(signed_tx.address().0),
431            "fb2f65ffad2971b699097990ab7a1d4ac35bd0ff"
432        );
433
434        assert_eq!(
435            signed_tx
436                .signature()
437                .recover_message(signed_tx.data())
438                .expect("failed to recover message")
439                .to_address(),
440            signed_tx.address()
441        );
442    }
443
444    /// Ported from master's `tx_pool::tests::validate_max_tx_size`.
445    /// One full-size [`SignedInjectedTransaction`] must always fit within
446    /// the per-MB cumulative size cap; otherwise the largest legal tx
447    /// could never be admitted.
448    #[test]
449    fn max_signed_injected_tx_fits_per_mb_cap() {
450        assert!(
451            SignedInjectedTransaction::max_encoded_len() <= MAX_INJECTED_TRANSACTIONS_SIZE_PER_MB
452        );
453    }
454
455    #[test]
456    fn promise_hashes_digest_equal_to_promise_digest() {
457        let promise = Promise::mock(());
458
459        assert_eq!(promise.to_digest(), promise.to_compact().to_digest());
460    }
461
462    #[test]
463    fn shifted_bytes_change_injected_tx_hash() {
464        let initial_tx = InjectedTransaction {
465            destination: ActorId::zero(),
466            payload: vec![1u8, 2u8, 3u8, 4u8].try_into().unwrap(),
467            value: 100,
468            reference_block: H256::random(),
469            salt: vec![1u8, 2u8].try_into().unwrap(),
470        };
471
472        let malicious_tx = {
473            let mut shifted_tx = initial_tx.clone();
474
475            let mut payload = shifted_tx.payload.into_vec();
476            let payload_last_byte = payload.pop().unwrap();
477            shifted_tx.payload = payload.try_into().unwrap();
478
479            let mut value_be = shifted_tx.value.to_be_bytes();
480            let value_last_byte = value_be[15];
481            value_be.copy_within(0..15, 1);
482            value_be[0] = payload_last_byte;
483            shifted_tx.value = u128::from_be_bytes(value_be);
484
485            let mut ref_block_data = shifted_tx.reference_block.0;
486            let last_ref_block = ref_block_data[31];
487
488            ref_block_data.copy_within(0..31, 1);
489            ref_block_data[0] = value_last_byte;
490
491            shifted_tx.reference_block = H256(ref_block_data);
492
493            let mut salt = shifted_tx.salt.clone().into_vec();
494            salt.insert(0, last_ref_block);
495            shifted_tx.salt = salt.try_into().unwrap();
496
497            shifted_tx
498        };
499
500        let tx_concat_bytes = |tx: &InjectedTransaction| -> Vec<u8> {
501            [
502                tx.destination.as_ref(),
503                tx.payload.as_ref(),
504                tx.value.to_be_bytes().as_ref(),
505                tx.reference_block.0.as_ref(),
506                tx.salt.as_ref(),
507            ]
508            .concat()
509        };
510
511        // Assert that transactions have the same concatenated bytes.
512        // In earlier hash implementation it will lead to the same tx hashes.
513        assert_eq!(tx_concat_bytes(&initial_tx), tx_concat_bytes(&malicious_tx));
514
515        // Assert that current hash implementation return different hashes for transactions
516        // that have equal concatenated bytes.
517        assert_ne!(initial_tx.to_hash(), malicious_tx.to_hash());
518    }
519
520    #[test]
521    fn tx_receipt_has_the_same_hash_for_promise() {
522        let pk = PrivateKey::random();
523        let promise = Promise::mock(());
524        let compact_promise = promise.to_compact();
525
526        let receipt_promise = Receipt::Promise(promise);
527        let receipt_compact_promise = Receipt::Promise(compact_promise);
528        assert_eq!(
529            receipt_promise.to_digest(),
530            receipt_compact_promise.to_digest()
531        );
532
533        let signed_receipt = SignedMessage::create(pk.clone(), receipt_promise).unwrap();
534        let signed_compact_receipt = SignedMessage::create(pk, receipt_compact_promise).unwrap();
535
536        assert_eq!(
537            *signed_receipt.signature(),
538            *signed_compact_receipt.signature()
539        );
540        assert_eq!(signed_receipt.address(), signed_compact_receipt.address());
541    }
542
543    #[test]
544    fn tx_receipt_has_the_same_hash_for_error() {
545        let purged = PurgedTransaction {
546            tx_hash: unsafe { HashOf::new(H256::random()) },
547            reason: TransactionPurgedReason::Outdated,
548        };
549        let receipt1 = Receipt::<Promise>::Purged(purged.clone());
550        let receipt2 = Receipt::<CompactPromise>::Purged(purged);
551
552        assert_eq!(receipt1.to_digest(), receipt2.to_digest());
553    }
554}