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