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::{SubstrateBlock, SubstrateBlockNumber, runtime_api::RuntimeApi},
20	subxt_client::{
21		SrcChainConfig,
22		revive::{
23			calls::EthTransact,
24			events::{ContractEmitted, EthExtrinsicRevert},
25		},
26	},
27};
28
29use pallet_revive::{
30	create1,
31	evm::{GenericTransaction, H256, TransactionSigned, U256},
32};
33use sp_crypto_hashing::keccak_256;
34use std::{
35	collections::{BTreeMap, HashMap, HashSet},
36	future::Future,
37	pin::Pin,
38	sync::{
39		Arc,
40		atomic::{AtomicU64, Ordering},
41	},
42};
43use subxt::{
44	OnlineClient,
45	events::{DecodeAsEvent, Phase},
46};
47
48type EventDetails<'a> = subxt::events::Event<'a, SrcChainConfig>;
49
50/// Outcome of decoding a single pallet-revive event.
51enum ReviveEvent {
52	Revert,
53	Log(Log),
54}
55
56/// Decode a single event detail into a [`ReviveEvent`], or `None` if it is not a pallet-revive
57/// event we care about.
58fn decode_revive_event(
59	event: &EventDetails<'_>,
60	block_number: U256,
61	transaction_hash: H256,
62	transaction_index: usize,
63	block_hash: H256,
64) -> Option<ReviveEvent> {
65	let pallet_name = event.pallet_name();
66	let event_name = event.event_name();
67
68	if EthExtrinsicRevert::is_event(pallet_name, event_name) {
69		return Some(ReviveEvent::Revert);
70	}
71	if ContractEmitted::is_event(pallet_name, event_name) {
72		match event.decode_fields_as::<ContractEmitted>() {
73			Some(Ok(evt)) => {
74				return Some(ReviveEvent::Log(Log {
75					address: evt.contract,
76					topics: evt.topics,
77					data: Some(evt.data.into()),
78					block_number,
79					transaction_hash,
80					transaction_index: transaction_index.into(),
81					block_hash,
82					log_index: event.index().into(),
83					..Default::default()
84				}));
85			},
86			Some(Err(err)) => log::warn!(
87				target: LOG_TARGET,
88				"Failed to decode ContractEmitted event {} in block {block_number} (tx {transaction_hash:?}): {err:?}, log dropped from receipt",
89				event.index()
90			),
91			// `is_event()` already confirmed the variant, so this is unreachable in practice.
92			None => {},
93		}
94	}
95	None
96}
97
98/// Iterate decoded block events and bucket revert flags and logs per extrinsic.
99/// Events for other extrinsics are skipped.
100///
101/// Events are stored sequentially without size markers, so a single
102/// undecodable event (e.g. from a runtime upgrade that shifted variant
103/// indices) corrupts the offset for all subsequent events.
104/// Decode errors are logged and skipped to avoid losing the entire receipt.
105///
106/// Returns `(reverted_extrinsics, logs_by_extrinsic)` keyed by extrinsic index.
107fn extract_revive_events(
108	block_events: &subxt::events::Events<SrcChainConfig>,
109	substrate_block_number: SubstrateBlockNumber,
110	eth_block_number: U256,
111	eth_block_hash: H256,
112	eth_tx_hash_for: impl Fn(usize) -> Option<H256>,
113) -> (HashSet<usize>, HashMap<usize, Vec<Log>>) {
114	let mut reverted_extrinsics: HashSet<usize> = HashSet::new();
115	let mut logs_by_extrinsic: HashMap<usize, Vec<Log>> = HashMap::new();
116
117	for (event_index, event_result) in block_events.iter().enumerate() {
118		let event = match event_result {
119			Ok(e) => e,
120			Err(err) => {
121				log::debug!(
122					target: LOG_TARGET,
123					"Failed to decode event {event_index} in block #{substrate_block_number}: {err:?}"
124				);
125				continue;
126			},
127		};
128
129		let extrinsic_index = match event.phase() {
130			Phase::ApplyExtrinsic(idx) => idx as usize,
131			_ => continue,
132		};
133
134		let Some(eth_tx_hash) = eth_tx_hash_for(extrinsic_index) else { continue };
135
136		match decode_revive_event(
137			&event,
138			eth_block_number,
139			eth_tx_hash,
140			extrinsic_index,
141			eth_block_hash,
142		) {
143			Some(ReviveEvent::Revert) => {
144				reverted_extrinsics.insert(extrinsic_index);
145			},
146			Some(ReviveEvent::Log(log)) => {
147				logs_by_extrinsic.entry(extrinsic_index).or_default().push(log);
148			},
149			None => {},
150		}
151	}
152
153	(reverted_extrinsics, logs_by_extrinsic)
154}
155
156type FetchReceiptDataFn = Arc<
157	dyn Fn(H256) -> Pin<Box<dyn Future<Output = Option<Vec<ReceiptGasInfoV1>>> + Send>>
158		+ Send
159		+ Sync,
160>;
161
162type FetchEthBlockHashFn = Arc<
163	dyn Fn(H256, SubstrateBlockNumber) -> Pin<Box<dyn Future<Output = Option<H256>> + Send>>
164		+ Send
165		+ Sync,
166>;
167
168type RecoverEthAddressFn = Arc<dyn Fn(&TransactionSigned) -> Result<H160, ()> + Send + Sync>;
169
170/// Utility to extract receipts from extrinsics.
171#[derive(Clone)]
172pub struct ReceiptExtractor {
173	/// Fetch the receipt data info.
174	fetch_receipt_data: FetchReceiptDataFn,
175
176	/// Fetch ethereum block hash.
177	fetch_eth_block_hash: FetchEthBlockHashFn,
178
179	/// Auto-discovered first EVM block on the chain.
180	/// Set once during backward sync when the first non-EVM block is encountered.
181	/// Uses `u64::MAX` as sentinel for "not yet discovered".
182	first_evm_block: Arc<AtomicU64>,
183
184	/// Recover the ethereum address from a transaction signature.
185	recover_eth_address: RecoverEthAddressFn,
186}
187
188impl ReceiptExtractor {
189	/// Create a new `ReceiptExtractor`.
190	pub async fn new(api: OnlineClient<SrcChainConfig>) -> Result<Self, ClientError> {
191		Self::new_with_custom_address_recovery(
192			api,
193			Arc::new(|signed_tx: &TransactionSigned| signed_tx.recover_eth_address()),
194		)
195		.await
196	}
197
198	/// Create a new `ReceiptExtractor` with custom Ethereum address recovery logic.
199	///
200	/// Use `ReceiptExtractor::new` if the default Ethereum address recovery
201	/// logic ([`TransactionSigned::recover_eth_address`] based) is enough.
202	pub async fn new_with_custom_address_recovery(
203		api: OnlineClient<SrcChainConfig>,
204		recover_eth_address_fn: RecoverEthAddressFn,
205	) -> Result<Self, ClientError> {
206		let api_inner = api.clone();
207		let fetch_eth_block_hash = Arc::new(move |block_hash, block_number| {
208			let api_inner = api_inner.clone();
209
210			let fut = async move {
211				let at_block = api_inner
212					.at_block_hash_and_number(block_hash, block_number)
213					.await
214					.inspect_err(|err| {
215						log::debug!(target: LOG_TARGET, "Failed to resolve block #{block_number} ({block_hash:?}) for eth_block_hash query: {err:?}");
216					})
217					.ok()?;
218				let runtime_api = RuntimeApi::new(at_block);
219				runtime_api.eth_block_hash(U256::from(block_number)).await.ok().flatten()
220			};
221
222			Box::pin(fut) as Pin<Box<_>>
223		});
224
225		let api_inner = api.clone();
226		let fetch_receipt_data = Arc::new(move |block_hash| {
227			let api_inner = api_inner.clone();
228
229			let fut = async move {
230				let at_block = api_inner.at_block(block_hash).await.ok()?;
231				let runtime_api = RuntimeApi::new(at_block);
232				runtime_api.eth_receipt_data().await.ok()
233			};
234
235			Box::pin(fut) as Pin<Box<_>>
236		});
237
238		Ok(Self {
239			fetch_receipt_data,
240			fetch_eth_block_hash,
241			first_evm_block: Arc::new(AtomicU64::new(u64::MAX)),
242			recover_eth_address: recover_eth_address_fn,
243		})
244	}
245
246	#[cfg(test)]
247	pub fn new_mock() -> Self {
248		let fetch_receipt_data = Arc::new(|_| Box::pin(std::future::ready(None)) as Pin<Box<_>>);
249		// This method is useful when testing eth - substrate mapping.
250		let fetch_eth_block_hash =
251			Arc::new(|block_hash: H256, block_number: SubstrateBlockNumber| {
252				// Generate hash from substrate block hash and number
253				let bytes: Vec<u8> = [block_hash.as_bytes(), &block_number.to_be_bytes()].concat();
254				let eth_block_hash = H256::from(keccak_256(&bytes));
255				Box::pin(std::future::ready(Some(eth_block_hash))) as Pin<Box<_>>
256			});
257
258		Self {
259			fetch_receipt_data,
260			fetch_eth_block_hash,
261			first_evm_block: Arc::new(AtomicU64::new(u64::MAX)),
262			recover_eth_address: Arc::new(|signed_tx: &TransactionSigned| {
263				signed_tx.recover_eth_address()
264			}),
265		}
266	}
267
268	/// Check if the block is before the `first_evm_block` floor.
269	/// When sentinel (`u64::MAX`), no blocks are rejected (permissive default).
270	pub fn is_before_first_evm_block(&self, block_number: SubstrateBlockNumber) -> bool {
271		let val = self.first_evm_block.load(Ordering::Acquire);
272		val != u64::MAX && block_number < val
273	}
274
275	/// Set the first EVM block. Only stores if lower than the current value.
276	pub fn set_first_evm_block(&self, block_number: SubstrateBlockNumber) {
277		let prev = self.first_evm_block.fetch_min(block_number, Ordering::AcqRel);
278		if block_number > prev {
279			log::debug!(target: LOG_TARGET,
280				"Ignored attempt to raise first_evm_block to #{block_number}, current is #{prev}");
281		}
282	}
283
284	/// The auto-discovered first EVM block, or `None` if not yet discovered.
285	pub fn first_evm_block(&self) -> Option<SubstrateBlockNumber> {
286		let val = self.first_evm_block.load(Ordering::Acquire);
287		(val != u64::MAX).then_some(val)
288	}
289
290	/// Resolve the Ethereum block hash for a substrate block, falling back to the substrate hash.
291	async fn resolve_eth_block_hash(
292		&self,
293		substrate_block_hash: H256,
294		substrate_block_number: SubstrateBlockNumber,
295	) -> H256 {
296		match (self.fetch_eth_block_hash)(substrate_block_hash, substrate_block_number).await {
297			Some(hash) => hash,
298			None => {
299				log::trace!(target: LOG_TARGET,
300					"eth_block_hash returned None for substrate block \
301					 #{substrate_block_number} ({substrate_block_hash:?}), \
302					 falling back to substrate hash as ETH hash");
303				substrate_block_hash
304			},
305		}
306	}
307
308	/// Decode the raw call payload into a [`TransactionSigned`] and construct its [`ReceiptInfo`].
309	fn decode_transaction_and_build_receipt(
310		&self,
311		eth_block_hash: H256,
312		block_number: U256,
313		call: EthTransact,
314		transaction_hash: H256,
315		transaction_index: usize,
316		receipt_gas_info: ReceiptGasInfoV1,
317		reverted: bool,
318		logs: Vec<Log>,
319	) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
320		let signed_tx =
321			TransactionSigned::decode(&call.payload).map_err(|_| ClientError::TxDecodingFailed)?;
322		let from = (self.recover_eth_address)(&signed_tx).map_err(|_| {
323			log::error!(target: LOG_TARGET, "Failed to recover eth address from signed tx");
324			ClientError::RecoverEthAddressFailed
325		})?;
326
327		let tx_info = GenericTransaction::from_signed(
328			signed_tx.clone(),
329			receipt_gas_info.effective_gas_price,
330			Some(from),
331		);
332
333		let contract_address = if tx_info.to.is_none() {
334			Some(create1(
335				&from,
336				tx_info
337					.nonce
338					.unwrap_or_default()
339					.try_into()
340					.map_err(|_| ClientError::ConversionFailed)?,
341			))
342		} else {
343			None
344		};
345
346		let receipt = ReceiptInfo::new(
347			eth_block_hash,
348			block_number,
349			contract_address,
350			from,
351			logs,
352			tx_info.to,
353			receipt_gas_info.effective_gas_price,
354			U256::from(receipt_gas_info.gas_used),
355			!reverted,
356			transaction_hash,
357			transaction_index.into(),
358			tx_info.r#type.unwrap_or_default(),
359		);
360		Ok((signed_tx, receipt))
361	}
362
363	/// Extract receipts from block.
364	pub async fn extract_from_block(
365		&self,
366		block: &SubstrateBlock,
367	) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
368		let eth_block_hash =
369			self.resolve_eth_block_hash(block.block_hash(), block.block_number()).await;
370
371		self.extract_from_block_with_eth_hash(block, eth_block_hash).await
372	}
373
374	/// Extract receipts from block, using a pre-fetched ethereum block hash.
375	///
376	/// Fetches block events once in a single pass before building receipts.
377	pub async fn extract_from_block_with_eth_hash(
378		&self,
379		block: &SubstrateBlock,
380		eth_block_hash: H256,
381	) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
382		if self.is_before_first_evm_block(block.block_number()) {
383			return Ok(vec![]);
384		}
385
386		let eth_tx_by_index: BTreeMap<usize, (EthTransact, H256, ReceiptGasInfoV1)> = self
387			.get_block_extrinsics(block)
388			.await?
389			.map(|(call, receipt_gas_info, extrinsic_index)| {
390				let hash = H256(keccak_256(&call.payload));
391				(extrinsic_index, (call, hash, receipt_gas_info))
392			})
393			.collect();
394
395		if eth_tx_by_index.is_empty() {
396			return Ok(vec![]);
397		}
398
399		let substrate_block_number = block.block_number();
400		let eth_block_number: U256 = substrate_block_number.into();
401		let block_events = block.events().fetch().await.inspect_err(|err| {
402			log::debug!(target: LOG_TARGET, "Error fetching events for block #{substrate_block_number}: {err:?}");
403		})?;
404		let (reverted_extrinsics, mut logs_by_extrinsic) = extract_revive_events(
405			&block_events,
406			substrate_block_number,
407			eth_block_number,
408			eth_block_hash,
409			|idx| eth_tx_by_index.get(&idx).map(|(_, hash, _)| *hash),
410		);
411
412		eth_tx_by_index
413			.into_iter()
414			.map(|(transaction_index, (call, transaction_hash, receipt_gas_info))| {
415				let reverted = reverted_extrinsics.contains(&transaction_index);
416				let logs = logs_by_extrinsic.remove(&transaction_index).unwrap_or_default();
417				self.decode_transaction_and_build_receipt(
418					eth_block_hash,
419					eth_block_number,
420					call,
421					transaction_hash,
422					transaction_index,
423					receipt_gas_info,
424					reverted,
425					logs,
426				)
427				.inspect_err(|err| {
428					log::warn!(target: LOG_TARGET, "Error extracting extrinsic: {err:?}");
429				})
430			})
431			.collect()
432	}
433
434	/// Return the ETH extrinsics of the block grouped with reconstruction receipt info and
435	/// extrinsic index
436	async fn get_block_extrinsics(
437		&self,
438		block: &SubstrateBlock,
439	) -> Result<impl Iterator<Item = (EthTransact, ReceiptGasInfoV1, usize)>, ClientError> {
440		// Filter extrinsics from pallet_revive
441		let extrinsics = block.extrinsics().fetch().await.inspect_err(|err| {
442			log::debug!(target: LOG_TARGET, "Error fetching for #{:?} extrinsics: {err:?}", block.block_number());
443		})?;
444
445		let receipt_data =
446			(self.fetch_receipt_data)(block.block_hash()).await.ok_or_else(|| {
447				log::trace!(target: LOG_TARGET,
448				"Receipt data not found for block #{} ({:?})",
449				block.block_number(), block.block_hash());
450				ClientError::ReceiptDataNotFound
451			})?;
452		let block_number = block.block_number();
453		let extrinsics: Vec<_> = extrinsics
454			.iter()
455			.enumerate()
456			.flat_map(|(ext_idx, ext)| {
457				let ext = ext.ok()?;
458				match ext.decode_call_data_fields_as::<EthTransact>()? {
459					Ok(call) => Some((call, ext_idx)),
460					Err(err) => {
461						log::warn!(
462							target: LOG_TARGET,
463							"Failed to decode EthTransact call in extrinsic {ext_idx} of block #{block_number}: {err:?}, transaction dropped from receipts"
464						);
465						None
466					},
467				}
468			})
469			.collect();
470
471		// Sanity check we received enough data from the pallet revive.
472		if receipt_data.len() != extrinsics.len() {
473			log::error!(
474				target: LOG_TARGET,
475				"Receipt data length ({}) does not match extrinsics length ({})",
476				receipt_data.len(),
477				extrinsics.len()
478			);
479			Err(ClientError::ReceiptDataLengthMismatch)
480		} else {
481			Ok(extrinsics
482				.into_iter()
483				.zip(receipt_data)
484				.map(|((call, ext_idx), rec)| (call, rec, ext_idx)))
485		}
486	}
487
488	/// Extract a [`TransactionSigned`] and a [`ReceiptInfo`] for a specific transaction in a
489	/// [`SubstrateBlock`]
490	pub async fn extract_from_transaction(
491		&self,
492		block: &SubstrateBlock,
493		transaction_index: usize,
494	) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
495		let (eth_call, receipt_gas_info, transaction_hash) = self
496			.get_block_extrinsics(block)
497			.await?
498			.find_map(|(call, receipt_gas_info, extrinsic_index)| {
499				(extrinsic_index == transaction_index).then(|| {
500					let hash = H256(keccak_256(&call.payload));
501					(call, receipt_gas_info, hash)
502				})
503			})
504			.ok_or_else(|| {
505				log::trace!(target: LOG_TARGET,
506					"extract_from_transaction: no EVM extrinsic at tx_index {transaction_index} \
507					 in block #{} ({:?})", block.block_number(), block.block_hash());
508				ClientError::EthExtrinsicNotFound
509			})?;
510
511		let substrate_block_number = block.block_number();
512		let eth_block_number: U256 = substrate_block_number.into();
513		let eth_block_hash =
514			self.resolve_eth_block_hash(block.block_hash(), substrate_block_number).await;
515		let block_events = block.events().fetch().await.inspect_err(|err| {
516			log::debug!(target: LOG_TARGET, "Error fetching events for block #{substrate_block_number}: {err:?}");
517		})?;
518		let (reverted_extrinsics, mut logs_by_extrinsic) = extract_revive_events(
519			&block_events,
520			substrate_block_number,
521			eth_block_number,
522			eth_block_hash,
523			|idx| (idx == transaction_index).then_some(transaction_hash),
524		);
525
526		let reverted = reverted_extrinsics.contains(&transaction_index);
527		let logs = logs_by_extrinsic.remove(&transaction_index).unwrap_or_default();
528		self.decode_transaction_and_build_receipt(
529			eth_block_hash,
530			eth_block_number,
531			eth_call,
532			transaction_hash,
533			transaction_index,
534			receipt_gas_info,
535			reverted,
536			logs,
537		)
538	}
539
540	/// Get the Ethereum block hash for the Substrate block with specific hash.
541	pub async fn get_ethereum_block_hash(
542		&self,
543		block_hash: &H256,
544		block_number: SubstrateBlockNumber,
545	) -> Option<H256> {
546		(self.fetch_eth_block_hash)(*block_hash, block_number).await
547	}
548}
549
550#[cfg(test)]
551mod tests {
552	use super::*;
553
554	use pallet_revive::evm::{Account, TransactionLegacyUnsigned, TransactionUnsigned};
555
556	fn signed_call(account: &Account, tx: TransactionUnsigned) -> (EthTransact, H256) {
557		let payload = account.sign_transaction(tx).signed_payload();
558		let hash = H256(keccak_256(&payload));
559		(EthTransact { payload }, hash)
560	}
561
562	fn legacy_call_tx(to: H160) -> TransactionUnsigned {
563		TransactionUnsigned::from(TransactionLegacyUnsigned {
564			chain_id: Some(U256::from(1)),
565			to: Some(to),
566			gas: U256::from(21_000),
567			..Default::default()
568		})
569	}
570
571	fn gas_info() -> ReceiptGasInfoV1 {
572		ReceiptGasInfoV1 {
573			gas_used: U256::from(21_000),
574			effective_gas_price: U256::from(1_000_000_000),
575		}
576	}
577
578	#[test]
579	fn build_receipt_for_call() {
580		let extractor = ReceiptExtractor::new_mock();
581		let account = Account::default();
582		let eth_block_hash = H256::from([0xAB; 32]);
583		let block_number = U256::from(42);
584		let (call, tx_hash) = signed_call(&account, legacy_call_tx(account.address()));
585
586		// Successful call
587		let (signed_tx, receipt) = extractor
588			.decode_transaction_and_build_receipt(
589				eth_block_hash,
590				block_number,
591				call,
592				tx_hash,
593				3,
594				gas_info(),
595				false,
596				vec![],
597			)
598			.unwrap();
599
600		assert!(receipt.is_success());
601		assert_eq!(receipt.from, account.address());
602		assert_eq!(receipt.to, Some(account.address()));
603		assert_eq!(receipt.contract_address, None);
604		assert_eq!(receipt.block_hash, eth_block_hash);
605		assert_eq!(receipt.block_number, block_number);
606		assert_eq!(receipt.transaction_hash, tx_hash);
607		assert_eq!(receipt.transaction_index, U256::from(3));
608		assert_eq!(receipt.gas_used, U256::from(21_000));
609		assert_eq!(signed_tx.recover_eth_address().unwrap(), account.address());
610
611		// Same call, but reverted
612		let (call, tx_hash) = signed_call(&account, legacy_call_tx(account.address()));
613		let (_, receipt) = extractor
614			.decode_transaction_and_build_receipt(
615				eth_block_hash,
616				block_number,
617				call,
618				tx_hash,
619				3,
620				gas_info(),
621				true,
622				vec![],
623			)
624			.unwrap();
625
626		assert!(!receipt.is_success());
627		assert_eq!(receipt.from, account.address());
628	}
629
630	#[test]
631	fn build_receipt_for_deploy() {
632		let extractor = ReceiptExtractor::new_mock();
633		let account = Account::default();
634		let deploy_tx = TransactionUnsigned::from(TransactionLegacyUnsigned {
635			chain_id: Some(U256::from(1)),
636			gas: U256::from(100_000),
637			nonce: U256::from(0),
638			..Default::default()
639		});
640		let (call, tx_hash) = signed_call(&account, deploy_tx);
641
642		let (_, receipt) = extractor
643			.decode_transaction_and_build_receipt(
644				H256::zero(),
645				U256::from(1),
646				call,
647				tx_hash,
648				0,
649				gas_info(),
650				false,
651				vec![],
652			)
653			.unwrap();
654
655		assert!(receipt.is_success());
656		assert_eq!(receipt.to, None);
657		assert_eq!(receipt.contract_address, Some(create1(&account.address(), 0)));
658		assert_eq!(receipt.from, account.address());
659	}
660
661	#[test]
662	fn build_receipt_rejects_invalid_payload() {
663		let extractor = ReceiptExtractor::new_mock();
664
665		// Corrupt payload
666		let call = EthTransact { payload: vec![0xde, 0xad] };
667		let hash = H256(keccak_256(&call.payload));
668		let err = extractor
669			.decode_transaction_and_build_receipt(
670				H256::zero(),
671				U256::from(1),
672				call,
673				hash,
674				0,
675				gas_info(),
676				false,
677				vec![],
678			)
679			.unwrap_err();
680		assert!(matches!(err, ClientError::TxDecodingFailed));
681
682		// Valid payload but address recovery fails
683		let extractor = ReceiptExtractor {
684			recover_eth_address: Arc::new(|_| Err(())),
685			..ReceiptExtractor::new_mock()
686		};
687		let account = Account::default();
688		let (call, hash) = signed_call(&account, legacy_call_tx(account.address()));
689		let err = extractor
690			.decode_transaction_and_build_receipt(
691				H256::zero(),
692				U256::from(1),
693				call,
694				hash,
695				0,
696				gas_info(),
697				false,
698				vec![],
699			)
700			.unwrap_err();
701		assert!(matches!(err, ClientError::RecoverEthAddressFailed));
702	}
703
704	#[test]
705	fn defaults_and_first_evm_block_only_decreases() {
706		let extractor = ReceiptExtractor::new_mock();
707
708		assert!(extractor.first_evm_block().is_none());
709
710		// first_evm_block only decreases
711		extractor.set_first_evm_block(100);
712		assert_eq!(extractor.first_evm_block(), Some(100));
713
714		extractor.set_first_evm_block(50);
715		assert_eq!(extractor.first_evm_block(), Some(50));
716
717		// Higher value is ignored
718		extractor.set_first_evm_block(100);
719		assert_eq!(extractor.first_evm_block(), Some(50));
720	}
721
722	use codec::{Compact, Decode, Encode};
723	use frame_system::EventRecord;
724	use revive_dev_runtime::{Runtime, RuntimeEvent};
725	use subxt::{
726		PolkadotConfig,
727		client::OfflineClient,
728		config::{polkadot::PolkadotConfigBuilder, substrate::SpecVersionForRange},
729		events::Events,
730		metadata::Metadata,
731	};
732
733	/// Build `Events` by SCALE-encoding revive events against the generated runtime metadata.
734	struct EventsBuilder {
735		metadata: Metadata,
736		bytes: Vec<u8>,
737		count: u32,
738	}
739
740	impl EventsBuilder {
741		fn new() -> Self {
742			let metadata_bytes: &[u8] =
743				include_bytes!(concat!(env!("OUT_DIR"), "/revive_chain.scale"));
744			let metadata = Metadata::decode(&mut &metadata_bytes[..]).unwrap();
745			Self { metadata, bytes: Vec::new(), count: 0 }
746		}
747
748		fn push_event(
749			mut self,
750			phase: frame_system::Phase,
751			event: pallet_revive::Event<Runtime>,
752		) -> Self {
753			EventRecord::<RuntimeEvent, H256> {
754				phase,
755				event: RuntimeEvent::Revive(event),
756				topics: vec![],
757			}
758			.encode_to(&mut self.bytes);
759			self.count += 1;
760			self
761		}
762
763		fn build(self) -> Events<SrcChainConfig> {
764			let mut encoded_events = Vec::new();
765			Compact(self.count).encode_to(&mut encoded_events);
766			encoded_events.extend(self.bytes);
767
768			let config = PolkadotConfigBuilder::new()
769				.set_metadata_for_spec_versions(std::iter::once((0u32, self.metadata.into())))
770				.set_spec_version_for_block_ranges(std::iter::once(SpecVersionForRange {
771					block_range: 0..u64::MAX,
772					spec_version: 0,
773					transaction_version: 0,
774				}))
775				.build();
776			let client = OfflineClient::<PolkadotConfig>::new_with_config(config);
777			let at_block =
778				client.at_block(0u64).expect("spec version range covers all block numbers; qed");
779			at_block.events().from_bytes(encoded_events)
780		}
781	}
782
783	#[test]
784	fn extract_revive_events_decodes_contract_emitted_log() {
785		let contract = H160::from([0x11; 20]);
786		let topics = vec![H256::from([0x22; 32]), H256::from([0x33; 32])];
787		let data = vec![0xde, 0xad, 0xbe, 0xef];
788		let events = EventsBuilder::new()
789			.push_event(
790				frame_system::Phase::ApplyExtrinsic(5),
791				pallet_revive::Event::ContractEmitted {
792					contract,
793					data: data.clone(),
794					topics: topics.clone(),
795				},
796			)
797			.build();
798
799		let tx_hash = H256::from([0xAA; 32]);
800		let eth_block_hash = H256::from([0xBB; 32]);
801		let substrate_block_number = 42u64;
802		let eth_block_number = U256::from(substrate_block_number);
803
804		let (reverts, logs) = extract_revive_events(
805			&events,
806			substrate_block_number,
807			eth_block_number,
808			eth_block_hash,
809			|idx| (idx == 5).then_some(tx_hash),
810		);
811
812		assert!(reverts.is_empty());
813		assert_eq!(logs.len(), 1);
814		let log = &logs[&5][0];
815		assert_eq!(log.address, contract);
816		assert_eq!(log.topics, topics);
817		assert_eq!(log.data.as_ref().unwrap().0, data);
818		assert_eq!(log.block_hash, eth_block_hash);
819		assert_eq!(log.block_number, eth_block_number);
820		assert_eq!(log.transaction_hash, tx_hash);
821		assert_eq!(log.transaction_index, U256::from(5));
822	}
823
824	#[test]
825	fn extract_revive_events_skips_irrelevant_events() {
826		// Events outside `ApplyExtrinsic` and events for extrinsics the tx-hash closure
827		// doesn't resolve are both dropped.
828		let empty_contract_emitted = pallet_revive::Event::ContractEmitted {
829			contract: H160::zero(),
830			data: vec![],
831			topics: vec![],
832		};
833		let revert = pallet_revive::Event::EthExtrinsicRevert {
834			dispatch_error: sp_runtime::DispatchError::Other("skipped-phase revert"),
835		};
836		let events = EventsBuilder::new()
837			.push_event(frame_system::Phase::Finalization, empty_contract_emitted.clone())
838			.push_event(frame_system::Phase::Initialization, revert.clone())
839			.push_event(frame_system::Phase::ApplyExtrinsic(5), empty_contract_emitted)
840			.push_event(frame_system::Phase::ApplyExtrinsic(5), revert)
841			.build();
842
843		// The tx-hash closure returns `Some` only for extrinsic 7 (not present)
844		let (reverts, logs) =
845			extract_revive_events(&events, 0, U256::zero(), H256::zero(), |idx| {
846				(idx == 7).then_some(H256::zero())
847			});
848
849		assert!(reverts.is_empty());
850		assert!(logs.is_empty());
851	}
852
853	#[test]
854	fn extract_revive_events_accumulates_per_extrinsic() {
855		let tx0 = H256::from([0x01; 32]);
856		let tx1 = H256::from([0x02; 32]);
857		let tx2 = H256::from([0x03; 32]);
858		let emitted_by = |contract: H160| pallet_revive::Event::ContractEmitted {
859			contract,
860			data: vec![],
861			topics: vec![],
862		};
863		let events = EventsBuilder::new()
864			.push_event(frame_system::Phase::ApplyExtrinsic(0), emitted_by(H160::from([0xaa; 20])))
865			.push_event(frame_system::Phase::ApplyExtrinsic(0), emitted_by(H160::from([0xbb; 20])))
866			.push_event(
867				frame_system::Phase::ApplyExtrinsic(1),
868				pallet_revive::Event::EthExtrinsicRevert {
869					dispatch_error: sp_runtime::DispatchError::Other("tx-1 revert"),
870				},
871			)
872			.push_event(frame_system::Phase::ApplyExtrinsic(2), emitted_by(H160::from([0xcc; 20])))
873			.build();
874
875		let (reverts, logs) =
876			extract_revive_events(&events, 0, U256::zero(), H256::zero(), |idx| match idx {
877				0 => Some(tx0),
878				1 => Some(tx1),
879				2 => Some(tx2),
880				_ => None,
881			});
882
883		assert_eq!(reverts, [1usize].into_iter().collect::<HashSet<_>>());
884		assert_eq!(logs[&0].len(), 2);
885		assert_eq!(logs[&2].len(), 1);
886		// log_index is block-wide
887		assert_eq!(logs[&0][0].log_index, U256::from(0));
888		assert_eq!(logs[&0][1].log_index, U256::from(1));
889		assert_eq!(logs[&2][0].log_index, U256::from(3));
890	}
891}