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	client::{runtime_api::RuntimeApi, SubstrateBlock, SubstrateBlockNumber},
19	subxt_client::{
20		revive::{
21			calls::types::EthTransact,
22			events::{ContractEmitted, EthExtrinsicRevert},
23		},
24		SrcChainConfig,
25	},
26	ClientError, H160, LOG_TARGET,
27};
28
29use futures::{stream, StreamExt};
30use pallet_revive::{
31	create1,
32	evm::{GenericTransaction, Log, ReceiptGasInfo, ReceiptInfo, TransactionSigned, H256, U256},
33};
34use sp_core::keccak_256;
35use std::{future::Future, pin::Pin, sync::Arc};
36use subxt::{blocks::ExtrinsicDetails, OnlineClient};
37
38type FetchReceiptDataFn = Arc<
39	dyn Fn(H256) -> Pin<Box<dyn Future<Output = Option<Vec<ReceiptGasInfo>>> + Send>> + Send + Sync,
40>;
41
42type FetchEthBlockHashFn =
43	Arc<dyn Fn(H256, u64) -> Pin<Box<dyn Future<Output = Option<H256>> + Send>> + Send + Sync>;
44
45type RecoverEthAddressFn = Arc<dyn Fn(&TransactionSigned) -> Result<H160, ()> + Send + Sync>;
46
47/// Utility to extract receipts from extrinsics.
48#[derive(Clone)]
49pub struct ReceiptExtractor {
50	/// Fetch the receipt data info.
51	fetch_receipt_data: FetchReceiptDataFn,
52
53	/// Fetch ethereum block hash.
54	fetch_eth_block_hash: FetchEthBlockHashFn,
55
56	/// Earliest block number to consider when searching for transaction receipts.
57	earliest_receipt_block: Option<SubstrateBlockNumber>,
58
59	/// Recover the ethereum address from a transaction signature.
60	recover_eth_address: RecoverEthAddressFn,
61}
62
63impl ReceiptExtractor {
64	/// Check if the block is before the earliest block.
65	pub fn is_before_earliest_block(&self, block_number: SubstrateBlockNumber) -> bool {
66		block_number < self.earliest_receipt_block.unwrap_or_default()
67	}
68
69	/// Create a new `ReceiptExtractor` with the given native to eth ratio.
70	pub async fn new(
71		api: OnlineClient<SrcChainConfig>,
72		earliest_receipt_block: Option<SubstrateBlockNumber>,
73	) -> Result<Self, ClientError> {
74		Self::new_with_custom_address_recovery(
75			api,
76			earliest_receipt_block,
77			Arc::new(|signed_tx: &TransactionSigned| signed_tx.recover_eth_address()),
78		)
79		.await
80	}
81
82	/// Create a new `ReceiptExtractor` with the given native to eth ratio.
83	///
84	/// Specify also a custom Ethereum address recovery logic.
85	/// Use `ReceiptExtractor::new` if the default Ethereum address recovery
86	/// logic ([`TransactionSigned::recover_eth_address`] based) is enough.
87	pub async fn new_with_custom_address_recovery(
88		api: OnlineClient<SrcChainConfig>,
89		earliest_receipt_block: Option<SubstrateBlockNumber>,
90		recover_eth_address_fn: RecoverEthAddressFn,
91	) -> Result<Self, ClientError> {
92		let api_inner = api.clone();
93		let fetch_eth_block_hash = Arc::new(move |block_hash, block_number| {
94			let api_inner = api_inner.clone();
95
96			let fut = async move {
97				let runtime_api = RuntimeApi::new(api_inner.runtime_api().at(block_hash));
98				runtime_api.eth_block_hash(U256::from(block_number)).await.ok().flatten()
99			};
100
101			Box::pin(fut) as Pin<Box<_>>
102		});
103
104		let api_inner = api.clone();
105		let fetch_receipt_data = Arc::new(move |block_hash| {
106			let api_inner = api_inner.clone();
107
108			let fut = async move {
109				let runtime_api = RuntimeApi::new(api_inner.runtime_api().at(block_hash));
110				runtime_api.eth_receipt_data().await.ok()
111			};
112
113			Box::pin(fut) as Pin<Box<_>>
114		});
115
116		Ok(Self {
117			fetch_receipt_data,
118			fetch_eth_block_hash,
119			earliest_receipt_block,
120			recover_eth_address: recover_eth_address_fn,
121		})
122	}
123
124	#[cfg(test)]
125	pub fn new_mock() -> Self {
126		let fetch_receipt_data = Arc::new(|_| Box::pin(std::future::ready(None)) as Pin<Box<_>>);
127		// This method is useful when testing eth - substrate mapping.
128		let fetch_eth_block_hash = Arc::new(|block_hash: H256, block_number: u64| {
129			// Generate hash from substrate block hash and number
130			let bytes: Vec<u8> = [block_hash.as_bytes(), &block_number.to_be_bytes()].concat();
131			let eth_block_hash = H256::from(keccak_256(&bytes));
132			Box::pin(std::future::ready(Some(eth_block_hash))) as Pin<Box<_>>
133		});
134
135		Self {
136			fetch_receipt_data,
137			fetch_eth_block_hash,
138			earliest_receipt_block: None,
139			recover_eth_address: Arc::new(|signed_tx: &TransactionSigned| {
140				signed_tx.recover_eth_address()
141			}),
142		}
143	}
144
145	/// Extract a [`TransactionSigned`] and a [`ReceiptInfo`] from an extrinsic.
146	async fn extract_from_extrinsic(
147		&self,
148		substrate_block: &SubstrateBlock,
149		eth_block_hash: H256,
150		ext: subxt::blocks::ExtrinsicDetails<SrcChainConfig, subxt::OnlineClient<SrcChainConfig>>,
151		call: EthTransact,
152		receipt_gas_info: ReceiptGasInfo,
153		transaction_index: usize,
154	) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
155		let events = ext.events().await?;
156		let block_number: U256 = substrate_block.number().into();
157
158		let success = !events.has::<EthExtrinsicRevert>().inspect_err(|err| {
159			log::debug!(
160				target: LOG_TARGET,
161				"Failed to lookup for EthExtrinsicRevert event in block {block_number}: {err:?}"
162			);
163		})?;
164
165		let transaction_hash = H256(keccak_256(&call.payload));
166
167		let signed_tx =
168			TransactionSigned::decode(&call.payload).map_err(|_| ClientError::TxDecodingFailed)?;
169		let from = (self.recover_eth_address)(&signed_tx).map_err(|_| {
170			log::error!(target: LOG_TARGET, "Failed to recover eth address from signed tx");
171			ClientError::RecoverEthAddressFailed
172		})?;
173
174		let tx_info = GenericTransaction::from_signed(
175			signed_tx.clone(),
176			receipt_gas_info.effective_gas_price,
177			Some(from),
178		);
179
180		// get logs from ContractEmitted event
181		let logs = events
182			.iter()
183			.filter_map(|event_details| {
184				let event_details = event_details.ok()?;
185				let event = event_details.as_event::<ContractEmitted>().ok()??;
186
187				Some(Log {
188					address: event.contract,
189					topics: event.topics,
190					data: Some(event.data.into()),
191					block_number,
192					transaction_hash,
193					transaction_index: transaction_index.into(),
194					block_hash: eth_block_hash,
195					log_index: event_details.index().into(),
196					..Default::default()
197				})
198			})
199			.collect();
200
201		let contract_address = if tx_info.to.is_none() {
202			Some(create1(
203				&from,
204				tx_info
205					.nonce
206					.unwrap_or_default()
207					.try_into()
208					.map_err(|_| ClientError::ConversionFailed)?,
209			))
210		} else {
211			None
212		};
213
214		let receipt = ReceiptInfo::new(
215			eth_block_hash,
216			block_number,
217			contract_address,
218			from,
219			logs,
220			tx_info.to,
221			receipt_gas_info.effective_gas_price,
222			U256::from(receipt_gas_info.gas_used),
223			success,
224			transaction_hash,
225			transaction_index.into(),
226			tx_info.r#type.unwrap_or_default(),
227		);
228		Ok((signed_tx, receipt))
229	}
230
231	/// Extract receipts from block.
232	pub async fn extract_from_block(
233		&self,
234		block: &SubstrateBlock,
235	) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
236		if self.is_before_earliest_block(block.number()) {
237			return Ok(vec![]);
238		}
239
240		let ext_iter = self.get_block_extrinsics(block).await?;
241
242		let substrate_block_number = block.number() as u64;
243		let substrate_block_hash = block.hash();
244		let eth_block_hash =
245			(self.fetch_eth_block_hash)(substrate_block_hash, substrate_block_number)
246				.await
247				.unwrap_or(substrate_block_hash);
248
249		// Process extrinsics in order while maintaining parallelism within buffer window
250		stream::iter(ext_iter)
251			.map(|(ext, call, receipt, ext_idx)| async move {
252				self.extract_from_extrinsic(block, eth_block_hash, ext, call, receipt, ext_idx)
253					.await
254					.inspect_err(|err| {
255						log::warn!(target: LOG_TARGET, "Error extracting extrinsic: {err:?}");
256					})
257			})
258			.buffered(10)
259			.collect::<Vec<Result<_, _>>>()
260			.await
261			.into_iter()
262			.collect::<Result<Vec<_>, _>>()
263	}
264
265	/// Return the ETH extrinsics of the block grouped with reconstruction receipt info and
266	/// extrinsic index
267	pub async fn get_block_extrinsics(
268		&self,
269		block: &SubstrateBlock,
270	) -> Result<
271		impl Iterator<
272			Item = (
273				ExtrinsicDetails<SrcChainConfig, OnlineClient<SrcChainConfig>>,
274				EthTransact,
275				ReceiptGasInfo,
276				usize,
277			),
278		>,
279		ClientError,
280	> {
281		// Filter extrinsics from pallet_revive
282		let extrinsics = block.extrinsics().await.inspect_err(|err| {
283			log::debug!(target: LOG_TARGET, "Error fetching for #{:?} extrinsics: {err:?}", block.number());
284		})?;
285
286		let receipt_data = (self.fetch_receipt_data)(block.hash())
287			.await
288			.ok_or(ClientError::ReceiptDataNotFound)?;
289		let extrinsics: Vec<_> = extrinsics
290			.iter()
291			.enumerate()
292			.flat_map(|(ext_idx, ext)| {
293				let call = ext.as_extrinsic::<EthTransact>().ok()??;
294				Some((ext, call, ext_idx))
295			})
296			.collect();
297
298		// Sanity check we received enough data from the pallet revive.
299		if receipt_data.len() != extrinsics.len() {
300			log::error!(
301				target: LOG_TARGET,
302				"Receipt data length ({}) does not match extrinsics length ({})",
303				receipt_data.len(),
304				extrinsics.len()
305			);
306			Err(ClientError::ReceiptDataLengthMismatch)
307		} else {
308			Ok(extrinsics
309				.into_iter()
310				.zip(receipt_data)
311				.map(|((extr, call, ext_idx), rec)| (extr, call, rec, ext_idx)))
312		}
313	}
314
315	/// Extract a [`TransactionSigned`] and a [`ReceiptInfo`] for a specific transaction in a
316	/// [`SubstrateBlock`]
317	pub async fn extract_from_transaction(
318		&self,
319		block: &SubstrateBlock,
320		transaction_index: usize,
321	) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
322		let ext_iter = self.get_block_extrinsics(block).await?;
323
324		let (ext, eth_call, receipt_gas_info, _) = ext_iter
325			.into_iter()
326			.find(|(_, _, _, ext_idx)| *ext_idx == transaction_index)
327			.ok_or(ClientError::EthExtrinsicNotFound)?;
328
329		let substrate_block_number = block.number() as u64;
330		let substrate_block_hash = block.hash();
331		let eth_block_hash =
332			(self.fetch_eth_block_hash)(substrate_block_hash, substrate_block_number)
333				.await
334				.unwrap_or(substrate_block_hash);
335
336		self.extract_from_extrinsic(
337			block,
338			eth_block_hash,
339			ext,
340			eth_call,
341			receipt_gas_info,
342			transaction_index,
343		)
344		.await
345	}
346
347	/// Get the Ethereum block hash for the Substrate block with specific hash.
348	pub async fn get_ethereum_block_hash(
349		&self,
350		block_hash: &H256,
351		block_number: u64,
352	) -> Option<H256> {
353		(self.fetch_eth_block_hash)(*block_hash, block_number).await
354	}
355}