referrerpolicy=no-referrer-when-downgrade

pallet_revive_eth_rpc/
receipt_extractor.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17use 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
54/// Outcome of decoding a single pallet-revive event.
55enum ReviveEvent {
56	Revert,
57	Log(Log),
58}
59
60/// Decode a single event detail into a [`ReviveEvent`], or `None` if it is not a pallet-revive
61/// event we care about.
62fn 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			// `is_event()` already confirmed the variant, so this is unreachable in practice.
96			None => {},
97		}
98	}
99	None
100}
101
102/// Iterate decoded block events and bucket revert flags and logs per extrinsic.
103/// Events for other extrinsics are skipped.
104///
105/// Events are stored sequentially without size markers, so a single
106/// undecodable event (e.g. from a runtime upgrade that shifted variant
107/// indices) corrupts the offset for all subsequent events.
108/// Decode errors are logged and skipped to avoid losing the entire receipt.
109///
110/// Returns `(reverted_extrinsics, logs_by_extrinsic)` keyed by extrinsic index.
111fn 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
160/// Returns the revive transactions from a block, and whether any extrinsic decode failed.
161fn 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			// Don't error here since the call type is unknown.
171			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			// Not a revive transaction.
187			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/// Utility to extract receipts from extrinsics.
209#[derive(Clone)]
210pub struct ReceiptExtractor {
211	/// Fetch the receipt data info.
212	fetch_receipt_data: FetchReceiptDataFn,
213
214	/// Fetch ethereum block hash.
215	fetch_eth_block_hash: FetchEthBlockHashFn,
216
217	/// Auto-discovered first EVM block on the chain.
218	/// Set once during backward sync when the first non-EVM block is encountered.
219	/// Uses `u64::MAX` as sentinel for "not yet discovered".
220	first_evm_block: Arc<AtomicU64>,
221
222	/// Recover the ethereum address from a transaction signature.
223	recover_eth_address: RecoverEthAddressFn,
224}
225
226impl ReceiptExtractor {
227	/// Create a new `ReceiptExtractor`.
228	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	/// Create a new `ReceiptExtractor` with custom Ethereum address recovery logic.
239	///
240	/// Use `ReceiptExtractor::new` if the default Ethereum address recovery
241	/// logic ([`TransactionSigned::recover_eth_address`] based) is enough.
242	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		// This method is useful when testing eth - substrate mapping.
323		let fetch_eth_block_hash =
324			Arc::new(|block_hash: H256, block_number: SubstrateBlockNumber| {
325				// Generate hash from substrate block hash and number
326				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	/// Check if the block is before the `first_evm_block` floor.
342	/// When sentinel (`u64::MAX`), no blocks are rejected (permissive default).
343	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	/// Set the first EVM block. Only stores if lower than the current value.
349	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	/// The auto-discovered first EVM block, or `None` if not yet discovered.
358	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	/// Resolve the Ethereum block hash for a substrate block, falling back to the substrate hash.
364	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	/// Decode the raw call payload into a [`TransactionSigned`] and construct its [`ReceiptInfo`].
382	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	/// Extract receipts from block.
437	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	/// Extract receipts from block, using a pre-fetched ethereum block hash.
448	///
449	/// Fetches block events once in a single pass before building receipts.
450	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	/// Return the ETH extrinsics of the block grouped with reconstruction receipt info and
508	/// extrinsic index
509	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		// Skip the runtime query for blocks with no revive extrinsics.
522		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		// Sanity check we received enough data from the pallet revive.
534		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	/// Extract a [`TransactionSigned`] and a [`ReceiptInfo`] for a specific transaction in a
551	/// [`SubstrateBlock`]
552	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	/// Get the Ethereum block hash for the Substrate block with specific hash.
603	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		// Successful call
649		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		// Same call, but reverted
674		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		// Corrupt payload
728		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		// Valid payload but address recovery fails
745		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		// first_evm_block only decreases
773		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		// Higher value is ignored
780		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	/// An offline client carrying the generated runtime metadata for every block.
791	fn offline_client() -> OfflineClient<PolkadotConfig> {
792		OfflineClient::<PolkadotConfig>::new_with_config(chain_config())
793	}
794
795	/// Build `Events` by SCALE-encoding revive events against the generated runtime metadata.
796	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		// Events outside `ApplyExtrinsic` and events for extrinsics the tx-hash closure
877		// doesn't resolve are both dropped.
878		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		// The tx-hash closure returns `Some` only for extrinsic 7 (not present)
894		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		// log_index is block-wide
937		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	/// SCALE-encode a bare extrinsic the way a block body carries it, length prefix included.
945	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	/// Run the extraction over a synthetic block body.
966	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}