1use 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
14pub const VALIDITY_WINDOW: u8 = 32;
16
17pub const MAX_INJECTED_TX_PAYLOAD_SIZE: usize = 126 * 1024;
22
23pub const MAX_INJECTED_TX_SALT_SIZE: usize = 32;
25
26pub 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 pub recipient: Address,
59 pub tx: SignedInjectedTransaction,
60}
61
62#[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 pub destination: ActorId,
69 #[cfg_attr(feature = "std", serde(with = "serde_hex"))]
71 pub payload: LimitedVec<u8, MAX_INJECTED_TX_PAYLOAD_SIZE>,
72 pub value: u128,
75 pub reference_block: H256,
77 #[cfg_attr(feature = "std", serde(with = "serde_hex"))]
81 pub salt: LimitedVec<u8, MAX_INJECTED_TX_SALT_SIZE>,
82}
83
84const 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 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 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 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#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
145#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq, Hash)]
146pub struct Promise {
147 pub tx_hash: HashOf<InjectedTransaction>,
149 pub reply: ReplyInfo,
151}
152
153impl Promise {
154 pub fn reply_hash(&self) -> HashOf<ReplyInfo> {
156 unsafe { HashOf::new(self.reply.to_hash()) }
158 }
159
160 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#[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#[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 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#[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#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Deref, derive_more::From)]
272pub struct SignedCompactTxReceipt(SignedMessage<Receipt<CompactPromise>>);
273
274#[derive(Debug, PartialEq, Eq, derive_more::From)]
280pub enum UpgradedReceipt {
281 Pending(UnfilledPromiseReceipt),
282 Ready(SignedTxReceipt),
283}
284
285impl SignedCompactTxReceipt {
286 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 SignedMessage::from_parts_unchecked(Receipt::Purged(purged), signature, address)
298 .into()
299 }),
300 }
301 }
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, derive_more::Deref)]
307pub struct UnfilledPromiseReceipt(#[deref] CompactPromise, Signature, Address);
308
309pub 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#[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#[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 #[display("transaction reference block is outdated")]
354 Outdated = 1,
355 #[display("transaction reference block is unknown")]
357 UnknownReferenceBlock = 2,
358
359 #[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#[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 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 #[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_eq!(tx_concat_bytes(&initial_tx), tx_concat_bytes(&malicious_tx));
514
515 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}