1use crate::{
18 ClientError, H160, LOG_TARGET, Log, ReceiptGasInfoV1, ReceiptInfo,
19 client::{
20 SubstrateBlock, SubstrateBlockNumber,
21 version_aware_runtime_api::VersionAwareRuntimeApiProvider,
22 },
23 subxt_client::{
24 SrcChainConfig,
25 revive::{
26 calls::EthTransact,
27 events::{ContractEmitted, EthExtrinsicRevert},
28 },
29 },
30};
31
32use pallet_revive::{
33 create1,
34 evm::{GenericTransaction, H256, TransactionSigned, U256},
35};
36use sp_crypto_hashing::keccak_256;
37use std::{
38 collections::{BTreeMap, HashMap, HashSet},
39 future::Future,
40 pin::Pin,
41 sync::{
42 Arc,
43 atomic::{AtomicU64, Ordering},
44 },
45};
46use subxt::{
47 client::OfflineClientAtBlockT,
48 events::{DecodeAsEvent, Phase},
49 extrinsics::Extrinsics,
50};
51
52type EventDetails<'a> = subxt::events::Event<'a, SrcChainConfig>;
53
54enum ReviveEvent {
56 Revert,
57 Log(Log),
58}
59
60fn decode_revive_event(
63 event: &EventDetails<'_>,
64 block_number: U256,
65 transaction_hash: H256,
66 transaction_index: usize,
67 block_hash: H256,
68) -> Option<ReviveEvent> {
69 let pallet_name = event.pallet_name();
70 let event_name = event.event_name();
71
72 if EthExtrinsicRevert::is_event(pallet_name, event_name) {
73 return Some(ReviveEvent::Revert);
74 }
75 if ContractEmitted::is_event(pallet_name, event_name) {
76 match event.decode_fields_as::<ContractEmitted>() {
77 Some(Ok(evt)) => {
78 return Some(ReviveEvent::Log(Log {
79 address: evt.contract,
80 topics: evt.topics,
81 data: Some(evt.data.into()),
82 block_number,
83 transaction_hash,
84 transaction_index: transaction_index.into(),
85 block_hash,
86 log_index: event.index().into(),
87 ..Default::default()
88 }));
89 },
90 Some(Err(err)) => log::warn!(
91 target: LOG_TARGET,
92 "Failed to decode ContractEmitted event {} in block {block_number} (tx {transaction_hash:?}): {err:?}, log dropped from receipt",
93 event.index()
94 ),
95 None => {},
97 }
98 }
99 None
100}
101
102fn extract_revive_events(
112 block_events: &subxt::events::Events<SrcChainConfig>,
113 substrate_block_number: SubstrateBlockNumber,
114 eth_block_number: U256,
115 eth_block_hash: H256,
116 eth_tx_hash_for: impl Fn(usize) -> Option<H256>,
117) -> (HashSet<usize>, HashMap<usize, Vec<Log>>) {
118 let mut reverted_extrinsics: HashSet<usize> = HashSet::new();
119 let mut logs_by_extrinsic: HashMap<usize, Vec<Log>> = HashMap::new();
120
121 for (event_index, event_result) in block_events.iter().enumerate() {
122 let event = match event_result {
123 Ok(e) => e,
124 Err(err) => {
125 log::debug!(
126 target: LOG_TARGET,
127 "Failed to decode event {event_index} in block #{substrate_block_number}: {err:?}"
128 );
129 continue;
130 },
131 };
132
133 let extrinsic_index = match event.phase() {
134 Phase::ApplyExtrinsic(idx) => idx as usize,
135 _ => continue,
136 };
137
138 let Some(eth_tx_hash) = eth_tx_hash_for(extrinsic_index) else { continue };
139
140 match decode_revive_event(
141 &event,
142 eth_block_number,
143 eth_tx_hash,
144 extrinsic_index,
145 eth_block_hash,
146 ) {
147 Some(ReviveEvent::Revert) => {
148 reverted_extrinsics.insert(extrinsic_index);
149 },
150 Some(ReviveEvent::Log(log)) => {
151 logs_by_extrinsic.entry(extrinsic_index).or_default().push(log);
152 },
153 None => {},
154 }
155 }
156
157 (reverted_extrinsics, logs_by_extrinsic)
158}
159
160fn extract_eth_transacts<C: OfflineClientAtBlockT<SrcChainConfig>>(
162 block_extrinsics: &Extrinsics<'_, SrcChainConfig, C>,
163 block_number: SubstrateBlockNumber,
164) -> Result<(Vec<(EthTransact, usize)>, bool), ClientError> {
165 let mut extrinsics = Vec::new();
166 let mut undecoded_extrinsic = false;
167 for (ext_idx, ext) in block_extrinsics.iter().enumerate() {
168 let ext = match ext {
169 Ok(ext) => ext,
170 Err(err) => {
172 log::debug!(target: LOG_TARGET,
173 "Failed to decode extrinsic {ext_idx} of block #{block_number}: {err:?}");
174 undecoded_extrinsic = true;
175 continue;
176 },
177 };
178 match ext.decode_call_data_fields_as::<EthTransact>() {
179 Some(Ok(call)) => extrinsics.push((call, ext_idx)),
180 Some(Err(err)) => {
181 log::error!(target: LOG_TARGET,
182 "Failed to decode the EthTransact call in extrinsic {ext_idx} of block \
183 #{block_number}: {err:?}");
184 return Err(subxt::Error::from(err).into());
185 },
186 None => {},
188 }
189 }
190
191 Ok((extrinsics, undecoded_extrinsic))
192}
193
194type FetchReceiptDataFn = Arc<
195 dyn Fn(SubstrateBlock) -> Pin<Box<dyn Future<Output = Option<Vec<ReceiptGasInfoV1>>> + Send>>
196 + Send
197 + Sync,
198>;
199
200type FetchEthBlockHashFn = Arc<
201 dyn Fn(H256, SubstrateBlockNumber) -> Pin<Box<dyn Future<Output = Option<H256>> + Send>>
202 + Send
203 + Sync,
204>;
205
206type RecoverEthAddressFn = Arc<dyn Fn(&TransactionSigned) -> Result<H160, ()> + Send + Sync>;
207
208#[derive(Clone)]
210pub struct ReceiptExtractor {
211 fetch_receipt_data: FetchReceiptDataFn,
213
214 fetch_eth_block_hash: FetchEthBlockHashFn,
216
217 first_evm_block: Arc<AtomicU64>,
221
222 recover_eth_address: RecoverEthAddressFn,
224}
225
226impl ReceiptExtractor {
227 pub async fn new(
229 runtime_api_provider: VersionAwareRuntimeApiProvider,
230 ) -> Result<Self, ClientError> {
231 Self::new_with_custom_address_recovery(
232 runtime_api_provider,
233 Arc::new(|signed_tx: &TransactionSigned| signed_tx.recover_eth_address()),
234 )
235 .await
236 }
237
238 pub async fn new_with_custom_address_recovery(
243 runtime_api_provider: VersionAwareRuntimeApiProvider,
244 recover_eth_address_fn: RecoverEthAddressFn,
245 ) -> Result<Self, ClientError> {
246 let provider = runtime_api_provider.clone();
247 let fetch_eth_block_hash = Arc::new(move |substrate_block_hash, substrate_block_number| {
248 let provider = provider.clone();
249
250 let fut = async move {
251 let runtime_api = provider
252 .at_block_hash_and_number(substrate_block_hash, substrate_block_number)
253 .await
254 .inspect_err(|err| {
255 log::debug!(
256 target: LOG_TARGET,
257 "Failed to access the runtime API at block #{substrate_block_number} \
258 ({substrate_block_hash:?}) for an eth_block_hash query: {err:?}"
259 );
260 })
261 .ok()?;
262 runtime_api
263 .eth_block_hash(U256::from(substrate_block_number))?
264 .await
265 .inspect_err(|err| {
266 log::debug!(
267 target: LOG_TARGET,
268 "Failed to query eth_block_hash at block #{substrate_block_number} \
269 ({substrate_block_hash:?}): {err:?}"
270 );
271 })
272 .ok()
273 .flatten()
274 };
275
276 Box::pin(fut) as Pin<Box<_>>
277 });
278
279 let provider = runtime_api_provider;
280 let fetch_receipt_data = Arc::new(move |at_block: SubstrateBlock| {
281 let provider = provider.clone();
282
283 let fut = async move {
284 let block_hash = at_block.block_hash();
285 let runtime_api = provider
286 .at_resolved_block(at_block)
287 .await
288 .inspect_err(|err| {
289 log::debug!(
290 target: LOG_TARGET,
291 "Failed to access the runtime API at block {block_hash:?} for an \
292 eth_receipt_data query: {err:?}"
293 );
294 })
295 .ok()?;
296 runtime_api
297 .eth_receipt_data()?
298 .await
299 .inspect_err(|err| {
300 log::debug!(
301 target: LOG_TARGET,
302 "Failed to query eth_receipt_data at block {block_hash:?}: {err:?}"
303 );
304 })
305 .ok()
306 };
307
308 Box::pin(fut) as Pin<Box<_>>
309 });
310
311 Ok(Self {
312 fetch_receipt_data,
313 fetch_eth_block_hash,
314 first_evm_block: Arc::new(AtomicU64::new(u64::MAX)),
315 recover_eth_address: recover_eth_address_fn,
316 })
317 }
318
319 #[cfg(test)]
320 pub fn new_mock() -> Self {
321 let fetch_receipt_data = Arc::new(|_| Box::pin(std::future::ready(None)) as Pin<Box<_>>);
322 let fetch_eth_block_hash =
324 Arc::new(|block_hash: H256, block_number: SubstrateBlockNumber| {
325 let bytes: Vec<u8> = [block_hash.as_bytes(), &block_number.to_be_bytes()].concat();
327 let eth_block_hash = H256::from(keccak_256(&bytes));
328 Box::pin(std::future::ready(Some(eth_block_hash))) as Pin<Box<_>>
329 });
330
331 Self {
332 fetch_receipt_data,
333 fetch_eth_block_hash,
334 first_evm_block: Arc::new(AtomicU64::new(u64::MAX)),
335 recover_eth_address: Arc::new(|signed_tx: &TransactionSigned| {
336 signed_tx.recover_eth_address()
337 }),
338 }
339 }
340
341 pub fn is_before_first_evm_block(&self, block_number: SubstrateBlockNumber) -> bool {
344 let val = self.first_evm_block.load(Ordering::Acquire);
345 val != u64::MAX && block_number < val
346 }
347
348 pub fn set_first_evm_block(&self, block_number: SubstrateBlockNumber) {
350 let prev = self.first_evm_block.fetch_min(block_number, Ordering::AcqRel);
351 if block_number > prev {
352 log::debug!(target: LOG_TARGET,
353 "Ignored attempt to raise first_evm_block to #{block_number}, current is #{prev}");
354 }
355 }
356
357 pub fn first_evm_block(&self) -> Option<SubstrateBlockNumber> {
359 let val = self.first_evm_block.load(Ordering::Acquire);
360 (val != u64::MAX).then_some(val)
361 }
362
363 async fn resolve_eth_block_hash(
365 &self,
366 substrate_block_hash: H256,
367 substrate_block_number: SubstrateBlockNumber,
368 ) -> H256 {
369 match (self.fetch_eth_block_hash)(substrate_block_hash, substrate_block_number).await {
370 Some(hash) => hash,
371 None => {
372 log::trace!(target: LOG_TARGET,
373 "eth_block_hash returned None for substrate block \
374 #{substrate_block_number} ({substrate_block_hash:?}), \
375 falling back to substrate hash as ETH hash");
376 substrate_block_hash
377 },
378 }
379 }
380
381 fn decode_transaction_and_build_receipt(
383 &self,
384 eth_block_hash: H256,
385 block_number: U256,
386 call: EthTransact,
387 transaction_hash: H256,
388 transaction_index: usize,
389 receipt_gas_info: ReceiptGasInfoV1,
390 reverted: bool,
391 logs: Vec<Log>,
392 ) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
393 let signed_tx =
394 TransactionSigned::decode(&call.payload).map_err(|_| ClientError::TxDecodingFailed)?;
395 let from = (self.recover_eth_address)(&signed_tx).map_err(|_| {
396 log::error!(target: LOG_TARGET, "Failed to recover eth address from signed tx");
397 ClientError::RecoverEthAddressFailed
398 })?;
399
400 let tx_info = GenericTransaction::from_signed(
401 signed_tx.clone(),
402 receipt_gas_info.effective_gas_price,
403 Some(from),
404 );
405
406 let contract_address = if tx_info.to.is_none() {
407 Some(create1(
408 &from,
409 tx_info
410 .nonce
411 .unwrap_or_default()
412 .try_into()
413 .map_err(|_| ClientError::ConversionFailed)?,
414 ))
415 } else {
416 None
417 };
418
419 let receipt = ReceiptInfo::new(
420 eth_block_hash,
421 block_number,
422 contract_address,
423 from,
424 logs,
425 tx_info.to,
426 receipt_gas_info.effective_gas_price,
427 U256::from(receipt_gas_info.gas_used),
428 !reverted,
429 transaction_hash,
430 transaction_index.into(),
431 tx_info.r#type.unwrap_or_default(),
432 );
433 Ok((signed_tx, receipt))
434 }
435
436 pub async fn extract_from_block(
438 &self,
439 block: &SubstrateBlock,
440 ) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
441 let eth_block_hash =
442 self.resolve_eth_block_hash(block.block_hash(), block.block_number()).await;
443
444 self.extract_from_block_with_eth_hash(block, eth_block_hash).await
445 }
446
447 pub async fn extract_from_block_with_eth_hash(
451 &self,
452 block: &SubstrateBlock,
453 eth_block_hash: H256,
454 ) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
455 if self.is_before_first_evm_block(block.block_number()) {
456 return Ok(vec![]);
457 }
458
459 let eth_tx_by_index: BTreeMap<usize, (EthTransact, H256, ReceiptGasInfoV1)> = self
460 .get_block_extrinsics(block)
461 .await?
462 .map(|(call, receipt_gas_info, extrinsic_index)| {
463 let hash = H256(keccak_256(&call.payload));
464 (extrinsic_index, (call, hash, receipt_gas_info))
465 })
466 .collect();
467
468 if eth_tx_by_index.is_empty() {
469 return Ok(vec![]);
470 }
471
472 let substrate_block_number = block.block_number();
473 let eth_block_number: U256 = substrate_block_number.into();
474 let block_events = block.events().fetch().await.inspect_err(|err| {
475 log::debug!(target: LOG_TARGET, "Error fetching events for block #{substrate_block_number}: {err:?}");
476 })?;
477 let (reverted_extrinsics, mut logs_by_extrinsic) = extract_revive_events(
478 &block_events,
479 substrate_block_number,
480 eth_block_number,
481 eth_block_hash,
482 |idx| eth_tx_by_index.get(&idx).map(|(_, hash, _)| *hash),
483 );
484
485 eth_tx_by_index
486 .into_iter()
487 .map(|(transaction_index, (call, transaction_hash, receipt_gas_info))| {
488 let reverted = reverted_extrinsics.contains(&transaction_index);
489 let logs = logs_by_extrinsic.remove(&transaction_index).unwrap_or_default();
490 self.decode_transaction_and_build_receipt(
491 eth_block_hash,
492 eth_block_number,
493 call,
494 transaction_hash,
495 transaction_index,
496 receipt_gas_info,
497 reverted,
498 logs,
499 )
500 .inspect_err(|err| {
501 log::warn!(target: LOG_TARGET, "Error extracting extrinsic: {err:?}");
502 })
503 })
504 .collect()
505 }
506
507 async fn get_block_extrinsics(
510 &self,
511 block: &SubstrateBlock,
512 ) -> Result<impl Iterator<Item = (EthTransact, ReceiptGasInfoV1, usize)>, ClientError> {
513 let block_extrinsics = block.extrinsics().fetch().await.inspect_err(|err| {
514 log::debug!(target: LOG_TARGET, "Error fetching for #{:?} extrinsics: {err:?}", block.block_number());
515 })?;
516
517 let block_number = block.block_number();
518 let (extrinsics, undecoded_extrinsic) =
519 extract_eth_transacts(&block_extrinsics, block_number)?;
520
521 let receipt_data = if extrinsics.is_empty() && !undecoded_extrinsic {
523 Vec::new()
524 } else {
525 (self.fetch_receipt_data)(block.clone()).await.ok_or_else(|| {
526 log::trace!(target: LOG_TARGET,
527 "Receipt data not found for block #{} ({:?})",
528 block.block_number(), block.block_hash());
529 ClientError::ReceiptDataNotFound
530 })?
531 };
532
533 if receipt_data.len() != extrinsics.len() {
535 log::error!(
536 target: LOG_TARGET,
537 "Receipt data length ({}) does not match extrinsics length ({})",
538 receipt_data.len(),
539 extrinsics.len()
540 );
541 Err(ClientError::ReceiptDataLengthMismatch)
542 } else {
543 Ok(extrinsics
544 .into_iter()
545 .zip(receipt_data)
546 .map(|((call, ext_idx), rec)| (call, rec, ext_idx)))
547 }
548 }
549
550 pub async fn extract_from_transaction(
553 &self,
554 block: &SubstrateBlock,
555 transaction_index: usize,
556 ) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
557 let (eth_call, receipt_gas_info, transaction_hash) = self
558 .get_block_extrinsics(block)
559 .await?
560 .find_map(|(call, receipt_gas_info, extrinsic_index)| {
561 (extrinsic_index == transaction_index).then(|| {
562 let hash = H256(keccak_256(&call.payload));
563 (call, receipt_gas_info, hash)
564 })
565 })
566 .ok_or_else(|| {
567 log::trace!(target: LOG_TARGET,
568 "extract_from_transaction: no EVM extrinsic at tx_index {transaction_index} \
569 in block #{} ({:?})", block.block_number(), block.block_hash());
570 ClientError::EthExtrinsicNotFound
571 })?;
572
573 let substrate_block_number = block.block_number();
574 let eth_block_number: U256 = substrate_block_number.into();
575 let eth_block_hash =
576 self.resolve_eth_block_hash(block.block_hash(), substrate_block_number).await;
577 let block_events = block.events().fetch().await.inspect_err(|err| {
578 log::debug!(target: LOG_TARGET, "Error fetching events for block #{substrate_block_number}: {err:?}");
579 })?;
580 let (reverted_extrinsics, mut logs_by_extrinsic) = extract_revive_events(
581 &block_events,
582 substrate_block_number,
583 eth_block_number,
584 eth_block_hash,
585 |idx| (idx == transaction_index).then_some(transaction_hash),
586 );
587
588 let reverted = reverted_extrinsics.contains(&transaction_index);
589 let logs = logs_by_extrinsic.remove(&transaction_index).unwrap_or_default();
590 self.decode_transaction_and_build_receipt(
591 eth_block_hash,
592 eth_block_number,
593 eth_call,
594 transaction_hash,
595 transaction_index,
596 receipt_gas_info,
597 reverted,
598 logs,
599 )
600 }
601
602 pub async fn get_ethereum_block_hash(
604 &self,
605 block_hash: &H256,
606 block_number: SubstrateBlockNumber,
607 ) -> Option<H256> {
608 (self.fetch_eth_block_hash)(*block_hash, block_number).await
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615
616 use pallet_revive::evm::{Account, TransactionLegacyUnsigned, TransactionUnsigned};
617
618 fn signed_call(account: &Account, tx: TransactionUnsigned) -> (EthTransact, H256) {
619 let payload = account.sign_transaction(tx).signed_payload();
620 let hash = H256(keccak_256(&payload));
621 (EthTransact { payload }, hash)
622 }
623
624 fn legacy_call_tx(to: H160) -> TransactionUnsigned {
625 TransactionUnsigned::from(TransactionLegacyUnsigned {
626 chain_id: Some(U256::from(1)),
627 to: Some(to),
628 gas: U256::from(21_000),
629 ..Default::default()
630 })
631 }
632
633 fn gas_info() -> ReceiptGasInfoV1 {
634 ReceiptGasInfoV1 {
635 gas_used: U256::from(21_000),
636 effective_gas_price: U256::from(1_000_000_000),
637 }
638 }
639
640 #[test]
641 fn build_receipt_for_call() {
642 let extractor = ReceiptExtractor::new_mock();
643 let account = Account::default();
644 let eth_block_hash = H256::from([0xAB; 32]);
645 let block_number = U256::from(42);
646 let (call, tx_hash) = signed_call(&account, legacy_call_tx(account.address()));
647
648 let (signed_tx, receipt) = extractor
650 .decode_transaction_and_build_receipt(
651 eth_block_hash,
652 block_number,
653 call,
654 tx_hash,
655 3,
656 gas_info(),
657 false,
658 vec![],
659 )
660 .unwrap();
661
662 assert!(receipt.is_success());
663 assert_eq!(receipt.from, account.address());
664 assert_eq!(receipt.to, Some(account.address()));
665 assert_eq!(receipt.contract_address, None);
666 assert_eq!(receipt.block_hash, eth_block_hash);
667 assert_eq!(receipt.block_number, block_number);
668 assert_eq!(receipt.transaction_hash, tx_hash);
669 assert_eq!(receipt.transaction_index, U256::from(3));
670 assert_eq!(receipt.gas_used, U256::from(21_000));
671 assert_eq!(signed_tx.recover_eth_address().unwrap(), account.address());
672
673 let (call, tx_hash) = signed_call(&account, legacy_call_tx(account.address()));
675 let (_, receipt) = extractor
676 .decode_transaction_and_build_receipt(
677 eth_block_hash,
678 block_number,
679 call,
680 tx_hash,
681 3,
682 gas_info(),
683 true,
684 vec![],
685 )
686 .unwrap();
687
688 assert!(!receipt.is_success());
689 assert_eq!(receipt.from, account.address());
690 }
691
692 #[test]
693 fn build_receipt_for_deploy() {
694 let extractor = ReceiptExtractor::new_mock();
695 let account = Account::default();
696 let deploy_tx = TransactionUnsigned::from(TransactionLegacyUnsigned {
697 chain_id: Some(U256::from(1)),
698 gas: U256::from(100_000),
699 nonce: U256::from(0),
700 ..Default::default()
701 });
702 let (call, tx_hash) = signed_call(&account, deploy_tx);
703
704 let (_, receipt) = extractor
705 .decode_transaction_and_build_receipt(
706 H256::zero(),
707 U256::from(1),
708 call,
709 tx_hash,
710 0,
711 gas_info(),
712 false,
713 vec![],
714 )
715 .unwrap();
716
717 assert!(receipt.is_success());
718 assert_eq!(receipt.to, None);
719 assert_eq!(receipt.contract_address, Some(create1(&account.address(), 0)));
720 assert_eq!(receipt.from, account.address());
721 }
722
723 #[test]
724 fn build_receipt_rejects_invalid_payload() {
725 let extractor = ReceiptExtractor::new_mock();
726
727 let call = EthTransact { payload: vec![0xde, 0xad] };
729 let hash = H256(keccak_256(&call.payload));
730 let err = extractor
731 .decode_transaction_and_build_receipt(
732 H256::zero(),
733 U256::from(1),
734 call,
735 hash,
736 0,
737 gas_info(),
738 false,
739 vec![],
740 )
741 .unwrap_err();
742 assert!(matches!(err, ClientError::TxDecodingFailed));
743
744 let extractor = ReceiptExtractor {
746 recover_eth_address: Arc::new(|_| Err(())),
747 ..ReceiptExtractor::new_mock()
748 };
749 let account = Account::default();
750 let (call, hash) = signed_call(&account, legacy_call_tx(account.address()));
751 let err = extractor
752 .decode_transaction_and_build_receipt(
753 H256::zero(),
754 U256::from(1),
755 call,
756 hash,
757 0,
758 gas_info(),
759 false,
760 vec![],
761 )
762 .unwrap_err();
763 assert!(matches!(err, ClientError::RecoverEthAddressFailed));
764 }
765
766 #[test]
767 fn defaults_and_first_evm_block_only_decreases() {
768 let extractor = ReceiptExtractor::new_mock();
769
770 assert!(extractor.first_evm_block().is_none());
771
772 extractor.set_first_evm_block(100);
774 assert_eq!(extractor.first_evm_block(), Some(100));
775
776 extractor.set_first_evm_block(50);
777 assert_eq!(extractor.first_evm_block(), Some(50));
778
779 extractor.set_first_evm_block(100);
781 assert_eq!(extractor.first_evm_block(), Some(50));
782 }
783
784 use crate::block_info_provider::test::chain_config;
785 use codec::{Compact, Encode};
786 use frame_system::EventRecord;
787 use revive_dev_runtime::{Runtime, RuntimeEvent};
788 use subxt::{PolkadotConfig, client::OfflineClient, events::Events};
789
790 fn offline_client() -> OfflineClient<PolkadotConfig> {
792 OfflineClient::<PolkadotConfig>::new_with_config(chain_config())
793 }
794
795 struct EventsBuilder {
797 bytes: Vec<u8>,
798 count: u32,
799 }
800
801 impl EventsBuilder {
802 fn new() -> Self {
803 Self { bytes: Vec::new(), count: 0 }
804 }
805
806 fn push_event(
807 mut self,
808 phase: frame_system::Phase,
809 event: pallet_revive::Event<Runtime>,
810 ) -> Self {
811 EventRecord::<RuntimeEvent, H256> {
812 phase,
813 event: RuntimeEvent::Revive(event),
814 topics: vec![],
815 }
816 .encode_to(&mut self.bytes);
817 self.count += 1;
818 self
819 }
820
821 fn build(self) -> Events<SrcChainConfig> {
822 let mut encoded_events = Vec::new();
823 Compact(self.count).encode_to(&mut encoded_events);
824 encoded_events.extend(self.bytes);
825
826 let client = offline_client();
827 let at_block =
828 client.at_block(0u64).expect("spec version range covers all block numbers; qed");
829 at_block.events().from_bytes(encoded_events)
830 }
831 }
832
833 #[test]
834 fn extract_revive_events_decodes_contract_emitted_log() {
835 let contract = H160::from([0x11; 20]);
836 let topics = vec![H256::from([0x22; 32]), H256::from([0x33; 32])];
837 let data = vec![0xde, 0xad, 0xbe, 0xef];
838 let events = EventsBuilder::new()
839 .push_event(
840 frame_system::Phase::ApplyExtrinsic(5),
841 pallet_revive::Event::ContractEmitted {
842 contract,
843 data: data.clone(),
844 topics: topics.clone(),
845 },
846 )
847 .build();
848
849 let tx_hash = H256::from([0xAA; 32]);
850 let eth_block_hash = H256::from([0xBB; 32]);
851 let substrate_block_number = 42u64;
852 let eth_block_number = U256::from(substrate_block_number);
853
854 let (reverts, logs) = extract_revive_events(
855 &events,
856 substrate_block_number,
857 eth_block_number,
858 eth_block_hash,
859 |idx| (idx == 5).then_some(tx_hash),
860 );
861
862 assert!(reverts.is_empty());
863 assert_eq!(logs.len(), 1);
864 let log = &logs[&5][0];
865 assert_eq!(log.address, contract);
866 assert_eq!(log.topics, topics);
867 assert_eq!(log.data.as_ref().unwrap().0, data);
868 assert_eq!(log.block_hash, eth_block_hash);
869 assert_eq!(log.block_number, eth_block_number);
870 assert_eq!(log.transaction_hash, tx_hash);
871 assert_eq!(log.transaction_index, U256::from(5));
872 }
873
874 #[test]
875 fn extract_revive_events_skips_irrelevant_events() {
876 let empty_contract_emitted = pallet_revive::Event::ContractEmitted {
879 contract: H160::zero(),
880 data: vec![],
881 topics: vec![],
882 };
883 let revert = pallet_revive::Event::EthExtrinsicRevert {
884 dispatch_error: sp_runtime::DispatchError::Other("skipped-phase revert"),
885 };
886 let events = EventsBuilder::new()
887 .push_event(frame_system::Phase::Finalization, empty_contract_emitted.clone())
888 .push_event(frame_system::Phase::Initialization, revert.clone())
889 .push_event(frame_system::Phase::ApplyExtrinsic(5), empty_contract_emitted)
890 .push_event(frame_system::Phase::ApplyExtrinsic(5), revert)
891 .build();
892
893 let (reverts, logs) =
895 extract_revive_events(&events, 0, U256::zero(), H256::zero(), |idx| {
896 (idx == 7).then_some(H256::zero())
897 });
898
899 assert!(reverts.is_empty());
900 assert!(logs.is_empty());
901 }
902
903 #[test]
904 fn extract_revive_events_accumulates_per_extrinsic() {
905 let tx0 = H256::from([0x01; 32]);
906 let tx1 = H256::from([0x02; 32]);
907 let tx2 = H256::from([0x03; 32]);
908 let emitted_by = |contract: H160| pallet_revive::Event::ContractEmitted {
909 contract,
910 data: vec![],
911 topics: vec![],
912 };
913 let events = EventsBuilder::new()
914 .push_event(frame_system::Phase::ApplyExtrinsic(0), emitted_by(H160::from([0xaa; 20])))
915 .push_event(frame_system::Phase::ApplyExtrinsic(0), emitted_by(H160::from([0xbb; 20])))
916 .push_event(
917 frame_system::Phase::ApplyExtrinsic(1),
918 pallet_revive::Event::EthExtrinsicRevert {
919 dispatch_error: sp_runtime::DispatchError::Other("tx-1 revert"),
920 },
921 )
922 .push_event(frame_system::Phase::ApplyExtrinsic(2), emitted_by(H160::from([0xcc; 20])))
923 .build();
924
925 let (reverts, logs) =
926 extract_revive_events(&events, 0, U256::zero(), H256::zero(), |idx| match idx {
927 0 => Some(tx0),
928 1 => Some(tx1),
929 2 => Some(tx2),
930 _ => None,
931 });
932
933 assert_eq!(reverts, [1usize].into_iter().collect::<HashSet<_>>());
934 assert_eq!(logs[&0].len(), 2);
935 assert_eq!(logs[&2].len(), 1);
936 assert_eq!(logs[&0][0].log_index, U256::from(0));
938 assert_eq!(logs[&0][1].log_index, U256::from(1));
939 assert_eq!(logs[&2][0].log_index, U256::from(3));
940 }
941
942 const ETH_TRANSACT_PAYLOAD: [u8; 4] = [0xde, 0xad, 0xbe, 0xef];
943
944 fn encode_bare(call: revive_dev_runtime::RuntimeCall) -> Vec<u8> {
946 let extrinsic: revive_dev_runtime::UncheckedExtrinsic =
947 pallet_revive::evm::runtime::UncheckedExtrinsic(
948 sp_runtime::generic::UncheckedExtrinsic::new_bare(call),
949 );
950 extrinsic.encode()
951 }
952
953 fn eth_transact_extrinsic() -> Vec<u8> {
954 encode_bare(revive_dev_runtime::RuntimeCall::Revive(pallet_revive::Call::eth_transact {
955 payload: ETH_TRANSACT_PAYLOAD.to_vec(),
956 }))
957 }
958
959 fn non_revive_extrinsic() -> Vec<u8> {
960 encode_bare(revive_dev_runtime::RuntimeCall::System(frame_system::Call::remark {
961 remark: vec![0x01],
962 }))
963 }
964
965 async fn extract_from(
967 blobs: Vec<Vec<u8>>,
968 ) -> Result<(Vec<(EthTransact, usize)>, bool), ClientError> {
969 const BLOCK_NUMBER: SubstrateBlockNumber = 42;
970
971 let client = offline_client();
972 let at_block = client
973 .at_block(BLOCK_NUMBER)
974 .expect("spec version range covers every block number; qed");
975 let extrinsics = at_block.extrinsics().from_bytes(blobs).await;
976 extract_eth_transacts(&extrinsics, BLOCK_NUMBER)
977 }
978
979 #[tokio::test]
980 async fn extract_eth_transacts_collects_revive_calls() {
981 let (calls, undecoded) =
982 extract_from(vec![non_revive_extrinsic(), eth_transact_extrinsic()])
983 .await
984 .unwrap();
985
986 assert!(!undecoded, "every extrinsic decoded");
987 assert_eq!(calls.len(), 1, "only the revive extrinsic is collected");
988 assert_eq!(calls[0].1, 1, "the extrinsic index is preserved");
989 assert_eq!(calls[0].0.payload, ETH_TRANSACT_PAYLOAD, "the call fields are decoded");
990 }
991
992 #[tokio::test]
993 async fn extract_eth_transacts_keeps_revive_calls_next_to_an_undecodable_one() {
994 let (calls, undecoded) =
995 extract_from(vec![vec![0xff; 4], eth_transact_extrinsic()]).await.unwrap();
996
997 assert_eq!(calls.len(), 1, "an undecodable extrinsic must not hide a decoded one");
998 assert_eq!(calls[0].1, 1, "the extrinsic index is preserved");
999 assert_eq!(calls[0].0.payload, ETH_TRANSACT_PAYLOAD, "the call fields are decoded");
1000 assert!(undecoded, "it may be a revive one, so report it");
1001 }
1002}