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, SyntheticTransactionV1,
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///
104/// `ContractEmitted` logs whose extrinsic is not an ethereum transaction (`eth_tx_hash_for`
105/// returns `None`) are "outside-of-frame" logs — emitted by a runtime component (e.g. a
106/// pallet-assets balance-change mirror) during a plain extrinsic. The runtime aggregates them into
107/// one synthetic transaction per block; here they are collected into a separate bucket, tagged with
108/// the synthetic transaction's hash and index, and returned alongside the per-extrinsic logs.
109///
110/// Events are stored sequentially without size markers, so a single
111/// undecodable event (e.g. from a runtime upgrade that shifted variant
112/// indices) corrupts the offset for all subsequent events.
113/// Decode errors are logged and skipped to avoid losing the entire receipt.
114///
115/// Returns `(reverted_extrinsics, logs_by_extrinsic, outside_frame_logs)`.
116fn extract_revive_events(
117	block_events: &subxt::events::Events<SrcChainConfig>,
118	substrate_block_number: SubstrateBlockNumber,
119	eth_block_number: U256,
120	eth_block_hash: H256,
121	eth_tx_hash_for: impl Fn(usize) -> Option<H256>,
122	synthetic_tx_hash: H256,
123	synthetic_tx_index: usize,
124) -> (HashSet<usize>, HashMap<usize, Vec<Log>>, Vec<Log>) {
125	let mut reverted_extrinsics: HashSet<usize> = HashSet::new();
126	let mut logs_by_extrinsic: HashMap<usize, Vec<Log>> = HashMap::new();
127	let mut outside_frame_logs: Vec<Log> = Vec::new();
128
129	for (event_index, event_result) in block_events.iter().enumerate() {
130		let event = match event_result {
131			Ok(e) => e,
132			Err(err) => {
133				log::debug!(
134					target: LOG_TARGET,
135					"Failed to decode event {event_index} in block #{substrate_block_number}: {err:?}"
136				);
137				continue;
138			},
139		};
140
141		// Only an `ApplyExtrinsic` event can belong to an ethereum transaction. A mirror firing in
142		// `on_initialize` (e.g. the message queue servicing an inbound XCM asset deposit) is
143		// committed into the synthetic transaction just like one from a plain extrinsic, so treat
144		// every other phase as outside-of-frame rather than dropping it.
145		let eth_tx = match event.phase() {
146			Phase::ApplyExtrinsic(idx) => {
147				let idx = idx as usize;
148				eth_tx_hash_for(idx).map(|hash| (idx, hash))
149			},
150			Phase::Initialization | Phase::Finalization => None,
151		};
152
153		match eth_tx {
154			Some((extrinsic_index, eth_tx_hash)) => match decode_revive_event(
155				&event,
156				eth_block_number,
157				eth_tx_hash,
158				extrinsic_index,
159				eth_block_hash,
160			) {
161				Some(ReviveEvent::Revert) => {
162					reverted_extrinsics.insert(extrinsic_index);
163				},
164				Some(ReviveEvent::Log(log)) => {
165					logs_by_extrinsic.entry(extrinsic_index).or_default().push(log);
166				},
167				None => {},
168			},
169			// Not an ethereum transaction: a `ContractEmitted` here is an outside-of-frame log,
170			// attributed to the block's synthetic transaction. Reverts are meaningless here.
171			None => {
172				if let Some(ReviveEvent::Log(log)) = decode_revive_event(
173					&event,
174					eth_block_number,
175					synthetic_tx_hash,
176					synthetic_tx_index,
177					eth_block_hash,
178				) {
179					outside_frame_logs.push(log);
180				}
181			},
182		}
183	}
184
185	(reverted_extrinsics, logs_by_extrinsic, outside_frame_logs)
186}
187
188/// Returns the revive transactions from a block.
189fn extract_eth_transacts<C: OfflineClientAtBlockT<SrcChainConfig>>(
190	block_extrinsics: &Extrinsics<'_, SrcChainConfig, C>,
191	block_number: SubstrateBlockNumber,
192) -> Result<Vec<(EthTransact, usize)>, ClientError> {
193	let mut extrinsics = Vec::new();
194	for (ext_idx, ext) in block_extrinsics.iter().enumerate() {
195		let ext = match ext {
196			Ok(ext) => ext,
197			// Don't error here since the call type is unknown. An undecodable `eth_transact` shows
198			// up as a length mismatch against the runtime's gas entries.
199			Err(err) => {
200				log::debug!(target: LOG_TARGET,
201					"Failed to decode extrinsic {ext_idx} of block #{block_number}: {err:?}");
202				continue;
203			},
204		};
205		match ext.decode_call_data_fields_as::<EthTransact>() {
206			Some(Ok(call)) => extrinsics.push((call, ext_idx)),
207			Some(Err(err)) => {
208				log::error!(target: LOG_TARGET,
209					"Failed to decode the EthTransact call in extrinsic {ext_idx} of block \
210					#{block_number}: {err:?}");
211				return Err(subxt::Error::from(err).into());
212			},
213			// Not a revive transaction.
214			None => {},
215		}
216	}
217
218	Ok(extrinsics)
219}
220
221/// The transaction index the block's synthetic transaction is served at.
222///
223/// An ethereum transaction is served at its extrinsic index, and every consumer of a transaction
224/// index treats it as one: `trace_tx` replays the extrinsic at that index, `post_dispatch_weight`
225/// reads that extrinsic's events. So the synthetic transaction takes the one index no extrinsic
226/// occupies, one past the last. The next slot after the last ethereum transaction would do only
227/// while no other extrinsic follows it.
228fn synthetic_tx_index<C: OfflineClientAtBlockT<SrcChainConfig>>(
229	block_extrinsics: &Extrinsics<'_, SrcChainConfig, C>,
230) -> usize {
231	block_extrinsics.len()
232}
233
234/// Reconcile the runtime's per-transaction gas entries against the ethereum transactions decoded
235/// from the block body.
236///
237/// A difference means the two sides disagree about what the block contains — most likely an
238/// extrinsic the metadata could not decode. Erroring beats pairing every later transaction with the
239/// wrong gas info.
240fn check_receipt_data_len(
241	receipt_data: &[ReceiptGasInfoV1],
242	extrinsics_len: usize,
243) -> Result<(), ClientError> {
244	if receipt_data.len() != extrinsics_len {
245		log::error!(
246			target: LOG_TARGET,
247			"Receipt data length ({}) does not match extrinsics length ({extrinsics_len})",
248			receipt_data.len(),
249		);
250		return Err(ClientError::ReceiptDataLengthMismatch);
251	}
252
253	Ok(())
254}
255
256/// Pick out of the logs rebuilt from block events the ones the block committed to its synthetic
257/// transaction, by the event index the runtime reports for each, and number them from
258/// `first_log_index`.
259///
260/// The events hold more: a contract log emitted outside an ethereum transaction is deposited but
261/// not buffered, and so is a log arriving past the buffer's cap. Serving those would hand out logs
262/// the header does not commit to. A committed index with no decoded log behind it is a decode
263/// failure (see [`extract_revive_events`]); the receipt is served without that log.
264///
265/// The event index serves only the selection. The synthetic transaction is the block's last, so
266/// its logs are numbered after every other log of the block: from the block's event count, which
267/// every log of an ethereum transaction stays below. Keeping the event index would send a mirror
268/// from `on_initialize` or an early plain extrinsic ahead of later transactions' logs in
269/// `eth_getLogs`, which orders by `log_index`.
270fn select_committed_outside_frame_logs(
271	logs: Vec<Log>,
272	log_event_indices: &[u32],
273	substrate_block_number: SubstrateBlockNumber,
274	first_log_index: u32,
275) -> Vec<Log> {
276	let mut by_event_index: HashMap<U256, Log> =
277		logs.into_iter().map(|log| (log.log_index, log)).collect();
278	log_event_indices
279		.iter()
280		.filter_map(|event_index| {
281			let log = by_event_index.remove(&U256::from(*event_index));
282			if log.is_none() {
283				log::warn!(
284					target: LOG_TARGET,
285					"Block #{substrate_block_number} committed the log at event {event_index} to its \
286					synthetic transaction but no such log decoded from its events",
287				);
288			}
289			log
290		})
291		.enumerate()
292		.map(|(position, mut log)| {
293			log.log_index = U256::from(first_log_index as u64 + position as u64);
294			log
295		})
296		.collect()
297}
298
299/// Outcome of querying the runtime for a block's receipt gas entries.
300enum ReceiptData {
301	/// The block's entries: one per ethereum transaction, plus the synthetic transaction's when the
302	/// runtime reports one.
303	Available { receipt_data: Vec<ReceiptGasInfoV1>, synthetic: Option<SyntheticTransactionV1> },
304	/// The runtime at this block has no `eth_receipt_data` API.
305	///
306	/// Permanent, and expected for pre-EVM history — such a block has neither ethereum
307	/// transactions nor mirrored logs, so it is read as having no entries rather than as an error.
308	Unsupported,
309	/// The query failed. May succeed on retry, so it must not be mistaken for an empty block.
310	Failed,
311}
312
313type FetchReceiptDataFn =
314	Arc<dyn Fn(SubstrateBlock) -> Pin<Box<dyn Future<Output = ReceiptData> + Send>> + Send + Sync>;
315
316type FetchEthBlockHashFn = Arc<
317	dyn Fn(H256, SubstrateBlockNumber) -> Pin<Box<dyn Future<Output = Option<H256>> + Send>>
318		+ Send
319		+ Sync,
320>;
321
322type RecoverEthAddressFn = Arc<dyn Fn(&TransactionSigned) -> Result<H160, ()> + Send + Sync>;
323
324/// Utility to extract receipts from extrinsics.
325#[derive(Clone)]
326pub struct ReceiptExtractor {
327	/// Fetch the receipt data info.
328	fetch_receipt_data: FetchReceiptDataFn,
329
330	/// Fetch ethereum block hash.
331	fetch_eth_block_hash: FetchEthBlockHashFn,
332
333	/// Auto-discovered first EVM block on the chain.
334	/// Set once during backward sync when the first non-EVM block is encountered.
335	/// Uses `u64::MAX` as sentinel for "not yet discovered".
336	first_evm_block: Arc<AtomicU64>,
337
338	/// Recover the ethereum address from a transaction signature.
339	recover_eth_address: RecoverEthAddressFn,
340
341	/// EVM chain id. Used to rebuild the synthetic transaction that carries a block's
342	/// outside-of-frame logs (its hash must match the one the runtime committed).
343	chain_id: u64,
344}
345
346impl ReceiptExtractor {
347	/// Create a new `ReceiptExtractor`.
348	///
349	/// `chain_id` is the chain's EVM chain id (the pallet-revive `ChainId` constant).
350	pub fn new(runtime_api_provider: VersionAwareRuntimeApiProvider, chain_id: u64) -> Self {
351		Self::new_with_custom_address_recovery(
352			runtime_api_provider,
353			chain_id,
354			Arc::new(|signed_tx: &TransactionSigned| signed_tx.recover_eth_address()),
355		)
356	}
357
358	/// Create a new `ReceiptExtractor` with custom Ethereum address recovery logic.
359	///
360	/// Use `ReceiptExtractor::new` if the default Ethereum address recovery
361	/// logic ([`TransactionSigned::recover_eth_address`] based) is enough.
362	pub fn new_with_custom_address_recovery(
363		runtime_api_provider: VersionAwareRuntimeApiProvider,
364		chain_id: u64,
365		recover_eth_address_fn: RecoverEthAddressFn,
366	) -> Self {
367		let provider = runtime_api_provider.clone();
368		let fetch_eth_block_hash = Arc::new(move |substrate_block_hash, substrate_block_number| {
369			let provider = provider.clone();
370
371			let fut = async move {
372				let runtime_api = provider
373					.at_block_hash_and_number(substrate_block_hash, substrate_block_number)
374					.await
375					.inspect_err(|err| {
376						log::debug!(
377							target: LOG_TARGET,
378							"Failed to access the runtime API at block #{substrate_block_number} \
379							({substrate_block_hash:?}) for an eth_block_hash query: {err:?}"
380						);
381					})
382					.ok()?;
383				runtime_api
384					.eth_block_hash(U256::from(substrate_block_number))?
385					.await
386					.inspect_err(|err| {
387						log::debug!(
388							target: LOG_TARGET,
389							"Failed to query eth_block_hash at block #{substrate_block_number} \
390							({substrate_block_hash:?}): {err:?}"
391						);
392					})
393					.ok()
394					.flatten()
395			};
396
397			Box::pin(fut) as Pin<Box<_>>
398		});
399
400		let provider = runtime_api_provider;
401		let fetch_receipt_data = Arc::new(move |at_block: SubstrateBlock| {
402			let provider = provider.clone();
403
404			let fut = async move {
405				let block_hash = at_block.block_hash();
406				let runtime_api = match provider.at_resolved_block(at_block).await {
407					Ok(api) => api,
408					Err(err) => {
409						log::debug!(
410							target: LOG_TARGET,
411							"Failed to access the runtime API at block {block_hash:?} for an \
412							eth_receipt_data query: {err:?}"
413						);
414						return ReceiptData::Failed;
415					},
416				};
417				let Some(query) = runtime_api.eth_receipt_data() else {
418					return ReceiptData::Unsupported;
419				};
420				match query.await {
421					Ok((receipt_data, synthetic)) => {
422						ReceiptData::Available { receipt_data, synthetic }
423					},
424					Err(err) => {
425						log::debug!(
426							target: LOG_TARGET,
427							"Failed to query eth_receipt_data at block {block_hash:?}: {err:?}"
428						);
429						ReceiptData::Failed
430					},
431				}
432			};
433
434			Box::pin(fut) as Pin<Box<_>>
435		});
436
437		Self {
438			fetch_receipt_data,
439			fetch_eth_block_hash,
440			first_evm_block: Arc::new(AtomicU64::new(u64::MAX)),
441			recover_eth_address: recover_eth_address_fn,
442			chain_id,
443		}
444	}
445
446	#[cfg(test)]
447	pub fn new_mock() -> Self {
448		let fetch_receipt_data =
449			Arc::new(|_| Box::pin(std::future::ready(ReceiptData::Unsupported)) as Pin<Box<_>>);
450		// This method is useful when testing eth - substrate mapping.
451		let fetch_eth_block_hash =
452			Arc::new(|block_hash: H256, block_number: SubstrateBlockNumber| {
453				// Generate hash from substrate block hash and number
454				let bytes: Vec<u8> = [block_hash.as_bytes(), &block_number.to_be_bytes()].concat();
455				let eth_block_hash = H256::from(keccak_256(&bytes));
456				Box::pin(std::future::ready(Some(eth_block_hash))) as Pin<Box<_>>
457			});
458
459		Self {
460			fetch_receipt_data,
461			fetch_eth_block_hash,
462			first_evm_block: Arc::new(AtomicU64::new(u64::MAX)),
463			recover_eth_address: Arc::new(|signed_tx: &TransactionSigned| {
464				signed_tx.recover_eth_address()
465			}),
466			chain_id: 420_420_420,
467		}
468	}
469
470	/// Check if the block is before the `first_evm_block` floor.
471	/// When sentinel (`u64::MAX`), no blocks are rejected (permissive default).
472	pub fn is_before_first_evm_block(&self, block_number: SubstrateBlockNumber) -> bool {
473		let val = self.first_evm_block.load(Ordering::Acquire);
474		val != u64::MAX && block_number < val
475	}
476
477	/// Set the first EVM block. Only stores if lower than the current value.
478	pub fn set_first_evm_block(&self, block_number: SubstrateBlockNumber) {
479		let prev = self.first_evm_block.fetch_min(block_number, Ordering::AcqRel);
480		if block_number > prev {
481			log::debug!(target: LOG_TARGET,
482				"Ignored attempt to raise first_evm_block to #{block_number}, current is #{prev}");
483		}
484	}
485
486	/// The auto-discovered first EVM block, or `None` if not yet discovered.
487	pub fn first_evm_block(&self) -> Option<SubstrateBlockNumber> {
488		let val = self.first_evm_block.load(Ordering::Acquire);
489		(val != u64::MAX).then_some(val)
490	}
491
492	/// Resolve the Ethereum block hash for a substrate block, falling back to the substrate hash.
493	async fn resolve_eth_block_hash(
494		&self,
495		substrate_block_hash: H256,
496		substrate_block_number: SubstrateBlockNumber,
497	) -> H256 {
498		match (self.fetch_eth_block_hash)(substrate_block_hash, substrate_block_number).await {
499			Some(hash) => hash,
500			None => {
501				log::trace!(target: LOG_TARGET,
502					"eth_block_hash returned None for substrate block \
503					 #{substrate_block_number} ({substrate_block_hash:?}), \
504					 falling back to substrate hash as ETH hash");
505				substrate_block_hash
506			},
507		}
508	}
509
510	/// Decode the raw call payload into a [`TransactionSigned`] and construct its [`ReceiptInfo`].
511	fn decode_transaction_and_build_receipt(
512		&self,
513		eth_block_hash: H256,
514		block_number: U256,
515		call: EthTransact,
516		transaction_hash: H256,
517		transaction_index: usize,
518		receipt_gas_info: ReceiptGasInfoV1,
519		reverted: bool,
520		logs: Vec<Log>,
521	) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
522		let signed_tx =
523			TransactionSigned::decode(&call.payload).map_err(|_| ClientError::TxDecodingFailed)?;
524		let from = (self.recover_eth_address)(&signed_tx).map_err(|_| {
525			log::error!(target: LOG_TARGET, "Failed to recover eth address from signed tx");
526			ClientError::RecoverEthAddressFailed
527		})?;
528
529		let tx_info = GenericTransaction::from_signed(
530			signed_tx.clone(),
531			receipt_gas_info.effective_gas_price,
532			Some(from),
533		);
534
535		let contract_address = if tx_info.to.is_none() {
536			Some(create1(
537				&from,
538				tx_info
539					.nonce
540					.unwrap_or_default()
541					.try_into()
542					.map_err(|_| ClientError::ConversionFailed)?,
543			))
544		} else {
545			None
546		};
547
548		let receipt = ReceiptInfo::new(
549			eth_block_hash,
550			block_number,
551			contract_address,
552			from,
553			logs,
554			tx_info.to,
555			receipt_gas_info.effective_gas_price,
556			U256::from(receipt_gas_info.gas_used),
557			!reverted,
558			transaction_hash,
559			transaction_index.into(),
560			tx_info.r#type.unwrap_or_default(),
561		);
562		Ok((signed_tx, receipt))
563	}
564
565	/// Rebuild the block's synthetic transaction payload and its hash. Must reproduce the exact
566	/// bytes the runtime committed, hence the shared
567	/// [`pallet_revive::evm::synthetic_log_transaction`] keyed by chain id and block number.
568	fn synthetic_tx(&self, eth_block_number: U256) -> (Vec<u8>, H256) {
569		let payload = pallet_revive::evm::synthetic_log_transaction(
570			eth_block_number,
571			U256::from(self.chain_id),
572		);
573		let hash = H256(keccak_256(&payload));
574		(payload, hash)
575	}
576
577	/// Assemble the receipt for the block's synthetic transaction, carrying its outside-of-frame
578	/// logs. The synthetic transaction has no real sender: `from` is zero and `to` /
579	/// `contract_address` are `None`.
580	fn build_synthetic_receipt(
581		&self,
582		eth_block_hash: H256,
583		eth_block_number: U256,
584		transaction_index: usize,
585		gas_info: ReceiptGasInfoV1,
586		logs: Vec<Log>,
587	) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
588		let (payload, transaction_hash) = self.synthetic_tx(eth_block_number);
589		let signed_tx =
590			TransactionSigned::decode(&payload).map_err(|_| ClientError::TxDecodingFailed)?;
591		let receipt = ReceiptInfo::new(
592			eth_block_hash,
593			eth_block_number,
594			None,
595			H160::zero(),
596			logs,
597			None,
598			gas_info.effective_gas_price,
599			U256::from(gas_info.gas_used),
600			true,
601			transaction_hash,
602			transaction_index.into(),
603			Default::default(),
604		);
605		Ok((signed_tx, receipt))
606	}
607
608	/// Extract receipts from block.
609	pub async fn extract_from_block(
610		&self,
611		block: &SubstrateBlock,
612	) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
613		let eth_block_hash =
614			self.resolve_eth_block_hash(block.block_hash(), block.block_number()).await;
615
616		self.extract_from_block_with_eth_hash(block, eth_block_hash).await
617	}
618
619	/// Extract receipts from block, using a pre-fetched ethereum block hash.
620	///
621	/// Fetches block events once in a single pass before building receipts.
622	pub async fn extract_from_block_with_eth_hash(
623		&self,
624		block: &SubstrateBlock,
625		eth_block_hash: H256,
626	) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
627		if self.is_before_first_evm_block(block.block_number()) {
628			return Ok(vec![]);
629		}
630
631		let (extrinsics, synthetic, synthetic_tx_index) = self.get_block_extrinsics(block).await?;
632		let eth_tx_by_index: BTreeMap<usize, (EthTransact, H256, ReceiptGasInfoV1)> = extrinsics
633			.into_iter()
634			.map(|(call, receipt_gas_info, extrinsic_index)| {
635				let hash = H256(keccak_256(&call.payload));
636				(extrinsic_index, (call, hash, receipt_gas_info))
637			})
638			.collect();
639
640		// Nothing to reconstruct: no ethereum transactions and no synthetic transaction (the
641		// latter is present iff the runtime reported one).
642		if eth_tx_by_index.is_empty() && synthetic.is_none() {
643			return Ok(vec![]);
644		}
645
646		let substrate_block_number = block.block_number();
647		let eth_block_number: U256 = substrate_block_number.into();
648		let (_, synthetic_tx_hash) = self.synthetic_tx(eth_block_number);
649		let block_events = block.events().fetch().await.inspect_err(|err| {
650			log::debug!(target: LOG_TARGET, "Error fetching events for block #{substrate_block_number}: {err:?}");
651		})?;
652		let (reverted_extrinsics, mut logs_by_extrinsic, outside_frame_logs) =
653			extract_revive_events(
654				&block_events,
655				substrate_block_number,
656				eth_block_number,
657				eth_block_hash,
658				|idx| eth_tx_by_index.get(&idx).map(|(_, hash, _)| *hash),
659				synthetic_tx_hash,
660				synthetic_tx_index,
661			);
662
663		let mut receipts: Vec<_> = eth_tx_by_index
664			.into_iter()
665			.map(|(transaction_index, (call, transaction_hash, receipt_gas_info))| {
666				let reverted = reverted_extrinsics.contains(&transaction_index);
667				let logs = logs_by_extrinsic.remove(&transaction_index).unwrap_or_default();
668				self.decode_transaction_and_build_receipt(
669					eth_block_hash,
670					eth_block_number,
671					call,
672					transaction_hash,
673					transaction_index,
674					receipt_gas_info,
675					reverted,
676					logs,
677				)
678				.inspect_err(|err| {
679					log::warn!(target: LOG_TARGET, "Error extracting extrinsic: {err:?}");
680				})
681			})
682			.collect::<Result<Vec<_>, _>>()?;
683
684		// Append the synthetic transaction receipt for the block's outside-of-frame logs. Keyed on
685		// the runtime's entry — the same condition `extract_from_transaction` uses — because that
686		// entry is what the block header commits to. Deciding on the decoded logs instead would
687		// omit a transaction the block contains whenever the logs fail to decode.
688		if let Some(synthetic) = synthetic {
689			let logs = select_committed_outside_frame_logs(
690				outside_frame_logs,
691				&synthetic.log_event_indices,
692				substrate_block_number,
693				block_events.len(),
694			);
695
696			receipts.push(self.build_synthetic_receipt(
697				eth_block_hash,
698				eth_block_number,
699				synthetic_tx_index,
700				synthetic.gas_info,
701				logs,
702			)?);
703		}
704
705		Ok(receipts)
706	}
707
708	/// Return the ETH extrinsics of the block grouped with reconstruction receipt info and
709	/// extrinsic index, plus the block's synthetic transaction when it has one and the index it
710	/// is served at.
711	///
712	/// See [`check_receipt_data_len`] for how the gas entries are reconciled.
713	async fn get_block_extrinsics(
714		&self,
715		block: &SubstrateBlock,
716	) -> Result<
717		(Vec<(EthTransact, ReceiptGasInfoV1, usize)>, Option<SyntheticTransactionV1>, usize),
718		ClientError,
719	> {
720		let block_extrinsics = block.extrinsics().fetch().await.inspect_err(|err| {
721			log::debug!(target: LOG_TARGET, "Error fetching for #{:?} extrinsics: {err:?}", block.block_number());
722		})?;
723
724		let block_number = block.block_number();
725		let extrinsics = extract_eth_transacts(&block_extrinsics, block_number)?;
726		let synthetic_tx_index = synthetic_tx_index(&block_extrinsics);
727
728		// Queried unconditionally: a block with no ethereum transactions can still carry a
729		// synthetic transaction for its outside-of-frame logs.
730		let (receipt_data, synthetic) = match (self.fetch_receipt_data)(block.clone()).await {
731			ReceiptData::Available { receipt_data, synthetic } => (receipt_data, synthetic),
732			// A block predating the runtime API has no ethereum transactions and no mirrored
733			// logs, so it reconstructs as an empty block instead of failing the request.
734			ReceiptData::Unsupported => (Vec::new(), None),
735			ReceiptData::Failed => {
736				log::trace!(target: LOG_TARGET,
737					"Receipt data not found for block #{} ({:?})",
738					block.block_number(), block.block_hash());
739				return Err(ClientError::ReceiptDataNotFound);
740			},
741		};
742
743		check_receipt_data_len(&receipt_data, extrinsics.len())?;
744
745		Ok((
746			extrinsics
747				.into_iter()
748				.zip(receipt_data)
749				.map(|((call, ext_idx), rec)| (call, rec, ext_idx))
750				.collect(),
751			synthetic,
752			synthetic_tx_index,
753		))
754	}
755
756	/// Extract a [`TransactionSigned`] and a [`ReceiptInfo`] for a specific transaction in a
757	/// [`SubstrateBlock`]
758	pub async fn extract_from_transaction(
759		&self,
760		block: &SubstrateBlock,
761		transaction_index: usize,
762	) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
763		let (extrinsics, synthetic, synthetic_tx_index) = self.get_block_extrinsics(block).await?;
764		let mut eth_tx_by_index: BTreeMap<usize, (EthTransact, H256, ReceiptGasInfoV1)> =
765			extrinsics
766				.into_iter()
767				.map(|(call, receipt_gas_info, extrinsic_index)| {
768					let hash = H256(keccak_256(&call.payload));
769					(extrinsic_index, (call, hash, receipt_gas_info))
770				})
771				.collect();
772
773		let is_synthetic = transaction_index == synthetic_tx_index && synthetic.is_some();
774
775		if !eth_tx_by_index.contains_key(&transaction_index) && !is_synthetic {
776			log::trace!(target: LOG_TARGET,
777				"extract_from_transaction: no EVM extrinsic at tx_index {transaction_index} \
778				 in block #{} ({:?})", block.block_number(), block.block_hash());
779			return Err(ClientError::EthExtrinsicNotFound);
780		}
781
782		let substrate_block_number = block.block_number();
783		let eth_block_number: U256 = substrate_block_number.into();
784		let eth_block_hash =
785			self.resolve_eth_block_hash(block.block_hash(), substrate_block_number).await;
786		let (_, synthetic_tx_hash) = self.synthetic_tx(eth_block_number);
787		let block_events = block.events().fetch().await.inspect_err(|err| {
788			log::debug!(target: LOG_TARGET, "Error fetching events for block #{substrate_block_number}: {err:?}");
789		})?;
790		let (reverted_extrinsics, mut logs_by_extrinsic, outside_frame_logs) =
791			extract_revive_events(
792				&block_events,
793				substrate_block_number,
794				eth_block_number,
795				eth_block_hash,
796				|idx| eth_tx_by_index.get(&idx).map(|(_, hash, _)| *hash),
797				synthetic_tx_hash,
798				synthetic_tx_index,
799			);
800
801		if is_synthetic {
802			let synthetic = synthetic.expect("is_synthetic implies Some; qed");
803			let logs = select_committed_outside_frame_logs(
804				outside_frame_logs,
805				&synthetic.log_event_indices,
806				substrate_block_number,
807				block_events.len(),
808			);
809			return self.build_synthetic_receipt(
810				eth_block_hash,
811				eth_block_number,
812				synthetic_tx_index,
813				synthetic.gas_info,
814				logs,
815			);
816		}
817
818		let (eth_call, transaction_hash, receipt_gas_info) =
819			eth_tx_by_index.remove(&transaction_index).expect("presence checked above; qed");
820		let reverted = reverted_extrinsics.contains(&transaction_index);
821		let logs = logs_by_extrinsic.remove(&transaction_index).unwrap_or_default();
822		self.decode_transaction_and_build_receipt(
823			eth_block_hash,
824			eth_block_number,
825			eth_call,
826			transaction_hash,
827			transaction_index,
828			receipt_gas_info,
829			reverted,
830			logs,
831		)
832	}
833
834	/// Get the Ethereum block hash for the Substrate block with specific hash.
835	pub async fn get_ethereum_block_hash(
836		&self,
837		block_hash: &H256,
838		block_number: SubstrateBlockNumber,
839	) -> Option<H256> {
840		(self.fetch_eth_block_hash)(*block_hash, block_number).await
841	}
842}
843
844#[cfg(test)]
845mod tests {
846	use super::*;
847
848	use pallet_revive::evm::{Account, TransactionLegacyUnsigned, TransactionUnsigned};
849
850	fn signed_call(account: &Account, tx: TransactionUnsigned) -> (EthTransact, H256) {
851		let payload = account.sign_transaction(tx).signed_payload();
852		let hash = H256(keccak_256(&payload));
853		(EthTransact { payload }, hash)
854	}
855
856	fn legacy_call_tx(to: H160) -> TransactionUnsigned {
857		TransactionUnsigned::from(TransactionLegacyUnsigned {
858			chain_id: Some(U256::from(1)),
859			to: Some(to),
860			gas: U256::from(21_000),
861			..Default::default()
862		})
863	}
864
865	fn gas_info() -> ReceiptGasInfoV1 {
866		ReceiptGasInfoV1 {
867			gas_used: U256::from(21_000),
868			effective_gas_price: U256::from(1_000_000_000),
869		}
870	}
871
872	#[test]
873	fn build_receipt_for_call() {
874		let extractor = ReceiptExtractor::new_mock();
875		let account = Account::default();
876		let eth_block_hash = H256::from([0xAB; 32]);
877		let block_number = U256::from(42);
878		let (call, tx_hash) = signed_call(&account, legacy_call_tx(account.address()));
879
880		// Successful call
881		let (signed_tx, receipt) = extractor
882			.decode_transaction_and_build_receipt(
883				eth_block_hash,
884				block_number,
885				call,
886				tx_hash,
887				3,
888				gas_info(),
889				false,
890				vec![],
891			)
892			.unwrap();
893
894		assert!(receipt.is_success());
895		assert_eq!(receipt.from, account.address());
896		assert_eq!(receipt.to, Some(account.address()));
897		assert_eq!(receipt.contract_address, None);
898		assert_eq!(receipt.block_hash, eth_block_hash);
899		assert_eq!(receipt.block_number, block_number);
900		assert_eq!(receipt.transaction_hash, tx_hash);
901		assert_eq!(receipt.transaction_index, U256::from(3));
902		assert_eq!(receipt.gas_used, U256::from(21_000));
903		assert_eq!(signed_tx.recover_eth_address().unwrap(), account.address());
904
905		// Same call, but reverted
906		let (call, tx_hash) = signed_call(&account, legacy_call_tx(account.address()));
907		let (_, receipt) = extractor
908			.decode_transaction_and_build_receipt(
909				eth_block_hash,
910				block_number,
911				call,
912				tx_hash,
913				3,
914				gas_info(),
915				true,
916				vec![],
917			)
918			.unwrap();
919
920		assert!(!receipt.is_success());
921		assert_eq!(receipt.from, account.address());
922	}
923
924	#[test]
925	fn build_receipt_for_deploy() {
926		let extractor = ReceiptExtractor::new_mock();
927		let account = Account::default();
928		let deploy_tx = TransactionUnsigned::from(TransactionLegacyUnsigned {
929			chain_id: Some(U256::from(1)),
930			gas: U256::from(100_000),
931			nonce: U256::from(0),
932			..Default::default()
933		});
934		let (call, tx_hash) = signed_call(&account, deploy_tx);
935
936		let (_, receipt) = extractor
937			.decode_transaction_and_build_receipt(
938				H256::zero(),
939				U256::from(1),
940				call,
941				tx_hash,
942				0,
943				gas_info(),
944				false,
945				vec![],
946			)
947			.unwrap();
948
949		assert!(receipt.is_success());
950		assert_eq!(receipt.to, None);
951		assert_eq!(receipt.contract_address, Some(create1(&account.address(), 0)));
952		assert_eq!(receipt.from, account.address());
953	}
954
955	#[test]
956	fn build_receipt_rejects_invalid_payload() {
957		let extractor = ReceiptExtractor::new_mock();
958
959		// Corrupt payload
960		let call = EthTransact { payload: vec![0xde, 0xad] };
961		let hash = H256(keccak_256(&call.payload));
962		let err = extractor
963			.decode_transaction_and_build_receipt(
964				H256::zero(),
965				U256::from(1),
966				call,
967				hash,
968				0,
969				gas_info(),
970				false,
971				vec![],
972			)
973			.unwrap_err();
974		assert!(matches!(err, ClientError::TxDecodingFailed));
975
976		// Valid payload but address recovery fails
977		let extractor = ReceiptExtractor {
978			recover_eth_address: Arc::new(|_| Err(())),
979			..ReceiptExtractor::new_mock()
980		};
981		let account = Account::default();
982		let (call, hash) = signed_call(&account, legacy_call_tx(account.address()));
983		let err = extractor
984			.decode_transaction_and_build_receipt(
985				H256::zero(),
986				U256::from(1),
987				call,
988				hash,
989				0,
990				gas_info(),
991				false,
992				vec![],
993			)
994			.unwrap_err();
995		assert!(matches!(err, ClientError::RecoverEthAddressFailed));
996	}
997
998	#[test]
999	fn defaults_and_first_evm_block_only_decreases() {
1000		let extractor = ReceiptExtractor::new_mock();
1001
1002		assert!(extractor.first_evm_block().is_none());
1003
1004		// first_evm_block only decreases
1005		extractor.set_first_evm_block(100);
1006		assert_eq!(extractor.first_evm_block(), Some(100));
1007
1008		extractor.set_first_evm_block(50);
1009		assert_eq!(extractor.first_evm_block(), Some(50));
1010
1011		// Higher value is ignored
1012		extractor.set_first_evm_block(100);
1013		assert_eq!(extractor.first_evm_block(), Some(50));
1014	}
1015
1016	use crate::block_info_provider::test::chain_config;
1017	use codec::{Compact, Encode};
1018	use frame_system::EventRecord;
1019	use revive_dev_runtime::{Runtime, RuntimeEvent};
1020	use subxt::{PolkadotConfig, client::OfflineClient, events::Events};
1021
1022	/// An offline client carrying the generated runtime metadata for every block.
1023	fn offline_client() -> OfflineClient<PolkadotConfig> {
1024		OfflineClient::<PolkadotConfig>::new_with_config(chain_config())
1025	}
1026
1027	/// Build `Events` by SCALE-encoding revive events against the generated runtime metadata.
1028	struct EventsBuilder {
1029		bytes: Vec<u8>,
1030		count: u32,
1031	}
1032
1033	impl EventsBuilder {
1034		fn new() -> Self {
1035			Self { bytes: Vec::new(), count: 0 }
1036		}
1037
1038		fn push_event(
1039			mut self,
1040			phase: frame_system::Phase,
1041			event: pallet_revive::Event<Runtime>,
1042		) -> Self {
1043			EventRecord::<RuntimeEvent, H256> {
1044				phase,
1045				event: RuntimeEvent::Revive(event),
1046				topics: vec![],
1047			}
1048			.encode_to(&mut self.bytes);
1049			self.count += 1;
1050			self
1051		}
1052
1053		/// The events as `System::Events` stores them.
1054		fn encode(self) -> Vec<u8> {
1055			let mut encoded_events = Vec::new();
1056			Compact(self.count).encode_to(&mut encoded_events);
1057			encoded_events.extend(self.bytes);
1058			encoded_events
1059		}
1060
1061		fn build(self) -> Events<SrcChainConfig> {
1062			let client = offline_client();
1063			let at_block =
1064				client.at_block(0u64).expect("spec version range covers all block numbers; qed");
1065			at_block.events().from_bytes(self.encode())
1066		}
1067	}
1068
1069	#[test]
1070	fn extract_revive_events_decodes_contract_emitted_log() {
1071		let contract = H160::from([0x11; 20]);
1072		let topics = vec![H256::from([0x22; 32]), H256::from([0x33; 32])];
1073		let data = vec![0xde, 0xad, 0xbe, 0xef];
1074		let events = EventsBuilder::new()
1075			.push_event(
1076				frame_system::Phase::ApplyExtrinsic(5),
1077				pallet_revive::Event::ContractEmitted {
1078					contract,
1079					data: data.clone(),
1080					topics: topics.clone(),
1081				},
1082			)
1083			.build();
1084
1085		let tx_hash = H256::from([0xAA; 32]);
1086		let eth_block_hash = H256::from([0xBB; 32]);
1087		let substrate_block_number = 42u64;
1088		let eth_block_number = U256::from(substrate_block_number);
1089
1090		let (reverts, logs, outside_frame) = extract_revive_events(
1091			&events,
1092			substrate_block_number,
1093			eth_block_number,
1094			eth_block_hash,
1095			|idx| (idx == 5).then_some(tx_hash),
1096			H256::zero(),
1097			99,
1098		);
1099
1100		assert!(reverts.is_empty());
1101		assert!(outside_frame.is_empty());
1102		assert_eq!(logs.len(), 1);
1103		let log = &logs[&5][0];
1104		assert_eq!(log.address, contract);
1105		assert_eq!(log.topics, topics);
1106		assert_eq!(log.data.as_ref().unwrap().0, data);
1107		assert_eq!(log.block_hash, eth_block_hash);
1108		assert_eq!(log.block_number, eth_block_number);
1109		assert_eq!(log.transaction_hash, tx_hash);
1110		assert_eq!(log.transaction_index, U256::from(5));
1111	}
1112
1113	#[test]
1114	fn extract_revive_events_buckets_non_eth_logs_as_outside_frame() {
1115		// A `ContractEmitted` that belongs to no ethereum transaction — because its extrinsic is
1116		// not one, or because it has no extrinsic at all — becomes an outside-of-frame log
1117		// attributed to the synthetic transaction. Reverts are meaningless in both cases and are
1118		// ignored.
1119		let empty_contract_emitted = pallet_revive::Event::ContractEmitted {
1120			contract: H160::zero(),
1121			data: vec![],
1122			topics: vec![],
1123		};
1124		let revert = pallet_revive::Event::EthExtrinsicRevert {
1125			dispatch_error: sp_runtime::DispatchError::Other("skipped-phase revert"),
1126		};
1127		let events = EventsBuilder::new()
1128			.push_event(frame_system::Phase::Finalization, empty_contract_emitted.clone())
1129			.push_event(frame_system::Phase::Initialization, revert.clone())
1130			.push_event(frame_system::Phase::ApplyExtrinsic(5), empty_contract_emitted)
1131			.push_event(frame_system::Phase::ApplyExtrinsic(5), revert)
1132			.build();
1133
1134		let synthetic_hash = H256::from([0x99; 32]);
1135		// The tx-hash closure returns `Some` only for extrinsic 7 (not present), so extrinsic 5 is
1136		// treated as non-eth.
1137		let (reverts, logs, outside_frame) = extract_revive_events(
1138			&events,
1139			0,
1140			U256::zero(),
1141			H256::zero(),
1142			|idx| (idx == 7).then_some(H256::zero()),
1143			synthetic_hash,
1144			3,
1145		);
1146
1147		assert!(reverts.is_empty());
1148		assert!(logs.is_empty());
1149		assert_eq!(outside_frame.len(), 2, "the hook-phase log and the non-eth extrinsic's log");
1150		for log in &outside_frame {
1151			assert_eq!(log.transaction_hash, synthetic_hash);
1152			assert_eq!(log.transaction_index, U256::from(3));
1153		}
1154	}
1155
1156	#[test]
1157	fn extract_revive_events_buckets_hook_phase_logs_as_outside_frame() {
1158		// A mirror firing in `on_initialize` — on Asset Hub, the message queue servicing an inbound
1159		// XCM asset deposit — is committed into the synthetic transaction like any other
1160		// outside-of-frame log, so it must be served rather than dropped for lacking an extrinsic.
1161		let synthetic_hash = H256::from([0xEE; 32]);
1162		let events = EventsBuilder::new()
1163			.push_event(
1164				frame_system::Phase::Initialization,
1165				pallet_revive::Event::ContractEmitted {
1166					contract: H160::from([0xaa; 20]),
1167					data: vec![],
1168					topics: vec![],
1169				},
1170			)
1171			.push_event(
1172				frame_system::Phase::Finalization,
1173				pallet_revive::Event::ContractEmitted {
1174					contract: H160::from([0xbb; 20]),
1175					data: vec![],
1176					topics: vec![],
1177				},
1178			)
1179			.build();
1180
1181		let (reverts, logs, outside_frame) = extract_revive_events(
1182			&events,
1183			0,
1184			U256::zero(),
1185			H256::zero(),
1186			// Every extrinsic index is an ethereum transaction, so only the phase can exclude
1187			// these.
1188			|_| Some(H256::from([0x77; 32])),
1189			synthetic_hash,
1190			4,
1191		);
1192
1193		assert!(reverts.is_empty());
1194		assert!(logs.is_empty(), "a hook-phase log belongs to no extrinsic");
1195		assert_eq!(outside_frame.len(), 2, "both hook phases are bucketed");
1196		for log in &outside_frame {
1197			assert_eq!(log.transaction_hash, synthetic_hash);
1198			assert_eq!(log.transaction_index, U256::from(4));
1199		}
1200	}
1201
1202	#[test]
1203	fn extract_revive_events_accumulates_per_extrinsic() {
1204		let tx0 = H256::from([0x01; 32]);
1205		let tx1 = H256::from([0x02; 32]);
1206		let tx2 = H256::from([0x03; 32]);
1207		let emitted_by = |contract: H160| pallet_revive::Event::ContractEmitted {
1208			contract,
1209			data: vec![],
1210			topics: vec![],
1211		};
1212		let events = EventsBuilder::new()
1213			.push_event(frame_system::Phase::ApplyExtrinsic(0), emitted_by(H160::from([0xaa; 20])))
1214			.push_event(frame_system::Phase::ApplyExtrinsic(0), emitted_by(H160::from([0xbb; 20])))
1215			.push_event(
1216				frame_system::Phase::ApplyExtrinsic(1),
1217				pallet_revive::Event::EthExtrinsicRevert {
1218					dispatch_error: sp_runtime::DispatchError::Other("tx-1 revert"),
1219				},
1220			)
1221			.push_event(frame_system::Phase::ApplyExtrinsic(2), emitted_by(H160::from([0xcc; 20])))
1222			.build();
1223
1224		let (reverts, logs, outside_frame) = extract_revive_events(
1225			&events,
1226			0,
1227			U256::zero(),
1228			H256::zero(),
1229			|idx| match idx {
1230				0 => Some(tx0),
1231				1 => Some(tx1),
1232				2 => Some(tx2),
1233				_ => None,
1234			},
1235			H256::zero(),
1236			3,
1237		);
1238
1239		assert!(outside_frame.is_empty());
1240		assert_eq!(reverts, [1usize].into_iter().collect::<HashSet<_>>());
1241		assert_eq!(logs[&0].len(), 2);
1242		assert_eq!(logs[&2].len(), 1);
1243		// log_index is block-wide
1244		assert_eq!(logs[&0][0].log_index, U256::from(0));
1245		assert_eq!(logs[&0][1].log_index, U256::from(1));
1246		assert_eq!(logs[&2][0].log_index, U256::from(3));
1247	}
1248
1249	const ETH_TRANSACT_PAYLOAD: [u8; 4] = [0xde, 0xad, 0xbe, 0xef];
1250
1251	/// SCALE-encode a bare extrinsic the way a block body carries it, length prefix included.
1252	fn encode_bare(call: revive_dev_runtime::RuntimeCall) -> Vec<u8> {
1253		let extrinsic: revive_dev_runtime::UncheckedExtrinsic =
1254			pallet_revive::evm::runtime::UncheckedExtrinsic(
1255				sp_runtime::generic::UncheckedExtrinsic::new_bare(call),
1256			);
1257		extrinsic.encode()
1258	}
1259
1260	fn eth_transact_extrinsic() -> Vec<u8> {
1261		encode_bare(revive_dev_runtime::RuntimeCall::Revive(pallet_revive::Call::eth_transact {
1262			payload: ETH_TRANSACT_PAYLOAD.to_vec(),
1263		}))
1264	}
1265
1266	fn non_revive_extrinsic() -> Vec<u8> {
1267		encode_bare(revive_dev_runtime::RuntimeCall::System(frame_system::Call::remark {
1268			remark: vec![0x01],
1269		}))
1270	}
1271
1272	/// Run the extraction over a synthetic block body.
1273	async fn extract_from(blobs: Vec<Vec<u8>>) -> Result<Vec<(EthTransact, usize)>, ClientError> {
1274		const BLOCK_NUMBER: SubstrateBlockNumber = 42;
1275
1276		let client = offline_client();
1277		let at_block = client
1278			.at_block(BLOCK_NUMBER)
1279			.expect("spec version range covers every block number; qed");
1280		let extrinsics = at_block.extrinsics().from_bytes(blobs).await;
1281		extract_eth_transacts(&extrinsics, BLOCK_NUMBER)
1282	}
1283
1284	#[tokio::test]
1285	async fn synthetic_tx_index_is_one_past_the_last_extrinsic() {
1286		// A native extrinsic after the last ethereum transaction sits at `max eth index + 1`.
1287		// Served there, the synthetic transaction's hash would trace and weigh that extrinsic.
1288		let blobs = vec![eth_transact_extrinsic(), non_revive_extrinsic(), vec![0xff; 4]];
1289
1290		let client = offline_client();
1291		let at_block = client
1292			.at_block(42u64)
1293			.expect("spec version range covers every block number; qed");
1294		let extrinsics = at_block.extrinsics().from_bytes(blobs).await;
1295
1296		let eth = extract_eth_transacts(&extrinsics, 42).unwrap();
1297		assert_eq!(eth.iter().map(|(_, idx)| *idx).max(), Some(0));
1298		assert_eq!(synthetic_tx_index(&extrinsics), 3, "past the undecodable extrinsic too");
1299	}
1300
1301	#[tokio::test]
1302	async fn extract_eth_transacts_collects_revive_calls() {
1303		let calls = extract_from(vec![non_revive_extrinsic(), eth_transact_extrinsic()])
1304			.await
1305			.unwrap();
1306
1307		assert_eq!(calls.len(), 1, "only the revive extrinsic is collected");
1308		assert_eq!(calls[0].1, 1, "the extrinsic index is preserved");
1309		assert_eq!(calls[0].0.payload, ETH_TRANSACT_PAYLOAD, "the call fields are decoded");
1310	}
1311
1312	#[tokio::test]
1313	async fn extract_eth_transacts_keeps_revive_calls_next_to_an_undecodable_one() {
1314		let calls = extract_from(vec![vec![0xff; 4], eth_transact_extrinsic()]).await.unwrap();
1315
1316		assert_eq!(calls.len(), 1, "an undecodable extrinsic must not hide a decoded one");
1317		assert_eq!(calls[0].1, 1, "the extrinsic index is preserved");
1318		assert_eq!(calls[0].0.payload, ETH_TRANSACT_PAYLOAD, "the call fields are decoded");
1319	}
1320
1321	/// `n` distinguishable gas entries, so a mispairing shows up as a wrong value.
1322	fn gas_infos(n: usize) -> Vec<ReceiptGasInfoV1> {
1323		(0..n)
1324			.map(|i| ReceiptGasInfoV1 {
1325				gas_used: U256::from(i),
1326				effective_gas_price: U256::from(1_000_000_000u64),
1327			})
1328			.collect()
1329	}
1330
1331	#[test]
1332	fn receipt_data_is_reconciled_against_the_block_body() {
1333		// The synthetic transaction is reported separately, so a block carrying one still has
1334		// exactly one entry per ethereum transaction here.
1335		check_receipt_data_len(&gas_infos(2), 2).unwrap();
1336
1337		// An extrinsic the metadata could not decode leaves an entry with no transaction to pair
1338		// it with. Accepting it would pair every later transaction with the preceding one's gas
1339		// info.
1340		let err = check_receipt_data_len(&gas_infos(3), 2).unwrap_err();
1341		assert!(matches!(err, ClientError::ReceiptDataLengthMismatch));
1342	}
1343
1344	/// One outside-frame log per event index in `event_indices`, distinguishable by address.
1345	fn outside_frame_logs(event_indices: &[u64]) -> Vec<Log> {
1346		event_indices
1347			.iter()
1348			.map(|i| Log {
1349				address: H160::from_low_u64_be(*i),
1350				log_index: U256::from(*i),
1351				..Default::default()
1352			})
1353			.collect()
1354	}
1355
1356	/// The logs of `outside_frame_logs(event_indices)` as the synthetic receipt serves them:
1357	/// numbered consecutively from `first_log_index`.
1358	fn served_logs(event_indices: &[u64], first_log_index: u64) -> Vec<Log> {
1359		outside_frame_logs(event_indices)
1360			.into_iter()
1361			.zip(first_log_index..)
1362			.map(|(log, log_index)| Log { log_index: U256::from(log_index), ..log })
1363			.collect()
1364	}
1365
1366	#[test]
1367	fn outside_frame_logs_are_served_as_emitted_when_they_all_fitted() {
1368		let logs =
1369			select_committed_outside_frame_logs(outside_frame_logs(&[0, 1, 2]), &[0, 1, 2], 1, 3);
1370
1371		assert_eq!(logs, served_logs(&[0, 1, 2], 3));
1372	}
1373
1374	#[test]
1375	fn outside_frame_logs_the_block_did_not_commit_are_not_served() {
1376		// A contract log outside an ethereum transaction, or a log past the buffer's cap, is an
1377		// event the block never committed. Serving it would hand out a log absent from the block's
1378		// `logs_bloom` and `receipts_root`.
1379		let logs = select_committed_outside_frame_logs(
1380			outside_frame_logs(&[0, 1, 2, 3, 4]),
1381			&[1, 3],
1382			1,
1383			5,
1384		);
1385
1386		assert_eq!(logs, served_logs(&[1, 3], 5));
1387	}
1388
1389	#[test]
1390	fn a_committed_log_that_did_not_decode_is_left_out() {
1391		// The block committed the log at event 4 but nothing decoded there: the warning reports it
1392		// and the receipt is served without it, in the runtime's order.
1393		let logs =
1394			select_committed_outside_frame_logs(outside_frame_logs(&[0, 6]), &[0, 4, 6], 1, 7);
1395
1396		assert_eq!(logs, served_logs(&[0, 6], 7));
1397	}
1398
1399	#[test]
1400	fn synthetic_logs_are_numbered_after_the_blocks_other_logs() {
1401		// An `on_initialize` mirror sits at event 0, below every log of the block's ethereum
1402		// transactions, yet it is served by the block's last transaction. Numbering it by its
1403		// event index would put it ahead of those logs in `eth_getLogs`, which orders by
1404		// `log_index`; numbering starts at the event count instead.
1405		let logs = select_committed_outside_frame_logs(outside_frame_logs(&[0, 9]), &[0, 9], 1, 10);
1406
1407		assert_eq!(logs, served_logs(&[0, 9], 10));
1408	}
1409
1410	#[test]
1411	fn synthetic_receipt_serves_the_log_the_block_committed() {
1412		// Extrinsic 0 is a `Revive::call` that instantiates a contract, which emits a log. The
1413		// runtime leaves a frame log outside an ethereum transaction substrate-only
1414		// (`block_storage::capture_frame_log`). Extrinsic 1 is an assets transfer whose mirrored
1415		// `Transfer` is buffered, so the block commits exactly one log, the mirror's at event 2.
1416		// Both logs arrive here as a `ContractEmitted` under a non-eth extrinsic, and the receipt
1417		// must carry the committed one.
1418		//
1419		// The `Instantiated` ahead of them keeps event indices apart from log positions: the
1420		// runtime reports the former, so a selection that keyed on the latter would find nothing.
1421		let contract = H160::from([0xc0; 20]);
1422		let asset_precompile = H160::from([0xa5; 20]);
1423		let emitted_by = |address: H160| pallet_revive::Event::ContractEmitted {
1424			contract: address,
1425			data: vec![],
1426			topics: vec![],
1427		};
1428		let events = EventsBuilder::new()
1429			.push_event(
1430				frame_system::Phase::ApplyExtrinsic(0),
1431				pallet_revive::Event::Instantiated { deployer: H160::zero(), contract },
1432			)
1433			.push_event(frame_system::Phase::ApplyExtrinsic(0), emitted_by(contract))
1434			.push_event(frame_system::Phase::ApplyExtrinsic(1), emitted_by(asset_precompile))
1435			.build();
1436
1437		let (_, _, outside_frame) = extract_revive_events(
1438			&events,
1439			1,
1440			U256::one(),
1441			H256::zero(),
1442			|_| None,
1443			H256::from([0x99; 32]),
1444			2,
1445		);
1446		let logs = select_committed_outside_frame_logs(outside_frame, &[2], 1, 3);
1447
1448		assert_eq!(logs.len(), 1, "the one committed log is served");
1449		assert_eq!(
1450			logs[0].address, asset_precompile,
1451			"the synthetic receipt must carry the committed mirror log, not the contract's"
1452		);
1453	}
1454
1455	use subxt::{
1456		OnlineClient,
1457		backend::LegacyBackend,
1458		config::substrate::SubstrateHeader,
1459		rpcs::{
1460			RpcClient,
1461			client::{MockRpcClient, mock_rpc_client::Json},
1462		},
1463	};
1464
1465	/// A chain of one block, served over a mocked RPC: its header, its body and its
1466	/// `System::Events`.
1467	async fn mocked_block(extrinsics: Vec<Vec<u8>>, events: Vec<u8>) -> SubstrateBlock {
1468		type Params = Option<Box<serde_json::value::RawValue>>;
1469		let block_hash = H256::repeat_byte(0x2a);
1470		let header = SubstrateHeader::<H256> {
1471			parent_hash: H256::zero(),
1472			number: 42,
1473			state_root: H256::zero(),
1474			extrinsics_root: H256::zero(),
1475			digest: Default::default(),
1476		};
1477		let hex = |bytes: &[u8]| format!("0x{}", hex::encode(bytes));
1478		let body = serde_json::json!({
1479			"block": {
1480				"header": header,
1481				"extrinsics": extrinsics.iter().map(|extrinsic| hex(extrinsic)).collect::<Vec<_>>(),
1482			},
1483			"justifications": null,
1484		});
1485		let events = hex(&events);
1486
1487		let mock =
1488			MockRpcClient::builder()
1489				.method_handler("chain_getBlockHash", move |_: Params| async move {
1490					Json(Some(block_hash))
1491				})
1492				.method_handler("chain_getFinalizedHead", move |_: Params| async move {
1493					Json(block_hash)
1494				})
1495				.method_handler("chain_getHeader", move |_: Params| {
1496					let header = header.clone();
1497					async move { Json(Some(header)) }
1498				})
1499				.method_handler("chain_getBlock", move |_: Params| {
1500					let body = body.clone();
1501					async move { Json(Some(body)) }
1502				})
1503				.method_handler("state_getStorage", move |_: Params| {
1504					let events = events.clone();
1505					async move { Json(Some(events)) }
1506				})
1507				.build();
1508		let backend = LegacyBackend::<SrcChainConfig>::builder().build(RpcClient::new(mock));
1509		let api = OnlineClient::<SrcChainConfig>::from_backend_with_config(
1510			chain_config(),
1511			Arc::new(backend),
1512		)
1513		.await
1514		.unwrap();
1515		api.at_block(block_hash).await.unwrap()
1516	}
1517
1518	#[tokio::test]
1519	async fn synthetic_receipt_is_served_alike_on_both_paths_with_its_logs_numbered_last() {
1520		// Event 0: a mirror from `on_initialize`, committed. Event 1: the ethereum transaction's
1521		// own log. Event 2: a contract log outside any ethereum transaction, deposited but not
1522		// committed. Event 3: a mirror from the plain extrinsic, committed.
1523		let account = Account::default();
1524		let (call, tx_hash) = signed_call(&account, legacy_call_tx(account.address()));
1525		let extrinsics = vec![
1526			encode_bare(revive_dev_runtime::RuntimeCall::Revive(
1527				pallet_revive::Call::eth_transact { payload: call.payload },
1528			)),
1529			non_revive_extrinsic(),
1530		];
1531		let hook_mirror = H160::from([0xa1; 20]);
1532		let contract = H160::from([0xc0; 20]);
1533		let uncommitted = H160::from([0xb1; 20]);
1534		let extrinsic_mirror = H160::from([0xb2; 20]);
1535		let emitted_by = |address: H160| pallet_revive::Event::ContractEmitted {
1536			contract: address,
1537			data: vec![],
1538			topics: vec![],
1539		};
1540		let events = EventsBuilder::new()
1541			.push_event(frame_system::Phase::Initialization, emitted_by(hook_mirror))
1542			.push_event(frame_system::Phase::ApplyExtrinsic(0), emitted_by(contract))
1543			.push_event(frame_system::Phase::ApplyExtrinsic(1), emitted_by(uncommitted))
1544			.push_event(frame_system::Phase::ApplyExtrinsic(1), emitted_by(extrinsic_mirror))
1545			.encode();
1546		let block = mocked_block(extrinsics, events).await;
1547
1548		let extractor = ReceiptExtractor {
1549			fetch_receipt_data: Arc::new(|_| {
1550				Box::pin(std::future::ready(ReceiptData::Available {
1551					receipt_data: vec![gas_info()],
1552					synthetic: Some(SyntheticTransactionV1 {
1553						gas_info: gas_info(),
1554						log_event_indices: vec![0, 3],
1555					}),
1556				})) as Pin<Box<_>>
1557			}),
1558			..ReceiptExtractor::new_mock()
1559		};
1560		let eth_block_hash = extractor
1561			.get_ethereum_block_hash(&block.block_hash(), block.block_number())
1562			.await
1563			.unwrap();
1564
1565		let receipts = extractor
1566			.extract_from_block_with_eth_hash(&block, eth_block_hash)
1567			.await
1568			.unwrap();
1569		let [(_, eth_receipt), (_, synthetic_receipt)] = receipts.as_slice() else {
1570			panic!("one ethereum transaction and the synthetic one, got {}", receipts.len());
1571		};
1572		assert_eq!(eth_receipt.transaction_hash, tx_hash);
1573		assert_eq!(eth_receipt.transaction_index, U256::zero());
1574		assert_eq!(eth_receipt.logs.len(), 1);
1575		assert_eq!(synthetic_receipt.transaction_index, U256::from(2));
1576		assert_eq!(
1577			synthetic_receipt.logs.iter().map(|log| log.address).collect::<Vec<_>>(),
1578			vec![hook_mirror, extrinsic_mirror],
1579			"the committed logs, in the runtime's order"
1580		);
1581		assert_eq!(
1582			synthetic_receipt.logs.iter().map(|log| log.log_index).collect::<Vec<_>>(),
1583			vec![U256::from(4), U256::from(5)],
1584			"numbered from the block's event count, not by event index"
1585		);
1586		assert!(
1587			eth_receipt.logs[0].log_index < synthetic_receipt.logs[0].log_index,
1588			"log_index follows transaction order"
1589		);
1590
1591		let (_, single) = extractor.extract_from_transaction(&block, 2).await.unwrap();
1592		assert_eq!(&single, synthetic_receipt, "the single-receipt path serves the same receipt");
1593	}
1594
1595	/// An extractor whose receipt data query always yields what `receipt_data` builds.
1596	fn extractor_answering(receipt_data: fn() -> ReceiptData) -> ReceiptExtractor {
1597		ReceiptExtractor {
1598			fetch_receipt_data: Arc::new(move |_| {
1599				Box::pin(std::future::ready(receipt_data())) as Pin<Box<_>>
1600			}),
1601			..ReceiptExtractor::new_mock()
1602		}
1603	}
1604
1605	#[tokio::test]
1606	async fn a_failed_receipt_data_query_is_an_error_on_both_paths() {
1607		// The query runs for every block, since one with no ethereum transaction can still carry
1608		// a synthetic transaction. A failed query must not read as an empty block: indexed that
1609		// way, a block whose header commits to a synthetic transaction would be stored without
1610		// its receipt.
1611		let block = mocked_block(vec![non_revive_extrinsic()], EventsBuilder::new().encode()).await;
1612		let extractor = extractor_answering(|| ReceiptData::Failed);
1613
1614		let err = extractor
1615			.extract_from_block_with_eth_hash(&block, H256::zero())
1616			.await
1617			.unwrap_err();
1618		assert!(matches!(err, ClientError::ReceiptDataNotFound), "{err:?}");
1619		let err = extractor.extract_from_transaction(&block, 0).await.unwrap_err();
1620		assert!(matches!(err, ClientError::ReceiptDataNotFound), "{err:?}");
1621	}
1622
1623	#[tokio::test]
1624	async fn a_block_before_the_receipt_data_api_is_an_empty_block_on_both_paths() {
1625		// A runtime without the API has neither ethereum transactions nor mirrored logs, so the
1626		// block reconstructs as empty rather than failing every pre-EVM request.
1627		let block = mocked_block(vec![non_revive_extrinsic()], EventsBuilder::new().encode()).await;
1628		let extractor = extractor_answering(|| ReceiptData::Unsupported);
1629
1630		let receipts =
1631			extractor.extract_from_block_with_eth_hash(&block, H256::zero()).await.unwrap();
1632		assert!(receipts.is_empty());
1633		let err = extractor.extract_from_transaction(&block, 0).await.unwrap_err();
1634		assert!(matches!(err, ClientError::EthExtrinsicNotFound), "{err:?}");
1635	}
1636}