From df8b163105fe8bdb89eb87af211ced8087437f14 Mon Sep 17 00:00:00 2001 From: Bear Wang Date: Wed, 23 Aug 2023 17:13:06 +0800 Subject: [PATCH] Add `eth_getBlockReceipts` (#1156) * Add rpc interface * Impl the rpc * Add tests * Update the test * Revert test config in dev process * Revert lock.json changes * Get BlockInfo in one place * Move instance code outside * Rename * Update `substrate_hash` type * Use reference * Self review * Use derive Default * Better `transaction_receipt` * Fix review --- client/rpc-core/src/eth.rs | 7 + client/rpc/src/eth/block.rs | 107 ++++-------- client/rpc/src/eth/mod.rs | 165 +++++++++++++++++- client/rpc/src/eth/transaction.rs | 277 ++++++++++-------------------- template/node/src/rpc/eth.rs | 2 +- ts-tests/tests/test-block.ts | 70 +++++++- 6 files changed, 367 insertions(+), 261 deletions(-) diff --git a/client/rpc-core/src/eth.rs b/client/rpc-core/src/eth.rs index bea3943d01..118fda550b 100644 --- a/client/rpc-core/src/eth.rs +++ b/client/rpc-core/src/eth.rs @@ -85,6 +85,13 @@ pub trait EthApi { number: BlockNumber, ) -> RpcResult>; + /// Returns the receipts of a block by number or hash. + #[method(name = "eth_getBlockReceipts")] + async fn block_transaction_receipts( + &self, + number: BlockNumber, + ) -> RpcResult>>; + /// Returns the number of uncles in a block with given hash. #[method(name = "eth_getUncleCountByBlockHash")] fn block_uncles_count_by_hash(&self, hash: H256) -> RpcResult; diff --git a/client/rpc/src/eth/block.rs b/client/rpc/src/eth/block.rs index c9a1f88017..90939bc26c 100644 --- a/client/rpc/src/eth/block.rs +++ b/client/rpc/src/eth/block.rs @@ -33,7 +33,7 @@ use fc_rpc_core::types::*; use fp_rpc::EthereumRuntimeRPCApi; use crate::{ - eth::{rich_block_build, Eth, EthConfig}, + eth::{rich_block_build, BlockInfo, Eth, EthConfig}, frontier_backend_client, internal_err, }; @@ -43,34 +43,17 @@ where C: ProvideRuntimeApi, C::Api: EthereumRuntimeRPCApi, C: HeaderBackend + StorageProvider + 'static, - BE: Backend, + BE: Backend + 'static, A: ChainApi + 'static, { pub async fn block_by_hash(&self, hash: H256, full: bool) -> RpcResult> { - let client = Arc::clone(&self.client); - let block_data_cache = Arc::clone(&self.block_data_cache); - let backend = Arc::clone(&self.backend); - - let substrate_hash = match frontier_backend_client::load_hash::( - client.as_ref(), - backend.as_ref(), - hash, - ) - .await - .map_err(|err| internal_err(format!("{:?}", err)))? - { - Some(hash) => hash, - _ => return Ok(None), - }; - - let schema = fc_storage::onchain_storage_schema(client.as_ref(), substrate_hash); - - let block = block_data_cache.current_block(schema, substrate_hash).await; - let statuses = block_data_cache - .current_transaction_statuses(schema, substrate_hash) - .await; - - let base_fee = client.runtime_api().gas_price(substrate_hash).ok(); + let BlockInfo { + block, + statuses, + substrate_hash, + base_fee, + .. + } = self.block_info_by_eth_block_hash(hash).await?; match (block, statuses) { (Some(block), Some(statuses)) => { @@ -79,7 +62,7 @@ where statuses.into_iter().map(Option::Some).collect(), Some(hash), full, - base_fee, + Some(base_fee), false, ); @@ -203,26 +186,8 @@ where } pub async fn block_transaction_count_by_hash(&self, hash: H256) -> RpcResult> { - let substrate_hash = match frontier_backend_client::load_hash::( - self.client.as_ref(), - self.backend.as_ref(), - hash, - ) - .await - .map_err(|err| internal_err(format!("{:?}", err)))? - { - Some(hash) => hash, - _ => return Ok(None), - }; - let schema = fc_storage::onchain_storage_schema(self.client.as_ref(), substrate_hash); - let block = self - .overrides - .schemas - .get(&schema) - .unwrap_or(&self.overrides.fallback) - .current_block(substrate_hash); - - match block { + let blockinfo = self.block_info_by_eth_block_hash(hash).await?; + match blockinfo.block { Some(block) => Ok(Some(U256::from(block.transactions.len()))), None => Ok(None), } @@ -239,34 +204,36 @@ where ))); } - let id = match frontier_backend_client::native_block_id::( - self.client.as_ref(), - self.backend.as_ref(), - Some(number), - ) - .await? - { - Some(id) => id, - None => return Ok(None), - }; - let substrate_hash = self - .client - .expect_block_hash_from_id(&id) - .map_err(|_| internal_err(format!("Expect block number from id: {}", id)))?; - let schema = fc_storage::onchain_storage_schema(self.client.as_ref(), substrate_hash); - let block = self - .overrides - .schemas - .get(&schema) - .unwrap_or(&self.overrides.fallback) - .current_block(substrate_hash); - - match block { + let block_info = self.block_info_by_number(number).await?; + match block_info.block { Some(block) => Ok(Some(U256::from(block.transactions.len()))), None => Ok(None), } } + pub async fn block_transaction_receipts( + &self, + number: BlockNumber, + ) -> RpcResult>> { + let block_info = self.block_info_by_number(number).await?; + let Some(statuses) = block_info.clone().statuses else { + return Ok(None); + }; + + let mut receipts = Vec::new(); + let transactions: Vec<(H256, usize)> = statuses + .iter() + .map(|tx| (tx.transaction_hash, tx.transaction_index as usize)) + .collect(); + for (hash, index) in transactions { + if let Some(receipt) = self.transaction_receipt(&block_info, hash, index).await? { + receipts.push(receipt); + } + } + + Ok(Some(receipts)) + } + pub fn block_uncles_count_by_hash(&self, _: H256) -> RpcResult { Ok(U256::zero()) } diff --git a/client/rpc/src/eth/mod.rs b/client/rpc/src/eth/mod.rs index 646924bb9b..14b2d896e3 100644 --- a/client/rpc/src/eth/mod.rs +++ b/client/rpc/src/eth/mod.rs @@ -51,7 +51,7 @@ use fp_rpc::{ RuntimeStorageOverride, TransactionStatus, }; -use crate::{internal_err, public_key, signer::EthSigner}; +use crate::{frontier_backend_client, internal_err, public_key, signer::EthSigner}; pub use self::{ cache::{EthBlockDataCacheTask, EthTask}, @@ -91,7 +91,16 @@ pub struct Eth> { _marker: PhantomData<(B, BE, EC)>, } -impl Eth { +impl Eth +where + A: ChainApi, + B: BlockT, + C: ProvideRuntimeApi, + C::Api: EthereumRuntimeRPCApi, + C: HeaderBackend + StorageProvider + 'static, + BE: Backend + 'static, + EC: EthConfig, +{ pub fn new( client: Arc, pool: Arc

, @@ -126,6 +135,117 @@ impl Eth { _marker: PhantomData, } } + + pub async fn block_info_by_number(&self, number: BlockNumber) -> RpcResult> { + let id = match frontier_backend_client::native_block_id::( + self.client.as_ref(), + self.backend.as_ref(), + Some(number), + ) + .await? + { + Some(id) => id, + None => return Ok(BlockInfo::default()), + }; + + let substrate_hash = self + .client + .expect_block_hash_from_id(&id) + .map_err(|_| internal_err(format!("Expect block number from id: {}", id)))?; + + self.block_info_by_substrate_hash(substrate_hash).await + } + + pub async fn block_info_by_eth_block_hash( + &self, + eth_block_hash: H256, + ) -> RpcResult> { + let substrate_hash = match frontier_backend_client::load_hash::( + self.client.as_ref(), + self.backend.as_ref(), + eth_block_hash, + ) + .await + .map_err(|err| internal_err(format!("{:?}", err)))? + { + Some(hash) => hash, + _ => return Ok(BlockInfo::default()), + }; + + self.block_info_by_substrate_hash(substrate_hash).await + } + + pub async fn block_info_by_eth_transaction_hash( + &self, + ethereum_tx_hash: H256, + ) -> RpcResult<(BlockInfo, usize)> { + let (eth_block_hash, index) = match frontier_backend_client::load_transactions::( + self.client.as_ref(), + self.backend.as_ref(), + ethereum_tx_hash, + true, + ) + .await + .map_err(|err| internal_err(format!("{:?}", err)))? + { + Some((hash, index)) => (hash, index as usize), + None => return Ok((BlockInfo::default(), 0)), + }; + + let substrate_hash = match frontier_backend_client::load_hash::( + self.client.as_ref(), + self.backend.as_ref(), + eth_block_hash, + ) + .await + .map_err(|err| internal_err(format!("{:?}", err)))? + { + Some(hash) => hash, + _ => return Ok((BlockInfo::default(), 0)), + }; + + Ok(( + self.block_info_by_substrate_hash(substrate_hash).await?, + index, + )) + } + + pub async fn block_info_by_substrate_hash( + &self, + substrate_hash: B::Hash, + ) -> RpcResult> { + let schema = fc_storage::onchain_storage_schema(self.client.as_ref(), substrate_hash); + let handler = self + .overrides + .schemas + .get(&schema) + .unwrap_or(&self.overrides.fallback); + + let block = self + .block_data_cache + .current_block(schema, substrate_hash) + .await; + let receipts = handler.current_receipts(substrate_hash); + let statuses = self + .block_data_cache + .current_transaction_statuses(schema, substrate_hash) + .await; + let is_eip1559 = handler.is_eip1559(substrate_hash); + let base_fee = self + .client + .runtime_api() + .gas_price(substrate_hash) + .unwrap_or_default(); + + Ok(BlockInfo::new( + block, + receipts, + statuses, + substrate_hash, + is_eip1559, + base_fee, + )) + } } impl> Eth { @@ -236,6 +356,13 @@ where self.block_transaction_count_by_number(number).await } + async fn block_transaction_receipts( + &self, + number: BlockNumber, + ) -> RpcResult>> { + self.block_transaction_receipts(number).await + } + fn block_uncles_count_by_hash(&self, hash: H256) -> RpcResult { self.block_uncles_count_by_hash(hash) } @@ -286,7 +413,8 @@ where } async fn transaction_receipt(&self, hash: H256) -> RpcResult> { - self.transaction_receipt(hash).await + let (block_info, index) = self.block_info_by_eth_transaction_hash(hash).await?; + self.transaction_receipt(&block_info, hash, index).await } // ######################################################################## @@ -587,3 +715,34 @@ where ))) } } + +/// The most commonly used block information in the rpc interfaces. +#[derive(Clone, Default)] +pub struct BlockInfo { + block: Option, + receipts: Option>, + statuses: Option>, + substrate_hash: H, + is_eip1559: bool, + base_fee: U256, +} + +impl BlockInfo { + pub fn new( + block: Option, + receipts: Option>, + statuses: Option>, + substrate_hash: H, + is_eip1559: bool, + base_fee: U256, + ) -> Self { + Self { + block, + receipts, + statuses, + substrate_hash, + is_eip1559, + base_fee, + } + } +} diff --git a/client/rpc/src/eth/transaction.rs b/client/rpc/src/eth/transaction.rs index f313260046..cfd23c3c51 100644 --- a/client/rpc/src/eth/transaction.rs +++ b/client/rpc/src/eth/transaction.rs @@ -34,7 +34,7 @@ use fc_rpc_core::types::*; use fp_rpc::EthereumRuntimeRPCApi; use crate::{ - eth::{transaction_build, Eth, EthConfig}, + eth::{transaction_build, BlockInfo, Eth, EthConfig}, frontier_backend_client, internal_err, }; @@ -49,11 +49,10 @@ where { pub async fn transaction_by_hash(&self, hash: H256) -> RpcResult> { let client = Arc::clone(&self.client); - let block_data_cache = Arc::clone(&self.block_data_cache); let backend = Arc::clone(&self.backend); let graph = Arc::clone(&self.graph); - let (hash, index) = match frontier_backend_client::load_transactions::( + let (eth_block_hash, index) = match frontier_backend_client::load_transactions::( client.as_ref(), backend.as_ref(), hash, @@ -62,7 +61,7 @@ where .await .map_err(|err| internal_err(format!("{:?}", err)))? { - Some((hash, index)) => (hash, index as usize), + Some((eth_block_hash, index)) => (eth_block_hash, index as usize), None => { let api = client.runtime_api(); let best_block = client.info().best_hash; @@ -123,30 +122,12 @@ where } }; - let substrate_hash = match frontier_backend_client::load_hash::( - client.as_ref(), - backend.as_ref(), - hash, - ) - .await - .map_err(|err| internal_err(format!("{:?}", err)))? - { - Some(hash) => hash, - _ => return Ok(None), - }; - - let schema = fc_storage::onchain_storage_schema(client.as_ref(), substrate_hash); - - let block = block_data_cache.current_block(schema, substrate_hash).await; - let statuses = block_data_cache - .current_transaction_statuses(schema, substrate_hash) - .await; - - let base_fee = client - .runtime_api() - .gas_price(substrate_hash) - .unwrap_or_default(); - + let BlockInfo { + block, + statuses, + base_fee, + .. + } = self.block_info_by_eth_block_hash(eth_block_hash).await?; match (block, statuses) { (Some(block), Some(statuses)) => Ok(Some(transaction_build( block.transactions[index].clone(), @@ -163,35 +144,13 @@ where hash: H256, index: Index, ) -> RpcResult> { - let client = Arc::clone(&self.client); - let block_data_cache = Arc::clone(&self.block_data_cache); - let backend = Arc::clone(&self.backend); - - let substrate_hash = match frontier_backend_client::load_hash::( - client.as_ref(), - backend.as_ref(), - hash, - ) - .await - .map_err(|err| internal_err(format!("{:?}", err)))? - { - Some(hash) => hash, - _ => return Ok(None), - }; - let index = index.value(); - - let schema = fc_storage::onchain_storage_schema(client.as_ref(), substrate_hash); - - let block = block_data_cache.current_block(schema, substrate_hash).await; - let statuses = block_data_cache - .current_transaction_statuses(schema, substrate_hash) - .await; - - let base_fee = client - .runtime_api() - .gas_price(substrate_hash) - .unwrap_or_default(); + let BlockInfo { + block, + statuses, + base_fee, + .. + } = self.block_info_by_eth_block_hash(hash).await?; match (block, statuses) { (Some(block), Some(statuses)) => { @@ -217,36 +176,13 @@ where number: BlockNumber, index: Index, ) -> RpcResult> { - let client = Arc::clone(&self.client); - let block_data_cache = Arc::clone(&self.block_data_cache); - let backend = Arc::clone(&self.backend); - - let id = match frontier_backend_client::native_block_id::( - client.as_ref(), - backend.as_ref(), - Some(number), - ) - .await? - { - Some(id) => id, - None => return Ok(None), - }; - let substrate_hash = client - .expect_block_hash_from_id(&id) - .map_err(|_| internal_err(format!("Expect block number from id: {}", id)))?; - let index = index.value(); - let schema = fc_storage::onchain_storage_schema(client.as_ref(), substrate_hash); - - let block = block_data_cache.current_block(schema, substrate_hash).await; - let statuses = block_data_cache - .current_transaction_statuses(schema, substrate_hash) - .await; - - let base_fee = client - .runtime_api() - .gas_price(substrate_hash) - .unwrap_or_default(); + let BlockInfo { + block, + statuses, + base_fee, + .. + } = self.block_info_by_number(number).await?; match (block, statuses) { (Some(block), Some(statuses)) => { @@ -267,114 +203,82 @@ where } } - pub async fn transaction_receipt(&self, hash: H256) -> RpcResult> { - let client = Arc::clone(&self.client); - let overrides = Arc::clone(&self.overrides); - let block_data_cache = Arc::clone(&self.block_data_cache); - let backend = Arc::clone(&self.backend); - - let (hash, index) = match frontier_backend_client::load_transactions::( - client.as_ref(), - backend.as_ref(), - hash, - true, - ) - .await - .map_err(|err| internal_err(format!("{:?}", err)))? - { - Some((hash, index)) => (hash, index as usize), - None => return Ok(None), - }; - - let substrate_hash = match frontier_backend_client::load_hash::( - client.as_ref(), - backend.as_ref(), - hash, - ) - .await - .map_err(|err| internal_err(format!("{:?}", err)))? - { - Some(hash) => hash, - _ => return Ok(None), - }; - - let schema = fc_storage::onchain_storage_schema(client.as_ref(), substrate_hash); - let handler = overrides - .schemas - .get(&schema) - .unwrap_or(&overrides.fallback); - - let block = block_data_cache.current_block(schema, substrate_hash).await; - let statuses = block_data_cache - .current_transaction_statuses(schema, substrate_hash) - .await; - - let receipts = handler.current_receipts(substrate_hash); - let is_eip1559 = handler.is_eip1559(substrate_hash); - + pub async fn transaction_receipt( + &self, + block_info: &BlockInfo, + hash: H256, + index: usize, + ) -> RpcResult> { + let BlockInfo { + block, + receipts, + statuses, + substrate_hash, + .. + } = block_info.clone(); match (block, statuses, receipts) { (Some(block), Some(statuses), Some(receipts)) => { let block_hash = H256::from(keccak_256(&rlp::encode(&block.header))); let receipt = receipts[index].clone(); - let (logs, logs_bloom, status_code, cumulative_gas_used, gas_used) = if !is_eip1559 - { - // Pre-london frontier update stored receipts require cumulative gas calculation. - match receipt { - ethereum::ReceiptV3::Legacy(ref d) => { - let index = core::cmp::min(receipts.len(), index + 1); - let cumulative_gas: u32 = receipts[..index] - .iter() - .map(|r| match r { - ethereum::ReceiptV3::Legacy(d) => Ok(d.used_gas.as_u32()), - _ => Err(internal_err(format!( - "Unknown receipt for request {}", - hash - ))), - }) - .sum::>()?; - ( - d.logs.clone(), - d.logs_bloom, - d.status_code, - U256::from(cumulative_gas), - d.used_gas, - ) - } - _ => { - return Err(internal_err(format!( - "Unknown receipt for request {}", - hash - ))) + let (logs, logs_bloom, status_code, cumulative_gas_used, gas_used) = + if !block_info.is_eip1559 { + // Pre-london frontier update stored receipts require cumulative gas calculation. + match receipt { + ethereum::ReceiptV3::Legacy(ref d) => { + let index = core::cmp::min(receipts.len(), index + 1); + let cumulative_gas: u32 = receipts[..index] + .iter() + .map(|r| match r { + ethereum::ReceiptV3::Legacy(d) => Ok(d.used_gas.as_u32()), + _ => Err(internal_err(format!( + "Unknown receipt for request {}", + hash + ))), + }) + .sum::>()?; + ( + d.logs.clone(), + d.logs_bloom, + d.status_code, + U256::from(cumulative_gas), + d.used_gas, + ) + } + _ => { + return Err(internal_err(format!( + "Unknown receipt for request {}", + hash + ))) + } } - } - } else { - match receipt { - ethereum::ReceiptV3::Legacy(ref d) - | ethereum::ReceiptV3::EIP2930(ref d) - | ethereum::ReceiptV3::EIP1559(ref d) => { - let cumulative_gas = d.used_gas; - let gas_used = if index > 0 { - let previous_receipt = receipts[index - 1].clone(); - let previous_gas_used = match previous_receipt { - ethereum::ReceiptV3::Legacy(d) - | ethereum::ReceiptV3::EIP2930(d) - | ethereum::ReceiptV3::EIP1559(d) => d.used_gas, + } else { + match receipt { + ethereum::ReceiptV3::Legacy(ref d) + | ethereum::ReceiptV3::EIP2930(ref d) + | ethereum::ReceiptV3::EIP1559(ref d) => { + let cumulative_gas = d.used_gas; + let gas_used = if index > 0 { + let previous_receipt = receipts[index - 1].clone(); + let previous_gas_used = match previous_receipt { + ethereum::ReceiptV3::Legacy(d) + | ethereum::ReceiptV3::EIP2930(d) + | ethereum::ReceiptV3::EIP1559(d) => d.used_gas, + }; + cumulative_gas.saturating_sub(previous_gas_used) + } else { + cumulative_gas }; - cumulative_gas.saturating_sub(previous_gas_used) - } else { - cumulative_gas - }; - ( - d.logs.clone(), - d.logs_bloom, - d.status_code, - cumulative_gas, - gas_used, - ) + ( + d.logs.clone(), + d.logs_bloom, + d.status_code, + cumulative_gas, + gas_used, + ) + } } - } - }; + }; let status = statuses[index].clone(); let mut cumulative_receipts = receipts; @@ -383,7 +287,8 @@ where let effective_gas_price = match transaction { EthereumTransaction::Legacy(t) => t.gas_price, EthereumTransaction::EIP2930(t) => t.gas_price, - EthereumTransaction::EIP1559(t) => client + EthereumTransaction::EIP1559(t) => self + .client .runtime_api() .gas_price(substrate_hash) .unwrap_or_default() diff --git a/template/node/src/rpc/eth.rs b/template/node/src/rpc/eth.rs index 2a117ab5b8..ad94176693 100644 --- a/template/node/src/rpc/eth.rs +++ b/template/node/src/rpc/eth.rs @@ -140,7 +140,7 @@ where } io.merge( - Eth::new( + Eth::::new( client.clone(), pool.clone(), graph.clone(), diff --git a/ts-tests/tests/test-block.ts b/ts-tests/tests/test-block.ts index 3ec80a545b..81471a5b5b 100644 --- a/ts-tests/tests/test-block.ts +++ b/ts-tests/tests/test-block.ts @@ -177,7 +177,7 @@ describeWithFrontier("Frontier RPC (Pending Block)", (context) => { nonce = nonce + 100; await sendTransaction(); - // do not seal, get pendign block + // do not seal, get pending block let pending_transactions = []; { const pending = (await customRequest(context.web3, "eth_getBlockByNumber", ["pending", false])).result; @@ -195,3 +195,71 @@ describeWithFrontier("Frontier RPC (Pending Block)", (context) => { expect(pending_transactions).to.be.deep.eq(latest_block.transactions); }); }); + +describeWithFrontier("Frontier RPC (BlockReceipts)", (context) => { + const TEST_ACCOUNT = "0x1111111111111111111111111111111111111111"; + const N = 5; + + it("should return empty if block without transaction", async function () { + await createAndFinalizeBlock(context.web3); + expect(await context.web3.eth.getBlockNumber()).to.equal(1); + + let result = await customRequest(context.web3, "eth_getBlockReceipts", [ + await context.web3.eth.getBlockNumber(), + ]); + expect(result.result.length).to.be.eq(0); + }); + + it("should return multiple receipts", async function () { + var nonce = 0; + let sendTransaction = async () => { + const tx = await context.web3.eth.accounts.signTransaction( + { + from: GENESIS_ACCOUNT, + to: TEST_ACCOUNT, + value: "0x200", // Must be higher than ExistentialDeposit + gasPrice: "0x3B9ACA00", + gas: "0x100000", + nonce: nonce, + }, + GENESIS_ACCOUNT_PRIVATE_KEY + ); + nonce = nonce + 1; + return (await customRequest(context.web3, "eth_sendRawTransaction", [tx.rawTransaction])).result; + }; + + // block 1 send 5 transactions + for (var _ of Array(N)) { + await sendTransaction(); + } + await createAndFinalizeBlock(context.web3); + expect(await context.web3.eth.getBlockNumber()).to.equal(2); + + let result = await customRequest(context.web3, "eth_getBlockReceipts", [2]); + expect(result.result.length).to.be.eq(N); + }); + + it("should support block number, tag and hash", async function () { + let block_number = await context.web3.eth.getBlockNumber(); + + // block number + expect((await customRequest(context.web3, "eth_getBlockReceipts", [block_number])).result.length).to.be.eq(N); + // block hash + let block = await context.web3.eth.getBlock(block_number); + expect( + ( + await customRequest(context.web3, "eth_getBlockReceipts", [ + { + blockHash: block.hash, + requireCanonical: true, + }, + ]) + ).result.length + ).to.be.eq(N); + // block tags + expect((await customRequest(context.web3, "eth_getBlockReceipts", ["earliest"])).result.length).to.be.eq(0); + expect((await customRequest(context.web3, "eth_getBlockReceipts", ["pending"])).result).to.be.null; + expect((await customRequest(context.web3, "eth_getBlockReceipts", ["finalized"])).result.length).to.be.eq(N); + expect((await customRequest(context.web3, "eth_getBlockReceipts", ["latest"])).result.length).to.be.eq(N); + }); +});