diff --git a/crates/stratus_metrics/macros/src/lib.rs b/crates/stratus_metrics/macros/src/lib.rs index 92cee7c3e..a7d4a29d9 100644 --- a/crates/stratus_metrics/macros/src/lib.rs +++ b/crates/stratus_metrics/macros/src/lib.rs @@ -77,7 +77,7 @@ pub fn metrics(input: TokenStream) -> TokenStream { /// /// ```ignore /// #[timed( -/// executor_external_transaction, +/// executor_imported_transaction, /// labels( /// contract = |input| contract_name(&input.execution_info.to), /// function = |input| function_sig(&input.execution_info.input), diff --git a/crates/stratus_metrics/src/definitions.rs b/crates/stratus_metrics/src/definitions.rs index 9c04a010b..a59b922b0 100644 --- a/crates/stratus_metrics/src/definitions.rs +++ b/crates/stratus_metrics/src/definitions.rs @@ -67,11 +67,11 @@ metrics! { }, group: executor { - "Time executing and persist an external block with all transactions." - histogram_duration executor_external_block{}, + "Time executing and persist an imported stratus block with all transactions." + histogram_duration executor_imported_block{}, - "Time executing an external transaction." - histogram_duration executor_external_transaction{contract, function}, + "Time executing an imported stratus transaction." + histogram_duration executor_imported_transaction{contract, function}, "Time executing a local transaction." histogram_duration executor_local_transaction{success, contract, function}, diff --git a/e2e/test/external/e2e-json-rpc.test.ts b/e2e/test/external/e2e-json-rpc.test.ts index 9dbbe5e50..b69437220 100644 --- a/e2e/test/external/e2e-json-rpc.test.ts +++ b/e2e/test/external/e2e-json-rpc.test.ts @@ -35,6 +35,13 @@ import { toPaddedHex, } from "../helpers/rpc"; +// The block DTO serializes hashes as byte arrays; convert them to hex for assertions. +const bytesToHex = (bytes: number[]) => "0x" + Buffer.from(bytes).toString("hex"); + +// The block DTO serializes block numbers as byte-order-swapped u32; swap them back for assertions. +const swapU32 = (value: number) => + ((value & 0xff) << 24) | ((value & 0xff00) << 8) | ((value >>> 8) & 0xff00) | (value >>> 24); + describe("JSON-RPC", () => { before(() => { expect(currentBlockMode()).eq(BlockMode.External, "Wrong block mining mode is used"); @@ -165,24 +172,18 @@ describe("JSON-RPC", () => { const block = await send("eth_getBlockByNumber", [blockNumber, true]); const blockHash = block.hash; - // Get individual block and receipt - const individualBlock = await send("eth_getBlockByHash", [blockHash, true]); - const individualReceipt = await send("eth_getTransactionReceipt", [txHash, true]); - // Get block and receipts using stratus endpoint const response = await send("stratus_getBlockAndReceipts", [blockHash]); - // Validate block - expect(response.block).to.not.be.null; - expect(response.block).to.deep.equal(individualBlock); - - // Validate receipt - expect(response.receipts).to.have.length(1); - const combinedReceipt = response.receipts[0]; - const safeIndividualReceipt = individualReceipt!; - - // Compare receipt fields - expect(combinedReceipt).to.deep.equal(safeIndividualReceipt); + // The response is the stratus block DTO, with receipts embedded in each transaction + expect(response.block).to.equal(undefined); + expect(response.receipts).to.equal(undefined); + expect(bytesToHex(response.header.hash)).to.equal(blockHash); + expect(swapU32(response.header.number)).to.equal(parseInt(blockNumber, 16)); + expect(response.transactions).to.have.length(1); + expect(bytesToHex(response.transactions[0].input.hash)).to.equal(txHash); + expect(response.transactions[0].execution).to.not.equal(undefined); + expect(response.transactions[0].logs).to.be.an("array"); }); }); describe("stratus_getBlockByTimestamp", () => { diff --git a/e2e/test/follower/e2e-pagination.test.ts b/e2e/test/follower/e2e-pagination.test.ts index 12f69c0f5..61ff62d67 100644 --- a/e2e/test/follower/e2e-pagination.test.ts +++ b/e2e/test/follower/e2e-pagination.test.ts @@ -11,14 +11,25 @@ import { FOLLOWER_URL, rpcCall, waitForFollowerBlock, waitForReceipt } from "./h const MAX_RESPONSE_BYTES = 8192; const FAT_TX_DATA_BYTES = 50_000; +// The block DTO serializes hashes as byte arrays; convert them to hex for assertions. +const bytesToHex = (bytes: number[]) => "0x" + Buffer.from(bytes).toString("hex"); + +// The block DTO serializes block numbers as byte-order-swapped u32; swap them back for assertions. +const swapU32 = (value: number) => + ((value & 0xff) << 24) | ((value & 0xff00) << 8) | ((value >>> 8) & 0xff00) | (value >>> 24); + describe("Pagination", () => { it("paginates oversized importer responses and keeps the follower syncing", async () => { - // a fitting response is served normally, with no envelope, so old followers are unaffected + // a fitting response is served normally, with no envelope const earlyBlock = await send("eth_getBlockByNumber", ["0x1", false]); - expect(earlyBlock).to.not.be.null; + expect(earlyBlock).to.not.equal(null); const small = await send("stratus_getBlockAndReceipts", [earlyBlock.hash]); - expect(small.stratus_paginated).to.be.undefined; - expect(small.block.number).to.equal("0x1"); + expect(small.stratus_paginated).to.equal(undefined); + expect(small.block).to.equal(undefined); + expect(small.receipts).to.equal(undefined); + expect(bytesToHex(small.header.hash)).to.equal(earlyBlock.hash); + expect(swapU32(small.header.number)).to.equal(1); + expect(small.transactions).to.be.an("array"); // fat contract deployment: the code always fails, but the fat data makes the response oversized const nonce = await send("eth_getTransactionCount", [ALICE.address]); @@ -38,7 +49,7 @@ describe("Pagination", () => { // the old single-parameter call fails with the oversized response error (-32008), // which is exactly what would stall an importer before pagination existed const legacy = await sendAndGetFullResponse("stratus_getBlockAndReceipts", [fatBlockHash]); - expect(legacy.data.error).to.not.be.undefined; + expect(legacy.data.error).to.not.equal(undefined); expect(legacy.data.error.code).to.equal(-32008); // paginated reassembly; the chunk size is decided by the leader's response size limit @@ -46,7 +57,7 @@ describe("Pagination", () => { let total = 0; for (let offset = 0; total === 0 || assembled.length < total; offset = assembled.length) { const envelope = await send("stratus_getBlockAndReceipts", [fatBlockHash, { offset: offset }]); - expect(envelope.stratus_paginated).to.not.be.undefined; + expect(envelope.stratus_paginated).to.not.equal(undefined); total = envelope.stratus_paginated.total; const chunk = Buffer.from(envelope.stratus_paginated.chunk, "base64"); expect(chunk.length).to.be.greaterThan(0); @@ -55,18 +66,31 @@ describe("Pagination", () => { expect(assembled.length).to.equal(total); expect(total).to.be.greaterThan(MAX_RESPONSE_BYTES, "the block response should be oversized"); - // the reassembled content matches the block + // the reassembled content is the block DTO, with receipts embedded const response = JSON.parse(assembled.toString("utf8")); - expect(response.block.hash).to.equal(fatBlockHash); - expect(parseInt(response.block.number, 16)).to.equal(fatBlockNumber); - expect(response.block.transactions).to.have.length(1); - expect(response.receipts).to.have.length(1); - expect(response.receipts[0].transactionHash).to.equal(txHash); + expect(response.block).to.equal(undefined); + expect(response.receipts).to.equal(undefined); + expect(bytesToHex(response.header.hash)).to.equal(fatBlockHash); + expect(swapU32(response.header.number)).to.equal(fatBlockNumber); + expect(response.transactions).to.have.length(1); + expect(bytesToHex(response.transactions[0].input.hash)).to.equal(txHash); + expect(response.transactions[0].execution).to.not.equal(undefined); + expect(response.transactions[0].logs).to.not.equal(undefined); // the follower imports the fat block through the paginated importer await waitForFollowerBlock(fatBlockNumber); + + // the follower serves the same block content; the leader block is requested thin because + // its response limit rejects the full fat block over `eth_getBlockByNumber` + const leaderBlock = await send("eth_getBlockByNumber", [receipt.blockNumber, false]); + const followerBlock = await rpcCall(FOLLOWER_URL, "eth_getBlockByNumber", [receipt.blockNumber, true]); + expect(followerBlock.result.hash).to.equal(leaderBlock.hash); + expect(followerBlock.result.transactions).to.have.lengthOf(leaderBlock.transactions.length); + expect(followerBlock.result.transactions[0].hash).to.equal(txHash); + + // and the transaction receipt is available on the follower const followerReceipt = await rpcCall(FOLLOWER_URL, "eth_getTransactionReceipt", [txHash]); - expect(followerReceipt.result).to.not.be.null; + expect(followerReceipt.result).to.not.equal(null); expect(followerReceipt.result.blockNumber).to.equal(receipt.blockNumber); }); }); diff --git a/src/alias.rs b/src/alias.rs index 2944f0ced..76f728e28 100644 --- a/src/alias.rs +++ b/src/alias.rs @@ -2,8 +2,6 @@ use alloy_primitives::B256; -use crate::eth::types::ExternalTransaction; - // ----------------------------------------------------------------------------- // Serde // ----------------------------------------------------------------------------- @@ -14,7 +12,6 @@ pub type JsonValue = serde_json::Value; // ----------------------------------------------------------------------------- pub type AlloyBlockVoid = alloy_rpc_types_eth::Block<()>; pub type AlloyBlockAlloyTransaction = alloy_rpc_types_eth::Block; -pub type AlloyBlockExternalTransaction = alloy_rpc_types_eth::Block; pub type AlloyBlockB256 = alloy_rpc_types_eth::Block; pub type AlloyBytes = alloy_primitives::Bytes; pub type AlloyLog = alloy_rpc_types_eth::Log; diff --git a/src/eth/executor/evm/types/output/transaction_execution.rs b/src/eth/executor/evm/types/output/transaction_execution.rs index af0e88363..7d8c1a5a5 100644 --- a/src/eth/executor/evm/types/output/transaction_execution.rs +++ b/src/eth/executor/evm/types/output/transaction_execution.rs @@ -1,4 +1,6 @@ +#[cfg(test)] use alloy_primitives::B256; +use alloy_primitives::U256; use derive_more::Deref; use derive_more::DerefMut; use display_json::DebugAsJson; @@ -15,14 +17,23 @@ use crate::eth::executor::types::state::Complete; use crate::eth::types::Account; use crate::eth::types::Address; use crate::eth::types::Bytes; -use crate::eth::types::ExternalReceipt; use crate::eth::types::Gas; +use crate::eth::types::Hash; use crate::eth::types::Log; use crate::eth::types::StratusError; use crate::eth::types::Wei; use crate::ext::not; use crate::log_and_err; +/// `ERC20Trace` event hash, whose first 32 data bytes carry the transaction `gasLeft` at emit time. +const ERC20_TRACE_EVENT_HASH: [u8; 32] = hex!("31738ac4a7c9a10ecbbfd3fed5037971ba81b8f6aa4f72a23f5364e9bc76d671"); + +/// `BalanceTrackerTrace` event hash, whose first 32 data bytes carry the transaction `gasLeft` at emit time. +const BALANCE_TRACKER_TRACE_EVENT_HASH: [u8; 32] = hex!("63f1e32b72965e2be75e03024856287aff9e4cdbcec65869c51014fc2c1c95d9"); + +/// Event hashes whose first 32 data bytes carry the transaction `gasLeft` at emit time. +const GAS_LEFT_EVENT_HASHES: [&[u8]; 2] = [&ERC20_TRACE_EVENT_HASH, &BALANCE_TRACKER_TRACE_EVENT_HASH]; + /// Output of a transaction executed in the EVM. #[derive(DebugAsJson, Clone, PartialEq, Eq, serde::Serialize, Default, Deref, DerefMut)] #[cfg_attr(test, derive(fake::Dummy))] @@ -57,13 +68,32 @@ pub struct TransactionExecutionResult { } impl TransactionExecutionOutput { - /// Creates an execution from an external transaction that failed. - pub fn from_failed_external_transaction(sender: Account, receipt: &ExternalReceipt) -> anyhow::Result { - if receipt.is_success() { + /// Checks if the current transaction was completed normally. + pub fn is_success(&self) -> bool { + self.result.is_success() + } + + /// Checks if the current transaction was completed with a failure (reverted or halted). + pub fn is_failure(&self) -> bool { + not(self.is_success()) + } + + /// Returns the address of the deployed contract if the transaction is a deployment. + pub fn contract_address(&self) -> Option
{ + if let Some(contract_address) = &self.deployed_contract_address { + return Some(contract_address.to_owned()); + } + + None + } + + /// Creates an execution from an imported stratus transaction that failed. + pub fn from_failed_imported_transaction(sender: Account, gas_price: Wei, stored: &TransactionExecutionResult) -> anyhow::Result { + if stored.result.is_success() { return log_and_err!("cannot create failed execution for successful transaction"); } - if not(receipt.inner.logs().is_empty()) { - return log_and_err!("failed receipt should not have produced logs"); + if not(stored.logs.is_empty()) { + return log_and_err!("failed transaction should not have produced logs"); } // generate sender changes incrementing the nonce @@ -82,121 +112,91 @@ impl TransactionExecutionOutput { result: ExecutionResult::new_reverted("reverted externally".into()), // assume it reverted output: Bytes::default(), // we cannot really know without performing an eth_call to the external system logs: Vec::new(), - gas_used: Gas::from(receipt.gas_used), + gas_used: stored.gas_used, deployed_contract_address: None, }, state: changes, }; - execution.apply_receipt(receipt)?; + execution.apply_imported(stored, gas_price, address)?; Ok(execution) } - /// Checks if the current transaction was completed normally. - pub fn is_success(&self) -> bool { - self.result.is_success() - } - - /// Checks if the current transaction was completed with a failure (reverted or halted). - pub fn is_failure(&self) -> bool { - not(self.is_success()) - } - - /// Returns the address of the deployed contract if the transaction is a deployment. - pub fn contract_address(&self) -> Option
{ - if let Some(contract_address) = &self.deployed_contract_address { - return Some(contract_address.to_owned()); - } - - None - } - - /// Checks if current execution state matches the information present in the external receipt. - pub fn compare_with_receipt(&self, receipt: &ExternalReceipt) -> anyhow::Result<()> { + /// Checks if current execution state matches the stored execution of an imported transaction. + pub fn compare_with_imported(&self, tx_hash: Hash, stored: &TransactionExecutionResult) -> anyhow::Result<()> { // compare execution status - if self.is_success() != receipt.is_success() { + if self.is_success() != stored.result.is_success() { return log_and_err!(format!( - "transaction status mismatch | hash={} execution={:?} receipt={:?}", - receipt.hash(), - self.result, - receipt.status() + "transaction status mismatch | hash={} execution={:?} imported={:?}", + tx_hash, self.result, stored.result )); } - let receipt_logs = receipt.inner.logs(); - // compare logs length - if self.logs.len() != receipt_logs.len() { + if self.logs.len() != stored.logs.len() { tracing::trace!(logs = ?self.logs, "execution logs"); - tracing::trace!(logs = ?receipt_logs, "receipt logs"); + tracing::trace!(logs = ?stored.logs, "imported logs"); return log_and_err!(format!( - "logs length mismatch | hash={} execution={} receipt={}", - receipt.hash(), + "logs length mismatch | hash={} execution={} imported={}", + tx_hash, self.logs.len(), - receipt_logs.len() + stored.logs.len() )); } // compare logs pairs - for (log_index, (execution_log, receipt_log)) in self.logs.iter().zip(receipt_logs).enumerate() { + for (log_index, (execution_log, imported_log)) in self.logs.iter().zip(&stored.logs).enumerate() { // compare log topics length - if execution_log.topics_non_empty().len() != receipt_log.topics().len() { + if execution_log.topics_non_empty().len() != imported_log.topics_non_empty().len() { return log_and_err!(format!( - "log topics length mismatch | hash={} log_index={} execution={} receipt={}", - receipt.hash(), + "log topics length mismatch | hash={} log_index={} execution={} imported={}", + tx_hash, log_index, execution_log.topics_non_empty().len(), - receipt_log.topics().len(), + imported_log.topics_non_empty().len(), )); } // compare log topics content - for (topic_index, (execution_log_topic, receipt_log_topic)) in execution_log.topics_non_empty().iter().zip(receipt_log.topics().iter()).enumerate() + for (topic_index, (execution_log_topic, imported_log_topic)) in + execution_log.topics_non_empty().iter().zip(imported_log.topics_non_empty().iter()).enumerate() { - if B256::from(*execution_log_topic) != *receipt_log_topic { + if execution_log_topic != imported_log_topic { return log_and_err!(format!( - "log topic content mismatch | hash={} log_index={} topic_index={} execution={} receipt={:#x}", - receipt.hash(), - log_index, - topic_index, - execution_log_topic, - receipt_log_topic, + "log topic content mismatch | hash={} log_index={} topic_index={} execution={:#x} imported={:#x}", + tx_hash, log_index, topic_index, execution_log_topic.0, imported_log_topic.0, )); } } // compare log data content - if execution_log.data.as_ref() != receipt_log.data().data.as_ref() { + if execution_log.data.as_ref() != imported_log.data.as_ref() { return log_and_err!(format!( - "log data content mismatch | hash={} log_index={} execution={} receipt={:#x}", - receipt.hash(), - log_index, - execution_log.data, - receipt_log.data().data, + "log data content mismatch | hash={} log_index={} execution={} imported={}", + tx_hash, log_index, execution_log.data, imported_log.data, )); } } Ok(()) } - /// External transactions are re-executed locally with max gas and zero gas price. + /// Imported transactions are re-executed locally with max gas and zero gas price. /// - /// This causes some attributes to be different from the original execution. + /// This causes some attributes to be different from the stored execution. /// - /// This method updates the attributes that can diverge based on the receipt of the external transaction. - pub fn apply_receipt(&mut self, receipt: &ExternalReceipt) -> anyhow::Result<()> { + /// This method updates the attributes that can diverge based on the stored execution of the imported transaction. + pub fn apply_imported(&mut self, stored: &TransactionExecutionResult, gas_price: Wei, sender: Address) -> anyhow::Result<()> { // fix gas - self.gas_used = Gas::from(receipt.gas_used); + self.gas_used = stored.gas_used; // fix logs - self.fix_logs_gas_left(receipt); + self.fix_logs_gas_left_from_stored(&stored.logs); // fix sender balance - let execution_cost = receipt.execution_cost(); + let execution_cost = Wei(gas_price.0 * U256::from(stored.gas_used.as_u64())); if execution_cost > Wei::ZERO { // find sender changes - let sender_address: Address = receipt.0.from.into(); - let Some(sender_changes) = self.state.accounts.get_mut(&sender_address) else { + let Some(sender_changes) = self.state.accounts.get_mut(&sender) else { return log_and_err!("sender changes not present in execution when applying execution costs"); }; @@ -214,37 +214,22 @@ impl TransactionExecutionOutput { Ok(()) } - /// Apply `gasLeft` values from receipt to execution logs. + /// Apply `gasLeft` values from the stored logs to the execution logs. /// - /// External transactions are re-executed locally with a different amount of gas limit, so, rely - /// on the given receipt to copy the `gasLeft` values found in Logs. - /// - /// This is necessary if the contract emits an event that puts `gasLeft` in a log, this function - /// covers the following events that do the described: - /// - /// - `ERC20Trace` (topic0: `0x31738ac4a7c9a10ecbbfd3fed5037971ba81b8f6aa4f72a23f5364e9bc76d671`) - /// - `BalanceTrackerTrace` (topic0: `0x63f1e32b72965e2be75e03024856287aff9e4cdbcec65869c51014fc2c1c95d9`) - /// - /// The overwriting should be done by copying the first 32 bytes from the receipt to log in `self`. - fn fix_logs_gas_left(&mut self, receipt: &ExternalReceipt) { - const ERC20_TRACE_EVENT_HASH: [u8; 32] = hex!("31738ac4a7c9a10ecbbfd3fed5037971ba81b8f6aa4f72a23f5364e9bc76d671"); - const BALANCE_TRACKER_TRACE_EVENT_HASH: [u8; 32] = hex!("63f1e32b72965e2be75e03024856287aff9e4cdbcec65869c51014fc2c1c95d9"); - - const EVENT_HASHES: [&[u8]; 2] = [&ERC20_TRACE_EVENT_HASH, &BALANCE_TRACKER_TRACE_EVENT_HASH]; - - let receipt_logs = receipt.inner.logs(); - - for (execution_log, receipt_log) in self.logs.iter_mut().zip(receipt_logs) { - let execution_log_matches = || execution_log.topic0.is_some_and(|topic| EVENT_HASHES.contains(&topic.as_ref())); - let receipt_log_matches = || receipt_log.topics().first().is_some_and(|topic| EVENT_HASHES.contains(&topic.as_ref())); + /// Imported transactions are re-executed locally with a different amount of gas limit, so rely + /// on the stored logs to copy the `gasLeft` values. + fn fix_logs_gas_left_from_stored(&mut self, stored_logs: &[Log]) { + for (execution_log, stored_log) in self.logs.iter_mut().zip(stored_logs) { + let execution_log_matches = || execution_log.topic0.is_some_and(|topic| GAS_LEFT_EVENT_HASHES.contains(&topic.0.as_ref())); + let stored_log_matches = || stored_log.topic0.is_some_and(|topic| GAS_LEFT_EVENT_HASHES.contains(&topic.0.as_ref())); // only try overwriting if both logs refer to the target event - let should_overwrite = execution_log_matches() && receipt_log_matches(); + let should_overwrite = execution_log_matches() && stored_log_matches(); if !should_overwrite { continue; } - let Some(source) = receipt_log.data().data.get(0..32) else { + let Some(source) = stored_log.data.as_ref().get(0..32) else { continue; }; let mut data = execution_log.data.0.to_vec(); @@ -349,7 +334,7 @@ mod tests { use crate::eth::types::Nonce; #[test] - fn test_from_failed_external_transaction() { + fn test_from_failed_imported_transaction() { // Create a mock sender account let sender_address: Address = Faker.fake(); let sender = Account { @@ -359,30 +344,21 @@ mod tests { bytecode: None, }; - // Create a mock failed receipt - let mut receipt: ExternalReceipt = Faker.fake(); - let mut inner_receipt = receipt.0.clone(); - - // Clear logs for failed transaction - if let alloy_consensus::ReceiptEnvelope::Legacy(ref mut r) = inner_receipt.inner { - r.receipt.status = alloy_consensus::Eip658Value::Eip658(false); - r.receipt.logs.clear(); - } else { - panic!("expected be legacy!") - } - - // Update from address - inner_receipt.from = sender_address.into(); - receipt.0 = inner_receipt; + // Create a stored failed execution + let mut stored: TransactionExecutionResult = Faker.fake(); + stored.result = ExecutionResult::new_reverted("reverted externally".into()); + stored.logs = Vec::new(); + stored.gas_used = Gas::from(100u64); // Test the method - let execution = TransactionExecutionOutput::from_failed_external_transaction(sender.clone(), &receipt).unwrap(); + let gas_price = Wei::from(1u64); + let execution = TransactionExecutionOutput::from_failed_imported_transaction(sender.clone(), gas_price, &stored).unwrap(); // Verify execution state assert!(execution.is_failure()); assert_eq!(execution.output, Bytes::default()); assert!(execution.logs.is_empty()); - assert_eq!(execution.gas_used, Gas::from(receipt.gas_used)); + assert_eq!(execution.gas_used, Gas::from(100u64)); // Verify sender changes let sender_changes = execution.state.accounts.get(&sender_address).unwrap(); @@ -391,54 +367,73 @@ mod tests { let modified_nonce = *sender_changes.nonce.value(); assert_eq!(modified_nonce, Nonce::from(2u64)); - // Balance should be reduced by execution cost - if receipt.execution_cost() > Wei::ZERO { - let modified_balance = *sender_changes.balance.value(); - assert!(sender.balance >= modified_balance); - } + // Balance should be reduced by execution cost (gas price * gas used) + let modified_balance = *sender_changes.balance.value(); + assert_eq!(modified_balance, Wei::from(900u64)); // 1000 - 1 * 100 + + // Guard: successful stored executions are rejected + let mut success_stored = stored.clone(); + success_stored.result = ExecutionResult::Success; + assert!(TransactionExecutionOutput::from_failed_imported_transaction(sender, gas_price, &success_stored).is_err()); } #[test] - fn test_compare_with_receipt_success_status_mismatch() { + fn test_compare_with_imported_matching_execution_ok() { + // Create a mock execution and stored execution with identical content + let mut log: Log = Faker.fake(); + log.topic0 = Some(B256::from([1u8; 32]).into()); + log.topic1 = None; + log.topic2 = None; + log.topic3 = None; + log.data = vec![1, 2, 3, 4].into(); + + let mut execution: TransactionExecutionOutput = Faker.fake(); + execution.result = ExecutionResult::Success; + execution.logs = vec![log.clone()]; + + let mut stored: TransactionExecutionResult = Faker.fake(); + stored.result = ExecutionResult::Success; + stored.logs = vec![log]; + + let tx_hash: Hash = Faker.fake(); + assert!(execution.compare_with_imported(tx_hash, &stored).is_ok()); + } + + #[test] + fn test_compare_with_imported_status_mismatch() { // Create a mock execution (success) let mut execution: TransactionExecutionOutput = Faker.fake(); execution.result = ExecutionResult::Success; - // Create a mock receipt (failed) - let mut receipt: ExternalReceipt = Faker.fake(); - if let alloy_consensus::ReceiptEnvelope::Legacy(r) = &mut receipt.0.inner { - r.receipt.status = alloy_consensus::Eip658Value::Eip658(false); - } else { - panic!("expected be legacy!") - } + // Create a stored execution (failed) + let mut stored: TransactionExecutionResult = Faker.fake(); + stored.result = ExecutionResult::new_reverted("reverted externally".into()); // Verify comparison fails - assert!(execution.compare_with_receipt(&receipt).is_err()); + let tx_hash: Hash = Faker.fake(); + assert!(execution.compare_with_imported(tx_hash, &stored).is_err()); } #[test] - fn test_compare_with_receipt_logs_length_mismatch() { + fn test_compare_with_imported_logs_length_mismatch() { // Create a mock execution with logs let mut execution: TransactionExecutionOutput = Faker.fake(); execution.result = ExecutionResult::Success; execution.logs = vec![Faker.fake(), Faker.fake()]; // Two logs - // Create a mock receipt with different number of logs - let mut receipt: ExternalReceipt = Faker.fake(); - if let alloy_consensus::ReceiptEnvelope::Legacy(r) = &mut receipt.0.inner { - r.receipt.status = alloy_consensus::Eip658Value::Eip658(true); - r.receipt.logs = vec![alloy_rpc_types_eth::Log::default()]; // Only one log - } else { - panic!("expected be legacy!") - } + // Create a stored execution with only one log + let mut stored: TransactionExecutionResult = Faker.fake(); + stored.result = ExecutionResult::Success; + stored.logs = vec![Faker.fake()]; // Only one log // Verify comparison fails - assert!(execution.compare_with_receipt(&receipt).is_err()); + let tx_hash: Hash = Faker.fake(); + assert!(execution.compare_with_imported(tx_hash, &stored).is_err()); } #[test] - fn test_compare_with_receipt_log_topics_length_mismatch() { - // Create a mock log with topics + fn test_compare_with_imported_log_topics_length_mismatch() { + // Create a mock log with two topics let mut log1: Log = Faker.fake(); log1.topic0 = Some(Faker.fake()); log1.topic1 = Some(Faker.fake()); @@ -450,32 +445,29 @@ mod tests { execution.result = ExecutionResult::Success; execution.logs = vec![log1]; - // Create receipt log with different number of topics - let mut receipt_log = alloy_rpc_types_eth::Log::::default(); - let topics = vec![B256::default()]; - receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(topics, alloy_primitives::Bytes::default()); - // Only one topic instead of two - - // Create a receipt with this log - let mut receipt: ExternalReceipt = Faker.fake(); - if let alloy_consensus::ReceiptEnvelope::Legacy(r) = &mut receipt.0.inner { - r.receipt.status = alloy_consensus::Eip658Value::Eip658(true); - r.receipt.logs = vec![receipt_log.clone()]; - } else { - panic!("expected be legacy!") - } + // Create a stored log with only one topic + let mut stored_log: Log = Faker.fake(); + stored_log.topic0 = Some(Faker.fake()); + stored_log.topic1 = None; + stored_log.topic2 = None; + stored_log.topic3 = None; + + let mut stored: TransactionExecutionResult = Faker.fake(); + stored.result = ExecutionResult::Success; + stored.logs = vec![stored_log]; // Verify comparison fails - assert!(execution.compare_with_receipt(&receipt).is_err()); + let tx_hash: Hash = Faker.fake(); + assert!(execution.compare_with_imported(tx_hash, &stored).is_err()); } #[test] - fn test_compare_with_receipt_topic_content_mismatch() { + fn test_compare_with_imported_topic_content_mismatch() { // Create two genuinely different topics let topic_value = B256::from([1u8; 32]); let different_topic = B256::from([2u8; 32]); - // Create a mock log with only topic0 set (clear others to avoid length mismatch) + // Create a mock log with only topic0 set let mut log1: Log = Faker.fake(); log1.topic0 = Some(topic_value.into()); log1.topic1 = None; @@ -488,27 +480,26 @@ mod tests { execution.result = ExecutionResult::Success; execution.logs = vec![log1]; - // Create receipt log with same number of topics but different content - let mut receipt_log = alloy_rpc_types_eth::Log::::default(); - let topics = vec![different_topic]; - receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(topics, alloy_primitives::Bytes::default()); - - // Create receipt with this log - let mut receipt: ExternalReceipt = Faker.fake(); - if let alloy_consensus::ReceiptEnvelope::Legacy(r) = &mut receipt.0.inner { - r.receipt.status = alloy_consensus::Eip658Value::Eip658(true); - r.receipt.logs = vec![receipt_log.clone()]; - } else { - panic!("expected be legacy!") - } + // Create a stored log with the same number of topics but different content + let mut stored_log: Log = Faker.fake(); + stored_log.topic0 = Some(different_topic.into()); + stored_log.topic1 = None; + stored_log.topic2 = None; + stored_log.topic3 = None; + stored_log.data = vec![].into(); + + let mut stored: TransactionExecutionResult = Faker.fake(); + stored.result = ExecutionResult::Success; + stored.logs = vec![stored_log]; // Verify comparison fails due to topic content mismatch - let err = execution.compare_with_receipt(&receipt).unwrap_err(); + let tx_hash: Hash = Faker.fake(); + let err = execution.compare_with_imported(tx_hash, &stored).unwrap_err(); assert!(err.to_string().contains("log topic content mismatch")); } #[test] - fn test_compare_with_receipt_data_content_mismatch() { + fn test_compare_with_imported_data_content_mismatch() { // Create a mock log with data let mut log1: Log = Faker.fake(); log1.topic0 = Some(Faker.fake()); @@ -519,48 +510,120 @@ mod tests { execution.result = ExecutionResult::Success; execution.logs = vec![log1]; - // Create receipt log with different data - let mut receipt_log = alloy_rpc_types_eth::Log::::default(); - let topics = vec![B256::default()]; - receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(topics, alloy_primitives::Bytes::default()); - receipt_log.inner.data = alloy_primitives::LogData::new(vec![B256::default()], alloy_primitives::Bytes::from(vec![5, 6, 7, 8])).unwrap(); - - // Create receipt with this log - let mut receipt: ExternalReceipt = Faker.fake(); - if let alloy_consensus::ReceiptEnvelope::Legacy(r) = &mut receipt.0.inner { - r.receipt.status = alloy_consensus::Eip658Value::Eip658(true); - r.receipt.logs = vec![receipt_log.clone()]; - } else { - panic!("expected be legacy!") - } + // Create a stored log with different data + let mut stored_log: Log = Faker.fake(); + stored_log.topic0 = Some(Faker.fake()); + stored_log.data = vec![5, 6, 7, 8].into(); + + let mut stored: TransactionExecutionResult = Faker.fake(); + stored.result = ExecutionResult::Success; + stored.logs = vec![stored_log]; // Verify comparison fails - assert!(execution.compare_with_receipt(&receipt).is_err()); + let tx_hash: Hash = Faker.fake(); + assert!(execution.compare_with_imported(tx_hash, &stored).is_err()); + } + + #[test] + fn test_apply_imported() { + // Create a mock sender account with balance + let sender_address: Address = Faker.fake(); + let sender = Account { + address: sender_address, + nonce: Nonce::from(1u64), + balance: Wei::from(1000u64), + bytecode: None, + }; + + // Create a mock execution with the sender account in its state + let mut execution: TransactionExecutionOutput = Faker.fake(); + let mut sender_changes = AccountChanges::default(); + sender_changes.apply_original(sender); + let mut accounts = HashMap::with_hasher(foldhash::fast::RandomState::default()); + accounts.insert(sender_address, sender_changes); + execution.state = State { + accounts, + ..Default::default() + }; + execution.gas_used = Gas::from(100u64); + + // Create a stored execution with different gas + let mut stored: TransactionExecutionResult = Faker.fake(); + stored.gas_used = Gas::from(200u64); + stored.logs = Vec::new(); + + // Apply the stored execution + let gas_price = Wei::from(3u64); + execution.apply_imported(&stored, gas_price, sender_address).unwrap(); + + // Gas used should match the stored execution + assert_eq!(execution.gas_used, Gas::from(200u64)); + + // Sender balance should be reduced by gas price * stored gas (1000 - 3 * 200 = 400) + let sender_changes = execution.state.accounts.get(&sender_address).unwrap(); + let modified_balance = *sender_changes.balance.value(); + assert_eq!(modified_balance, Wei::from(400u64)); + } + + #[test] + fn test_apply_imported_balance_floors_at_zero() { + // Create a mock sender account with a balance smaller than the execution cost + let sender_address: Address = Faker.fake(); + let sender = Account { + address: sender_address, + nonce: Nonce::from(1u64), + balance: Wei::from(100u64), + bytecode: None, + }; + + // Create a mock execution with the sender account in its state + let mut execution: TransactionExecutionOutput = Faker.fake(); + let mut sender_changes = AccountChanges::default(); + sender_changes.apply_original(sender); + let mut accounts = HashMap::with_hasher(foldhash::fast::RandomState::default()); + accounts.insert(sender_address, sender_changes); + execution.state = State { + accounts, + ..Default::default() + }; + + // Stored execution with a cost far above the balance (3 * 200 = 600 > 100) + let mut stored: TransactionExecutionResult = Faker.fake(); + stored.gas_used = Gas::from(200u64); + stored.logs = Vec::new(); + + let gas_price = Wei::from(3u64); + execution.apply_imported(&stored, gas_price, sender_address).unwrap(); + + // Sender balance should floor at zero instead of underflowing + let sender_changes = execution.state.accounts.get(&sender_address).unwrap(); + let modified_balance = *sender_changes.balance.value(); + assert_eq!(modified_balance, Wei::ZERO); } #[test] - fn test_fix_logs_gas_left() { + fn test_fix_logs_gas_left_from_stored() { // Set up test constants const ERC20_TRACE_HASH: [u8; 32] = hex!("31738ac4a7c9a10ecbbfd3fed5037971ba81b8f6aa4f72a23f5364e9bc76d671"); const BALANCE_TRACKER_TRACE_HASH: [u8; 32] = hex!("63f1e32b72965e2be75e03024856287aff9e4cdbcec65869c51014fc2c1c95d9"); - // Create a mock execution with logs that have gasLeft value we want to override + // Create an execution with logs that have gasLeft values we want to override let mut execution: TransactionExecutionOutput = Faker.fake(); execution.result = ExecutionResult::Success; // Create an ERC20 Trace log with mock gasLeft value let mut erc20_log: Log = Faker.fake(); erc20_log.topic0 = Some(ERC20_TRACE_HASH.into()); - let execution_gas_left = vec![0u8; 32]; // Initial value all zeros + let execution_gas_left = vec![0u8; 32]; let mut log_data = Vec::with_capacity(execution_gas_left.len() + 32); log_data.extend_from_slice(&execution_gas_left); - log_data.extend_from_slice(&[99u8; 32]); // Add some additional data + log_data.extend_from_slice(&[99u8; 32]); erc20_log.data = log_data.into(); // Create a Balance Tracker Trace log let mut balance_log: Log = Faker.fake(); balance_log.topic0 = Some(BALANCE_TRACKER_TRACE_HASH.into()); - let balance_gas_left = vec![0u8; 32]; // Initial value all zeros + let balance_gas_left = vec![0u8; 32]; balance_log.data = balance_gas_left.into(); // Create a regular log (not one we're targeting) @@ -568,40 +631,33 @@ mod tests { execution.logs = vec![erc20_log, balance_log, regular_log.clone()]; - // Create receipt logs with different gasLeft values - let receipt_erc20_gas_left = vec![42u8; 32]; // Different value for comparison - let mut erc20_receipt_log = alloy_rpc_types_eth::Log::::default(); - let erc20_topics = vec![B256::from_slice(&ERC20_TRACE_HASH)]; - - let mut erc20_receipt_data = Vec::with_capacity(receipt_erc20_gas_left.len() + 32); - erc20_receipt_data.extend_from_slice(&receipt_erc20_gas_left); - erc20_receipt_data.extend_from_slice(&[99u8; 32]); // Match additional data - - erc20_receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(erc20_topics, alloy_primitives::Bytes::from(erc20_receipt_data)); + // Create stored logs with different gasLeft values + let receipt_erc20_gas_left = vec![42u8; 32]; + let mut stored_erc20_log: Log = Faker.fake(); + stored_erc20_log.topic0 = Some(ERC20_TRACE_HASH.into()); + let mut stored_erc20_data = Vec::with_capacity(receipt_erc20_gas_left.len() + 32); + stored_erc20_data.extend_from_slice(&receipt_erc20_gas_left); + stored_erc20_data.extend_from_slice(&[99u8; 32]); + stored_erc20_log.data = stored_erc20_data.into(); - // Balance tracker receipt log let receipt_balance_gas_left = vec![24u8; 32]; - let mut balance_receipt_log = alloy_rpc_types_eth::Log::::default(); - let balance_topics = vec![B256::from_slice(&BALANCE_TRACKER_TRACE_HASH)]; - balance_receipt_log.inner.data = - alloy_primitives::LogData::new_unchecked(balance_topics, alloy_primitives::Bytes::from(receipt_balance_gas_left.clone())); - - // Regular log for receipt - let mut regular_receipt_log = alloy_rpc_types_eth::Log::::default(); - let regular_topics = Vec::new(); - regular_receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(regular_topics, alloy_primitives::Bytes::default()); - - // Create receipt with these logs - let mut receipt: ExternalReceipt = Faker.fake(); - if let alloy_consensus::ReceiptEnvelope::Legacy(r) = &mut receipt.0.inner { - r.receipt.status = alloy_consensus::Eip658Value::Eip658(true); - r.receipt.logs = vec![erc20_receipt_log.clone(), balance_receipt_log.clone(), regular_receipt_log.clone()]; - } else { - panic!("expected be legacy!") - } + let mut stored_balance_log: Log = Faker.fake(); + stored_balance_log.topic0 = Some(BALANCE_TRACKER_TRACE_HASH.into()); + stored_balance_log.data = receipt_balance_gas_left.clone().into(); + + // Regular stored log (topic0 will not match the target hashes) + let stored_regular_log: Log = Faker.fake(); + + let stored = TransactionExecutionResult { + result: ExecutionResult::Success, + output: Bytes::default(), + logs: vec![stored_erc20_log, stored_balance_log, stored_regular_log], + gas_used: Gas::default(), + deployed_contract_address: None, + }; // Apply the fix - execution.fix_logs_gas_left(&receipt); + execution.fix_logs_gas_left_from_stored(&stored.logs); // Verify the first 32 bytes of ERC20 log data was overwritten let updated_erc20_data = execution.logs[0].data.as_ref(); @@ -610,53 +666,9 @@ mod tests { assert_eq!(&updated_erc20_data[32..], &[99u8; 32]); // Verify the first 32 bytes of Balance Tracker log data was overwritten - assert_eq!(execution.logs[1].data.as_ref(), &receipt_balance_gas_left[..]); + assert_eq!(execution.logs[1].data.as_ref()[..32].to_vec(), receipt_balance_gas_left); - // Verify regular log data wasn't modified + // Verify regular log data was not modified assert_eq!(execution.logs[2].data, regular_log.data); } - - #[test] - fn test_apply_receipt() { - // Create a mock sender account with balance - let sender_address: Address = Faker.fake(); - let sender = Account { - address: sender_address, - nonce: Nonce::from(1u64), - balance: Wei::from(1000u64), - bytecode: None, - }; - - // Create a mock execution - let mut execution: TransactionExecutionOutput = Faker.fake(); - - // Set up execution with sender account - let mut sender_changes = AccountChanges::default(); - sender_changes.apply_original(sender); - let mut accounts = HashMap::with_hasher(foldhash::fast::RandomState::default()); - accounts.insert(sender_address, sender_changes); - let changes = State:: { - accounts, - ..Default::default() - }; - execution.state = changes; - execution.gas_used = Gas::from(100u64); - - // Create a receipt with higher gas used and execution cost - let mut receipt: ExternalReceipt = Faker.fake(); - receipt.0.from = sender_address.into(); - receipt.0.gas_used = 100u64; // Higher gas - - // Make sure transaction has a cost - let gas_price = Wei::from(1u64); - receipt.0.effective_gas_price = gas_price.try_into().expect("wei was created with u64 which fits u128 qed."); - - // Apply receipt - execution.apply_receipt(&receipt).unwrap(); - - // Verify sender balance was reduced by execution cost - let sender_changes = execution.state.accounts.get(&sender_address).unwrap(); - let modified_balance = *sender_changes.balance.value(); - assert_eq!(modified_balance, Wei::from(900u64)); // 1000 - 100 - } } diff --git a/src/eth/executor/mod.rs b/src/eth/executor/mod.rs index 1aeae0268..7f54a663a 100644 --- a/src/eth/executor/mod.rs +++ b/src/eth/executor/mod.rs @@ -7,8 +7,6 @@ pub mod types; use std::mem; use std::sync::Arc; -#[cfg(feature = "metrics")] -use alloy_consensus::Transaction; use alloy_rpc_types_trace::geth::GethDebugTracerType; use alloy_rpc_types_trace::geth::GethDebugTracingOptions; use alloy_rpc_types_trace::geth::GethTrace; @@ -40,23 +38,21 @@ use crate::eth::executor::evm::types::InspectorInput; use crate::eth::executor::evm_worker_pool::EvmWorkerPool; use crate::eth::executor::transaction_worker::TransactionWorker; use crate::eth::executor::types::EvmRoute; +use crate::eth::executor::types::state::Complete; use crate::eth::miner::Miner; use crate::eth::storage::ExecutionKind; use crate::eth::storage::StorageError; use crate::eth::storage::StratusStorage; use crate::eth::types::Address; +use crate::eth::types::Block; use crate::eth::types::BlockNumber; use crate::eth::types::CallInput; -use crate::eth::types::ExternalBlock; -use crate::eth::types::ExternalReceipt; -use crate::eth::types::ExternalReceipts; -use crate::eth::types::ExternalTransaction; use crate::eth::types::Hash; use crate::eth::types::PointInTime; use crate::eth::types::StratusError; use crate::eth::types::TransactionInput; -#[cfg(feature = "metrics")] -use crate::ext::OptionExt; +use crate::eth::types::TransactionMined; +use crate::eth::types::Wei; use crate::ext::to_json_string; use crate::infra::tracing::SpanExt; use crate::utils::Semaphore; @@ -97,122 +93,65 @@ impl Executor { } } - // ------------------------------------------------------------------------- - // External transactions - // ------------------------------------------------------------------------- - - /// Reexecutes an external block locally and imports it to the temporary storage. - /// - /// Returns the remaining receipts that were not consumed by the execution. - #[timed(executor_external_block)] - pub fn execute_external_block(&self, mut block: ExternalBlock, mut receipts: ExternalReceipts) -> anyhow::Result<()> { + /// Reexecutes an imported stratus block locally and imports it to the temporary storage. + #[timed(executor_imported_block)] + pub fn execute_imported_block(&self, mut block: Block) -> anyhow::Result<()> { #[cfg(feature = "tracing")] - let _span = info_span!("executor::external_block", block_number = %block.number()).entered(); - tracing::info!(block_number = %block.number(), "reexecuting external block"); - - self.storage.set_pending_from_external(&block); + let _span = info_span!("executor::imported_block", block_number = %block.number()).entered(); + tracing::info!(block_number = %block.number(), "reexecuting imported block"); // track pending block + self.storage.set_pending_header(block.number(), block.timestamp()); let block_number = block.number(); let block_transactions = mem::take(&mut block.transactions); // determine how to execute each transaction - for tx in block_transactions.into_transactions() { - let receipt = receipts.try_remove(tx.hash())?; - self.execute_external_transaction(tx, receipt, block_number)?; + for tx in block_transactions { + self.execute_imported_transaction(tx, block_number)?; } Ok(()) } - /// Reexecutes an external transaction locally ensuring it produces the same output. - /// - /// This function wraps `reexecute_external_tx_inner` and returns back the payload - /// to facilitate re-execution of parallel transactions that failed - #[timed(executor_external_transaction, labels( - contract = |tx| codegen::contract_name(&tx.0.to().map_into()), - function = |tx| codegen::function_sig(tx.inner.input()) + /// Reexecutes an imported stratus transaction locally ensuring it produces the same output. + #[timed(executor_imported_transaction, labels( + contract = |tx| codegen::contract_name(&tx.execution.input.to), + function = |tx| codegen::function_sig(tx.execution.input.data.as_ref()) ) )] - fn execute_external_transaction(&self, tx: ExternalTransaction, receipt: ExternalReceipt, block_number: BlockNumber) -> anyhow::Result<()> { + fn execute_imported_transaction(&self, tx: TransactionMined, block_number: BlockNumber) -> anyhow::Result<()> { #[cfg(feature = "tracing")] - let _span = info_span!("executor::external_transaction", tx_hash = %tx.hash()).entered(); - tracing::info!(%block_number, tx_hash = %tx.hash(), "reexecuting external transaction"); + let _span = info_span!("executor::imported_transaction", tx_hash = %tx.execution.info.hash).entered(); + tracing::info!(%block_number, tx_hash = %tx.execution.info.hash, "reexecuting imported transaction"); - self.transaction_worker.execute_external_transaction(tx, receipt, block_number) + self.transaction_worker.execute_imported_transaction(tx, block_number) } - fn execute_external_transaction_inner( + fn execute_imported_transaction_inner( storage: &StratusStorage, miner: &Miner, evm: &mut Evm, - tx: ExternalTransaction, - receipt: ExternalReceipt, + tx: TransactionMined, block_number: BlockNumber, ) -> anyhow::Result<()> { - let tx_input: TransactionInput = tx.try_into()?; - let pending_header = storage.read_pending_block_header(); - let mut evm_input = TransactionExecutionInput::create(&tx_input, pending_header); - - // when transaction externally failed, create fake transaction instead of reexecuting - let (tx_execution, state) = match receipt.is_success() { - // successful external transaction, re-execute locally - true => { - // re-execute transaction - let evm_execution = evm - .execute(evm_input.clone()) - .and_then(|(result, metrics)| Ok((TransactionExecutionOutput::try_from(result)?, metrics))); - - // handle re-execution result - let (mut evm_result, _evm_metrics) = match evm_execution { - Ok((evm_result, evm_metrics)) => (evm_result, evm_metrics), - Err(e) => { - let json_tx = to_json_string(&tx_input); - let json_receipt = to_json_string(&receipt); - tracing::error!(reason = ?e, %block_number, tx_hash = %tx_input.transaction_info.hash, %json_tx, %json_receipt, "failed to reexecute external transaction"); - return Err(e.into()); - } - }; - - // update execution with receipt - evm_result.apply_receipt(&receipt)?; - - // ensure it matches receipt before saving - if let Err(e) = evm_result.compare_with_receipt(&receipt) { - let json_tx = to_json_string(&tx_input); - let json_receipt = to_json_string(&receipt); - let json_execution_logs = to_json_string(&evm_result.logs); - tracing::error!(reason = ?e, %block_number, tx_hash = %tx_input.transaction_info.hash, %json_tx, %json_receipt, %json_execution_logs, "failed to reexecute external transaction"); - return Err(e); - }; - - ( - TransactionExecution::new(tx_input.transaction_info, tx_input.signature, evm_input, evm_result.outcome), - evm_result.state, - ) - } - // - // failed external transaction, re-create from receipt without re-executing - false => { - let (sender, _) = storage.read_account(receipt.from.into(), ExecutionKind::Transaction)?; - if tx_input.execution_info.nonce != sender.nonce { - bail!( - "reverted external transaction should have the correct nonce. address: {:?}, input: {:?}, sender: {:?}", - tx_input.signer(), - tx_input.execution_info.nonce, - sender.nonce - ); - } - let evm_result = TransactionExecutionOutput::from_failed_external_transaction(sender, &receipt)?; - - evm_input.gas_limit = tx_input.execution_info.gas_limit; - evm_input.gas_price = tx_input.execution_info.gas_price; - - ( - TransactionExecution::new(tx_input.transaction_info, tx_input.signature, evm_input, evm_result.outcome), - evm_result.state, - ) - } + let tx_input = tx.execution.transaction_input(); + let TransactionExecution { + input: stored_input, + output: stored_output, + .. + } = tx.execution; + + let recovered = tx_input.recover_signer_address()?; + + let mut evm_input = stored_input; + evm_input.from = recovered; + let gas_price = Wei::from(evm_input.gas_price); + + let (tx_execution, state) = match stored_output.result.is_success() { + // successful imported transaction, re-execute locally + true => Self::reexecute_imported_transaction(evm, &tx_input, evm_input, &stored_output, gas_price, recovered, block_number)?, + // failed imported transaction, re-create from the stored execution without re-executing + false => Self::recreate_failed_imported_transaction(storage, &tx_input, evm_input, &stored_output, gas_price, recovered)?, }; // persist state @@ -220,6 +159,82 @@ impl Executor { Ok(()) } + /// Reexecutes a successful imported transaction and validates it against the stored execution. + fn reexecute_imported_transaction( + evm: &mut Evm, + tx_input: &TransactionInput, + evm_input: TransactionExecutionInput, + stored_output: &TransactionExecutionResult, + gas_price: Wei, + recovered: Address, + block_number: BlockNumber, + ) -> anyhow::Result<(TransactionExecution, State)> { + let tx_hash = tx_input.transaction_info.hash; + + // re-execute transaction + let evm_execution = evm + .execute(evm_input.clone()) + .and_then(|(result, metrics)| Ok((TransactionExecutionOutput::try_from(result)?, metrics))); + + // handle re-execution result + let (mut evm_result, _evm_metrics) = match evm_execution { + Ok((evm_result, evm_metrics)) => (evm_result, evm_metrics), + Err(e) => { + let json_tx = to_json_string(tx_input); + let json_stored = to_json_string(stored_output); + tracing::error!(reason = ?e, %block_number, tx_hash = %tx_hash, %json_tx, %json_stored, "failed to reexecute imported transaction"); + return Err(e.into()); + } + }; + + // update execution with the stored execution + evm_result.apply_imported(stored_output, gas_price, recovered)?; + + // ensure it matches the stored execution before saving + if let Err(e) = evm_result.compare_with_imported(tx_hash, stored_output) { + let json_tx = to_json_string(tx_input); + let json_stored = to_json_string(stored_output); + let json_execution_logs = to_json_string(&evm_result.logs); + tracing::error!(reason = ?e, %block_number, tx_hash = %tx_hash, %json_tx, %json_stored, %json_execution_logs, "failed to reexecute imported transaction"); + return Err(e); + }; + + Ok(( + TransactionExecution::new(tx_input.transaction_info, tx_input.signature, evm_input, evm_result.outcome), + evm_result.state, + )) + } + + /// Re-creates a failed imported transaction from the stored execution without re-executing. + fn recreate_failed_imported_transaction( + storage: &StratusStorage, + tx_input: &TransactionInput, + mut evm_input: TransactionExecutionInput, + stored_output: &TransactionExecutionResult, + gas_price: Wei, + recovered: Address, + ) -> anyhow::Result<(TransactionExecution, State)> { + let (sender, _) = storage.read_account(recovered, ExecutionKind::Transaction)?; + if tx_input.execution_info.nonce != sender.nonce { + bail!( + "reverted imported transaction should have the correct nonce. address: {:?}, input: {:?}, sender: {:?}", + tx_input.signer(), + tx_input.execution_info.nonce, + sender.nonce + ); + } + + let evm_result = TransactionExecutionOutput::from_failed_imported_transaction(sender, gas_price, stored_output)?; + + evm_input.gas_limit = tx_input.execution_info.gas_limit; + evm_input.gas_price = tx_input.execution_info.gas_price; + + Ok(( + TransactionExecution::new(tx_input.transaction_info, tx_input.signature, evm_input, evm_result.outcome), + evm_result.state, + )) + } + // ------------------------------------------------------------------------- // Local transactions // ------------------------------------------------------------------------- diff --git a/src/eth/executor/transaction_worker.rs b/src/eth/executor/transaction_worker.rs index 81cae68df..1742389ab 100644 --- a/src/eth/executor/transaction_worker.rs +++ b/src/eth/executor/transaction_worker.rs @@ -24,17 +24,16 @@ use crate::eth::miner::Miner; use crate::eth::storage::StorageError; use crate::eth::storage::StratusStorage; use crate::eth::types::BlockNumber; -use crate::eth::types::ExternalReceipt; -use crate::eth::types::ExternalTransaction; use crate::eth::types::StratusError; use crate::eth::types::TransactionInput; +use crate::eth::types::TransactionMined; use crate::eth::types::UnexpectedError; use crate::ext::spawn_thread; use crate::infra::tracing::warn_task_tx_closed; const TASK_NAME: &str = "evm-tx-1"; -type ExternalTransactionResult = anyhow::Result<()>; +type ImportedTransactionResult = anyhow::Result<()>; type LocalTransactionResult = Result; type LocalTransactionResponse = (Duration, LocalTransactionResult); @@ -67,11 +66,11 @@ impl TransactionWorker { Self { task_tx } } - /// Reexecutes and persists an external transaction. - pub fn execute_external_transaction(&self, tx: ExternalTransaction, receipt: ExternalReceipt, block_number: BlockNumber) -> ExternalTransactionResult { + /// Reexecutes and persists an imported stratus transaction. + pub fn execute_imported_transaction(&self, tx: TransactionMined, block_number: BlockNumber) -> ImportedTransactionResult { let (response_tx, response_rx) = oneshot::channel(); self.task_tx - .send(TransactionTask::external(tx, receipt, block_number, response_tx)) + .send(TransactionTask::imported(tx, block_number, response_tx)) .map_err(StratusError::from)?; match response_rx.recv() { Ok(result) => result, @@ -150,23 +149,22 @@ struct TransactionTask { } impl TransactionTask { - fn external(tx: ExternalTransaction, receipt: ExternalReceipt, block_number: BlockNumber, response_tx: oneshot::Sender) -> Self { + fn local(tx_input: TransactionInput, response_tx: oneshot::Sender) -> Self { Self { span: Span::current(), - kind: TransactionTaskKind::External { - tx: Box::new(tx), - receipt: Box::new(receipt), - block_number, + kind: TransactionTaskKind::Local { + tx_input: Box::new(tx_input), response_tx, }, } } - fn local(tx_input: TransactionInput, response_tx: oneshot::Sender) -> Self { + fn imported(tx: TransactionMined, block_number: BlockNumber, response_tx: oneshot::Sender) -> Self { Self { span: Span::current(), - kind: TransactionTaskKind::Local { - tx_input: Box::new(tx_input), + kind: TransactionTaskKind::Imported { + tx: Box::new(tx), + block_number, response_tx, }, } @@ -177,17 +175,6 @@ impl TransactionTask { let _enter = span.enter(); catch_unwind(AssertUnwindSafe(|| match kind { - TransactionTaskKind::External { - tx, - receipt, - block_number, - response_tx, - } => { - let result = Executor::execute_external_transaction_inner(storage, miner, evm, *tx, *receipt, block_number); - if let Err(e) = response_tx.send(result) { - tracing::error!(reason = ?e, "failed to send external transaction execution result"); - } - } TransactionTaskKind::Local { tx_input, response_tx } => { let start = stratus_metrics::now(); let result = TransactionWorker::execute_local_transaction_attempts(storage, miner, evm, *tx_input, usize::MAX); @@ -196,20 +183,25 @@ impl TransactionTask { tracing::error!(reason = ?e, "failed to send local transaction execution result"); } } + TransactionTaskKind::Imported { tx, block_number, response_tx } => { + let result = Executor::execute_imported_transaction_inner(storage, miner, evm, *tx, block_number); + if let Err(e) = response_tx.send(result) { + tracing::error!(reason = ?e, "failed to send imported transaction execution result"); + } + } })) .map_err(|err| ExecutorError::Panic { err: anyhow!("{err:?}") }.into()) } } enum TransactionTaskKind { - External { - tx: Box, - receipt: Box, - block_number: BlockNumber, - response_tx: oneshot::Sender, - }, Local { tx_input: Box, response_tx: oneshot::Sender, }, + Imported { + tx: Box, + block_number: BlockNumber, + response_tx: oneshot::Sender, + }, } diff --git a/src/eth/executor/types/transaction_execution.rs b/src/eth/executor/types/transaction_execution.rs index ff3af332f..aa266f3f0 100644 --- a/src/eth/executor/types/transaction_execution.rs +++ b/src/eth/executor/types/transaction_execution.rs @@ -30,6 +30,15 @@ pub struct TransactionExecution { } impl TransactionExecution { + /// Rebuilds the transaction input that produced this execution. + pub fn transaction_input(&self) -> TransactionInput { + TransactionInput { + transaction_info: self.info, + execution_info: self.input.clone().into(), + signature: self.signature, + } + } + pub fn create_alloy_logs(&self) -> Vec { self.logs() .iter() diff --git a/src/eth/follower/importer/fetchers/block_with_receipts.rs b/src/eth/follower/importer/fetchers/block_with_receipts.rs index e710a75bb..d10b26556 100644 --- a/src/eth/follower/importer/fetchers/block_with_receipts.rs +++ b/src/eth/follower/importer/fetchers/block_with_receipts.rs @@ -1,74 +1,37 @@ use std::sync::Arc; -use alloy_rpc_types_eth::BlockTransactions; -use anyhow::anyhow; -use anyhow::bail; - use crate::eth::follower::importer::BlockchainClient; use crate::eth::follower::importer::fetch_with_retry; use crate::eth::follower::importer::fetchers::DataFetcher; +use crate::eth::types::Block; use crate::eth::types::BlockNumber; -use crate::eth::types::ExternalBlock; -use crate::eth::types::ExternalReceipt; pub struct BlockWithReceiptsFetcher { pub chain: Arc, } impl DataFetcher for BlockWithReceiptsFetcher { - type FetchedType = (ExternalBlock, Vec); - type PostProcessType = (ExternalBlock, Vec); + type FetchedType = Block; + type PostProcessType = Block; async fn fetch(&self, block_number: BlockNumber) -> Self::FetchedType { let fetch_fn = |bn| { let chain = Arc::clone(&self.chain); - async move { - chain - .fetch_block_and_receipts(bn) - .await - .map(|opt| opt.map(|response| (response.block, response.receipts))) - } + async move { chain.fetch_block_and_receipts(bn).await } }; fetch_with_retry(block_number, fetch_fn, "block and receipts").await } - async fn post_process(&self, data: Self::FetchedType) -> anyhow::Result { - let (mut block, mut receipts) = data; - let block_number = block.number(); - let BlockTransactions::Full(transactions) = &mut block.transactions else { - bail!("expected full transactions, got hashes or uncle"); - }; - - if transactions.len() != receipts.len() { - bail!( - "block {} has mismatched transaction and receipt length: {} transactions but {} receipts", - block_number, - transactions.len(), - receipts.len() - ); - } - - // Stably sort transactions and receipts by transaction_index - transactions.sort_by_key(|a| a.transaction_index); - receipts.sort_by_key(|a| a.transaction_index); - - // perform additional checks on the transaction index - for window in transactions.windows(2) { - let tx_index = window[0].transaction_index.ok_or(anyhow!("missing transaction index"))? as u32; - let next_tx_index = window[1].transaction_index.ok_or(anyhow!("missing transaction index"))? as u32; + async fn post_process(&self, block: Self::FetchedType) -> anyhow::Result { + for window in block.transactions.windows(2) { + let tx_index = window[0].mined_data.index.0; + let next_tx_index = window[1].mined_data.index.0; if tx_index + 1 != next_tx_index { tracing::error!(tx_index, next_tx_index, "two consecutive transactions must have consecutive indices"); } } - for window in receipts.windows(2) { - let tx_index = window[0].transaction_index.ok_or(anyhow!("missing transaction index"))? as u32; - let next_tx_index = window[1].transaction_index.ok_or(anyhow!("missing transaction index"))? as u32; - if tx_index + 1 != next_tx_index { - tracing::error!(tx_index, next_tx_index, "two consecutive receipts must have consecutive indices"); - } - } - Ok((block, receipts)) + Ok(block) } } diff --git a/src/eth/follower/importer/importers/blockchain_client.rs b/src/eth/follower/importer/importers/blockchain_client.rs index 4a24c36d6..523034481 100644 --- a/src/eth/follower/importer/importers/blockchain_client.rs +++ b/src/eth/follower/importer/importers/blockchain_client.rs @@ -18,7 +18,7 @@ use tokio::sync::RwLockReadGuard; use crate::GlobalState; use crate::alias::AlloyBytes; -use crate::alias::AlloyTransaction; +use crate::alias::AlloyHeader; use crate::alias::JsonValue; use crate::eth::executor::AccessListOutput; use crate::eth::executor::ExecutorError; @@ -26,10 +26,8 @@ use crate::eth::rpc::pagination; use crate::eth::storage::permanent::rocks::types::BlockChangesRocksdb; use crate::eth::storage::permanent::rocks::types::BlockRocksdb; use crate::eth::types::Address; +use crate::eth::types::Block; use crate::eth::types::BlockNumber; -use crate::eth::types::ExternalBlock; -use crate::eth::types::ExternalBlockWithReceipts; -use crate::eth::types::ExternalReceipt; use crate::eth::types::Hash; use crate::eth::types::StratusError; use crate::eth::types::Wei; @@ -196,7 +194,7 @@ impl BlockchainClient { let Some(full) = self.fetch_serialized_response(method, block_number).await? else { return Ok(None); // block not available yet }; - let value = serde_json::from_str(full.get()).with_context(|| format!("failed to deserialize importer data from {method}"))?; + let value = serde_json::from_str::(full.get()).with_context(|| format!("failed to deserialize importer data from {method}"))?; Ok(Some(value)) } @@ -263,11 +261,13 @@ impl BlockchainClient { } /// Fetches a block by number with receipts. - pub async fn fetch_block_and_receipts(&self, block_number: BlockNumber) -> anyhow::Result> { + pub async fn fetch_block_and_receipts(&self, block_number: BlockNumber) -> anyhow::Result> { tracing::debug!(%block_number, "fetching block"); - match self.request_importer_data("stratus_getBlockAndReceipts", block_number).await { - Ok(block) => Ok(block), + const METHOD: &str = "stratus_getBlockAndReceipts"; + + match self.request_importer_data::(METHOD, block_number).await { + Ok(block) => Ok(block.map(|block| block.into())), Err(e) => log_and_err!(reason = e, "failed to fetch block with receipts"), } } @@ -282,48 +282,6 @@ impl BlockchainClient { } } - /// Fetches a block by number. - pub async fn fetch_block(&self, block_number: BlockNumber) -> anyhow::Result> { - tracing::debug!(%block_number, "fetching block"); - - let number = to_json_value(block_number); - let result = self - .http - .request::, _>("eth_getBlockByNumber", [number, JsonValue::Bool(true)]) - .await; - - match result { - Ok(block) => Ok(block), - Err(e) => log_and_err!(reason = e, "failed to fetch block by number"), - } - } - - /// Fetches a transaction by hash. - pub async fn fetch_transaction(&self, tx_hash: Hash) -> anyhow::Result> { - tracing::debug!(%tx_hash, "fetching transaction"); - - let hash = to_json_value(tx_hash); - let result = self.http.request::, _>("eth_getTransactionByHash", [hash]).await; - - match result { - Ok(tx) => Ok(tx), - Err(e) => log_and_err!(reason = e, "failed to fetch transaction by hash"), - } - } - - /// Fetches a receipt by hash. - pub async fn fetch_receipt(&self, tx_hash: Hash) -> anyhow::Result> { - tracing::debug!(%tx_hash, "fetching transaction receipt"); - - let hash = to_json_value(tx_hash); - let result = self.http.request::, _>("eth_getTransactionReceipt", [hash]).await; - - match result { - Ok(receipt) => Ok(receipt), - Err(e) => log_and_err!(reason = e, "failed to fetch transaction receipt by hash"), - } - } - /// Fetches account balance by address and block number. pub async fn fetch_balance(&self, address: Address, block_number: Option) -> anyhow::Result { tracing::debug!(%address, block_number = %block_number.or_empty(), "fetching account balance"); @@ -365,7 +323,7 @@ impl BlockchainClient { // RPC subscriptions // ------------------------------------------------------------------------- - pub async fn subscribe_new_heads(&self) -> anyhow::Result> { + pub async fn subscribe_new_heads(&self) -> anyhow::Result> { const TASK_NAME: &str = "blockchain::subscribe_new_heads"; tracing::debug!("subscribing to newHeads event"); @@ -377,7 +335,7 @@ impl BlockchainClient { let ws_read = self.require_ws().await?; let result = ws_read - .subscribe::("eth_subscribe", [JsonValue::String("newHeads".to_owned())], "eth_unsubscribe") + .subscribe::("eth_subscribe", [JsonValue::String("newHeads".to_owned())], "eth_unsubscribe") .await; match result { diff --git a/src/eth/follower/importer/importers/execution.rs b/src/eth/follower/importer/importers/execution.rs index 2819c83d9..3861e518c 100644 --- a/src/eth/follower/importer/importers/execution.rs +++ b/src/eth/follower/importer/importers/execution.rs @@ -9,9 +9,7 @@ use crate::eth::follower::importer::importers::ImporterWorker; use crate::eth::follower::importer::send_block_to_kafka; use crate::eth::miner::Miner; use crate::eth::miner::miner::CommitItem; -use crate::eth::types::ExternalBlock; -use crate::eth::types::ExternalReceipt; -use crate::eth::types::ExternalReceipts; +use crate::eth::types::Block; use crate::infra::kafka::KafkaConnector; use crate::log_and_err; @@ -23,31 +21,31 @@ pub struct ReexecutionWorker { impl ImportData for ::DataType { fn block_number(&self) -> crate::eth::types::BlockNumber { - self.0.number() + self.number() } } impl ImporterWorker for ReexecutionWorker { - type DataType = (ExternalBlock, Vec); + type DataType = Block; #[timed(import_online_mined_block)] - async fn import(&self, (block, receipts): Self::DataType) -> anyhow::Result { + async fn import(&self, block: Self::DataType) -> anyhow::Result { const TASK_NAME: &str = "block-executor"; - let receipts_len = receipts.len(); + let receipts_len = block.transactions.len(); - if let Err(e) = self.executor.execute_external_block(block.clone(), ExternalReceipts::from(receipts)) { - let message = GlobalState::shutdown_from(TASK_NAME, "failed to reexecute external block"); + if let Err(e) = self.executor.execute_imported_block(block.clone()) { + let message = GlobalState::shutdown_from(TASK_NAME, "failed to reexecute imported block"); return log_and_err!(reason = e, message); }; - let (mined_block, changes) = match self.miner.mine_external(block) { + let (mined_block, changes) = match self.miner.mine_imported(block) { Ok((mined_block, changes)) => { - tracing::info!(number = %mined_block.number(), "mined external block"); + tracing::info!(number = %mined_block.number(), "mined imported block"); (mined_block, changes) } Err(e) => { - let message = GlobalState::shutdown_from(TASK_NAME, "failed to mine external block"); + let message = GlobalState::shutdown_from(TASK_NAME, "failed to mine imported block"); return log_and_err!(reason = e, message); } }; @@ -56,10 +54,10 @@ impl ImporterWorker for ReexecutionWorker { match self.miner.commit(CommitItem::Block(mined_block), changes) { Ok(_) => { - tracing::info!("committed external block"); + tracing::info!("committed imported block"); } Err(e) => { - let message = GlobalState::shutdown_from(TASK_NAME, "failed to commit external block"); + let message = GlobalState::shutdown_from(TASK_NAME, "failed to commit imported block"); return log_and_err!(reason = e, message); } } diff --git a/src/eth/follower/importer/importers/fake_leader.rs b/src/eth/follower/importer/importers/fake_leader.rs index 412419f90..123a838aa 100644 --- a/src/eth/follower/importer/importers/fake_leader.rs +++ b/src/eth/follower/importer/importers/fake_leader.rs @@ -14,6 +14,7 @@ use crate::eth::miner::Miner; use crate::eth::miner::miner::interval_miner::commit_retry; use crate::eth::storage::StratusStorage; use crate::eth::types::StratusError; +use crate::eth::types::TransactionInput; pub struct FakeLeaderWorker { pub executor: Arc, @@ -23,7 +24,7 @@ pub struct FakeLeaderWorker { impl ImportData for ::DataType { fn block_number(&self) -> crate::eth::types::BlockNumber { - self.0.block_number() + self.0.number() } } @@ -31,12 +32,21 @@ impl ImporterWorker for FakeLeaderWorker { type DataType = ::PostProcessType; #[timed(import_online_mined_block)] - async fn import(&self, ((block, _), (expected_block, expected_changes)): Self::DataType) -> anyhow::Result { + async fn import(&self, (mut block, (expected_block, expected_changes)): Self::DataType) -> anyhow::Result { let block_tx_len = block.transactions.len(); - self.storage.set_pending_from_external(&block); - for tx in block.0.transactions.into_transactions() { + self.storage.set_pending_header(block.number(), block.timestamp()); + let transactions = std::mem::take(&mut block.transactions) + .into_iter() + .map(|tx| -> anyhow::Result { + let tx_input = tx.execution.transaction_input(); + tx_input.recover_signer_address()?; + Ok(tx_input) + }) + .collect::>>()?; + + for tx in transactions { tracing::info!(?tx, "executing tx as fake miner"); - if let Err(e) = self.executor.execute_local_transaction(tx.try_into()?, None) { + if let Err(e) = self.executor.execute_local_transaction(tx, None) { match e { StratusError::Executor(ExecutorError::Nonce { transaction: _, account: _ }) => { tracing::warn!(reason = ?e, "transaction failed, was this node restarted?"); diff --git a/src/eth/follower/importer/mod.rs b/src/eth/follower/importer/mod.rs index cf0ffa68a..c07c5d9c8 100644 --- a/src/eth/follower/importer/mod.rs +++ b/src/eth/follower/importer/mod.rs @@ -155,8 +155,8 @@ async fn start_number_fetcher(chain: Arc, sync_interval: Durat tracing::info!("{} awaiting block number from newHeads subscription", TASK_NAME); match timeout(TIMEOUT_NEW_HEADS, sub.next()).await { Ok(Some(Ok(block))) => { - tracing::info!(block_number = %block.number(), "{} received newHeads event", TASK_NAME); - set_external_rpc_current_block(block.number()); + tracing::info!(block_number = %block.number, "{} received newHeads event", TASK_NAME); + set_external_rpc_current_block(BlockNumber::from(block.number)); continue; } Ok(None) => diff --git a/src/eth/miner/miner.rs b/src/eth/miner/miner.rs index 9f3575b76..978df6493 100644 --- a/src/eth/miner/miner.rs +++ b/src/eth/miner/miner.rs @@ -23,7 +23,6 @@ use crate::eth::storage::StorageError; use crate::eth::storage::StratusStorage; use crate::eth::types::Block; use crate::eth::types::BlockHeader; -use crate::eth::types::ExternalBlock; use crate::eth::types::Hash; use crate::eth::types::LogMessage; use crate::eth::types::StratusError; @@ -241,13 +240,13 @@ impl Miner { Ok(()) } - /// Mines external block and external transactions. + /// Mines an imported stratus block and its reexecuted transactions. /// /// Local transactions are not allowed to be part of the block. - pub fn mine_external(&self, external_block: ExternalBlock) -> anyhow::Result<(Block, State)> { + pub fn mine_imported(&self, imported_block: Block) -> anyhow::Result<(Block, State)> { // track #[cfg(feature = "tracing")] - let _span = info_span!("miner::mine_external", block_number = field::Empty).entered(); + let _span = info_span!("miner::mine_imported", block_number = field::Empty).entered(); // lock let _mine_lock = self.locks.mine.lock(); @@ -257,18 +256,18 @@ impl Miner { let mut block: Block = pending_block.into(); Span::with(|s| s.rec_str("block_number", &block.header.number)); - block.apply_external(&external_block); + block.apply_imported(&imported_block); - match external_block == block { + match block.number() == imported_block.number() && block.header.timestamp == imported_block.header.timestamp && block.hash() == imported_block.hash() { true => Ok((block, changes)), false => Err(anyhow!( - "mismatching block info:\n\tlocal:\n\t\tnumber: {:?}\n\t\ttimestamp: {:?}\n\t\thash: {:?}\n\texternal:\n\t\tnumber: {:?}\n\t\ttimestamp: {:?}\n\t\thash: {:?}", + "mismatching block info:\n\tlocal:\n\t\tnumber: {:?}\n\t\ttimestamp: {:?}\n\t\thash: {:?}\n\timported:\n\t\tnumber: {:?}\n\t\ttimestamp: {:?}\n\t\thash: {:?}", block.number(), block.header.timestamp, block.hash(), - external_block.number(), - external_block.timestamp(), - external_block.hash() + imported_block.number(), + imported_block.header.timestamp, + imported_block.hash() )), } } diff --git a/src/eth/rpc/mod.rs b/src/eth/rpc/mod.rs index 472fa9a26..9fea457d4 100644 --- a/src/eth/rpc/mod.rs +++ b/src/eth/rpc/mod.rs @@ -49,11 +49,27 @@ mod tests { use super::pagination::is_envelope; use super::pagination::parse_envelope; use super::pagination::parse_request; + use super::pagination::request_params; use super::pagination::respond; use super::types::RpcError; use crate::eth::types::StratusError; use crate::ext::InfallibleExt; + #[test] + fn parse_request_decodes_offset() { + let params = jsonrpsee::types::Params::new(Some(r#"["0x1", {"offset": 5}]"#)); + let mut sequence = params.sequence(); + sequence.optional_next::().expect("parse first").expect("present"); + let pagination = parse_request(sequence).expect("parse request").expect("present"); + assert_eq!(pagination.offset, 5); + } + + #[test] + fn request_params_wire_format() { + // the wire parameter is unchanged, so old leaders see the same bytes + assert_eq!(request_params(5), json!({"offset": 5})); + } + #[test] fn respond_without_pagination_is_byte_identical() { let value = json!({"block": "abc", "receipts": [1, 2, 3]}); @@ -284,10 +300,11 @@ mod wire_tests { use super::types::BlockFilter; use crate::alias::JsonValue; use crate::eth::follower::importer::BlockchainClient; + use crate::eth::storage::permanent::rocks::types::BlockRocksdb; + use crate::eth::types::Block; use crate::eth::types::BlockNumber; - use crate::eth::types::ExternalBlockWithReceipts; - use crate::eth::types::ExternalReceipt; use crate::eth::types::StratusError; + use crate::eth::types::TransactionMined; use crate::ext::to_json_value; use crate::utils::test_utils::fake_first; use crate::utils::test_utils::fake_list; @@ -295,17 +312,18 @@ mod wire_tests { /// Response limit for both leader and follower sides in the tests below. const MAX_RESPONSE_BYTES: u32 = 2048; - /// Builds an importer response well above the response limits. - fn big_block_with_receipts() -> ExternalBlockWithReceipts { - let mut block = fake_first::(); - block.receipts = fake_list::(200); + /// Builds an importer block well above the response limits, in the stratus-native format. + fn big_block() -> Block { + let mut block = fake_first::(); + block.header.number = BlockNumber::from(1u32); + block.transactions = fake_list::(200); block } /// Asserts the serialized form of the test value cannot fit in the response limits. #[test] fn test_value_is_oversized() { - let value = to_json_value(big_block_with_receipts()); + let value = to_json_value(BlockRocksdb::from(big_block())); let serialized = serde_json::to_string(&value).expect("serialize"); assert!( serialized.len() > MAX_RESPONSE_BYTES as usize, @@ -316,8 +334,9 @@ mod wire_tests { #[tokio::test] async fn oversized_importer_response_is_paginated_over_the_wire() { - let expected = big_block_with_receipts(); - let storage = Arc::new(RwLock::new(to_json_value(expected.clone()))); + let expected_rocks = BlockRocksdb::from(big_block()); + let expected: Block = expected_rocks.clone().into(); + let storage = Arc::new(RwLock::new(to_json_value(expected_rocks))); // leader with a tiny response limit, using the same handler shape as the real one let server_config = jsonrpsee::server::ServerConfig::builder().max_response_body_size(MAX_RESPONSE_BYTES).build(); @@ -342,13 +361,17 @@ mod wire_tests { let url = format!("http://{addr}"); let client = BlockchainClient::new_http(&url, Duration::from_secs(10)).await.expect("build client"); - let fetched = client.fetch_block_and_receipts(BlockNumber::from(1)).await.expect("fetch block"); - assert_eq!(fetched.expect("block present"), expected); + let fetched_block = client + .fetch_block_and_receipts(BlockNumber::from(1)) + .await + .expect("fetch block") + .expect("block present"); + assert_eq!(fetched_block, expected); } #[tokio::test] async fn old_leader_without_pagination_still_fails_as_before() { - let storage = Arc::new(RwLock::new(to_json_value(big_block_with_receipts()))); + let storage = Arc::new(RwLock::new(to_json_value(BlockRocksdb::from(big_block())))); // old leader: ignores the extra pagination parameter, returns the full response let server_config = jsonrpsee::server::ServerConfig::builder().max_response_body_size(MAX_RESPONSE_BYTES).build(); @@ -375,6 +398,38 @@ mod wire_tests { assert!(error.to_string().contains("failed to fetch block with receipts")); } + #[tokio::test] + async fn legacy_alloy_shape_fails_to_deserialize() { + // a leader still answering the pre-cleanup alloy shape now fails fast at deserialization + let alloy_response = serde_json::json!({ + "block": { "number": "0x1" }, + "receipts": [], + }); + let storage = Arc::new(RwLock::new(alloy_response)); + + let server_config = jsonrpsee::server::ServerConfig::builder().max_response_body_size(MAX_RESPONSE_BYTES).build(); + let server = Server::builder().set_config(server_config).build("127.0.0.1:0").await.expect("build server"); + let addr = server.local_addr().expect("server addr"); + + let mut module = RpcModule::new(Arc::clone(&storage)); + module + .register_method("net_listening", |_, _, _| Ok::<_, StratusError>(true)) + .expect("register net_listening"); + module + .register_method("stratus_getBlockAndReceipts", |_, storage, _| { + let value = storage.read().expect("read storage").clone(); + Ok(value) as Result + }) + .expect("register stratus_getBlockAndReceipts"); + let _server_handle = server.start(module); + + let url = format!("http://{addr}"); + let client = BlockchainClient::new_http(&url, Duration::from_secs(10)).await.expect("build client"); + + let error = client.fetch_block_and_receipts(BlockNumber::from(1)).await.expect_err("fetch should fail"); + assert!(format!("{error:#}").contains("failed to deserialize importer data")); + } + #[tokio::test] async fn null_response_means_block_not_available() { let storage = Arc::new(RwLock::new(JsonValue::Null)); @@ -468,4 +523,42 @@ mod wire_tests { let error = client.fetch_block_and_receipts(BlockNumber::from(1)).await.expect_err("fetch should fail"); assert!(format!("{error:?}").contains("expected paginated chunk but got normal response")); } + + #[tokio::test] + async fn stratus_native_response_round_trips_through_paginated_fetch() { + // a big block in the stratus-native format, above the response limits + let expected_rocks = BlockRocksdb::from(big_block()); + let expected: Block = expected_rocks.clone().into(); + let storage = Arc::new(RwLock::new(to_json_value(expected_rocks))); + + // leader with a tiny response limit, using the same handler shape as the real stratus branch + let server_config = jsonrpsee::server::ServerConfig::builder().max_response_body_size(MAX_RESPONSE_BYTES).build(); + let server = Server::builder().set_config(server_config).build("127.0.0.1:0").await.expect("build server"); + let addr = server.local_addr().expect("server addr"); + + let mut module = RpcModule::new(Arc::clone(&storage)); + module + .register_method("net_listening", |_, _, _| Ok::<_, StratusError>(true)) + .expect("register net_listening"); + module + .register_method("stratus_getBlockAndReceipts", |params, storage, _| { + let (sequence, _filter) = next_rpc_param::(params.sequence())?; + let pagination = parse_request(sequence)?; + let value = storage.read().expect("read storage").clone(); + respond(value, pagination, MAX_RESPONSE_BYTES) + }) + .expect("register stratus_getBlockAndReceipts"); + let _server_handle = server.start(module); + + // follower with a tiny response limit, like the importer uses + let url = format!("http://{addr}"); + let client = BlockchainClient::new_http(&url, Duration::from_secs(10)).await.expect("build client"); + + let fetched_block = client + .fetch_block_and_receipts(BlockNumber::from(1)) + .await + .expect("fetch block") + .expect("block present"); + assert_eq!(fetched_block, expected); + } } diff --git a/src/eth/rpc/pagination.rs b/src/eth/rpc/pagination.rs index 1aaa3e0b5..7ae1e3b4c 100644 --- a/src/eth/rpc/pagination.rs +++ b/src/eth/rpc/pagination.rs @@ -167,6 +167,8 @@ pub fn respond(value: JsonValue, pagination: Option, max_respo } /// Builds pagination request params for the follower side. +/// +/// The serialized params are byte-identical to the pre-format wire format (`{"offset": N}`). pub fn request_params(offset: u64) -> JsonValue { to_json_value(PaginationParams { offset }) } diff --git a/src/eth/rpc/server.rs b/src/eth/rpc/server.rs index 7251cb2f9..113ffc088 100644 --- a/src/eth/rpc/server.rs +++ b/src/eth/rpc/server.rs @@ -50,7 +50,6 @@ use tracing::info_span; use crate::GlobalState; use crate::NodeMode; -use crate::alias::AlloyReceipt; use crate::alias::JsonValue; use crate::config::StratusConfig; use crate::eth::codegen; @@ -88,6 +87,7 @@ use crate::eth::rpc::subscriptions::RpcSubscriptionsHandles; use crate::eth::storage::ExecutionKind; use crate::eth::storage::StorageError; use crate::eth::storage::StratusStorage; +use crate::eth::storage::permanent::rocks::types::BlockRocksdb; use crate::eth::types::Address; use crate::eth::types::Bytes; use crate::eth::types::CallInput; @@ -987,12 +987,8 @@ fn stratus_get_block_and_receipts(params: Params<'_>, ctx: Arc, ext: }; tracing::info!(%filter, "block with transactions found"); - let receipts = block.transactions.iter().cloned().map(AlloyReceipt::from).collect::>(); - let value = json!({ - "block": block.to_json_rpc_with_full_transactions(), - "receipts": receipts, - }); + let value = to_json_value(BlockRocksdb::from(block)); pagination::respond(value, pagination, ctx.server.rpc_config.rpc_max_response_size_bytes) } diff --git a/src/eth/storage/permanent/rocks/types/block.rs b/src/eth/storage/permanent/rocks/types/block.rs index 17ea78691..51e297dee 100644 --- a/src/eth/storage/permanent/rocks/types/block.rs +++ b/src/eth/storage/permanent/rocks/types/block.rs @@ -80,3 +80,63 @@ impl From for Block { } impl SerializeDeserializeWithContext for BlockRocksdb {} + +#[cfg(test)] +mod tests { + use super::BlockRocksdb; + use crate::eth::types::Block; + use crate::eth::types::BlockNumber; + use crate::eth::types::Index; + use crate::eth::types::Log; + use crate::eth::types::TransactionMined; + use crate::ext::to_json_value; + use crate::utils::test_utils::fake_first; + use crate::utils::test_utils::fake_list; + + /// Builds a block with a small number and transactions that have logs and small log indexes, + /// like a leader mines. Small values are required because the storage DTO narrows the block + /// number to `u32` and log indexes are derived from the log list. + fn sample_block() -> Block { + let mut block = fake_first::(); + block.header.number = BlockNumber::from(1u32); + block.transactions = fake_list::(3); + // give every transaction exactly two logs at a small, stable first log index, like the + // leader does, so mined data round-trips without depending on random fixture values + for (transaction_index, transaction) in block.transactions.iter_mut().enumerate() { + let transaction_index = (transaction_index as u64) * 2; + let first_log_index = Index::from(transaction_index * 10); + transaction.mined_data.index = Index::from(transaction_index); + transaction.mined_data.first_log_index = first_log_index; + let log = fake_first::(); + transaction.execution.output.logs = vec![log.clone(), log]; + } + block + } + + /// The stratus response format serializes a block through the storage DTO and JSON, exactly + /// like the leader serializes it and the follower deserializes it in `stratus_getBlockAndReceipts`. + #[test] + fn block_rocksdb_json_round_trip_is_lossless() { + let original = sample_block(); + + // first hop: the same serialization the leader performs in the RPC handler + let json = to_json_value(BlockRocksdb::from(original.clone())); + let first: Block = serde_json::from_value::(json).expect("deserialize from json").into(); + + // second hop must be a fixed point: nothing is canonicalized further + let second: Block = BlockRocksdb::from(first.clone()).into(); + assert_eq!(first, second); + + // the header is copied directly, field by field + assert_eq!(first.header, original.header); + + // mined data is rebuilt from the DTO fields, preserving the leader invariants + for (original_transaction, rebuilt_transaction) in original.transactions.iter().zip(first.transactions.iter()) { + assert_eq!(rebuilt_transaction.mined_data.index, original_transaction.mined_data.index); + assert_eq!(rebuilt_transaction.mined_data.first_log_index, original_transaction.mined_data.first_log_index); + // the block hash is rebuilt from the header the block was read with, not the stale + // fixture value, mirroring what the leader guarantees in real blocks + assert_eq!(rebuilt_transaction.mined_data.block_hash, original.header.hash); + } + } +} diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index d088be13e..f9f001ec2 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -30,7 +30,6 @@ use crate::eth::types::BlockInfo; use crate::eth::types::BlockNumber; #[cfg(feature = "dev")] use crate::eth::types::Bytes; -use crate::eth::types::ExternalBlock; use crate::eth::types::Hash; use crate::eth::types::LogMessage; #[cfg(feature = "dev")] @@ -133,10 +132,6 @@ impl StratusStorage { self.perm.read_mined_block_number() } - pub fn set_pending_from_external(&self, block: &ExternalBlock) { - self.temp.set_pending_header(block.number(), block.timestamp()); - } - pub fn set_pending_header(&self, number: BlockNumber, timestamp: UnixTime) { self.temp.set_pending_header(number, timestamp); } diff --git a/src/eth/types/block/block.rs b/src/eth/types/block/block.rs index e3abb0010..86bc0dfaf 100644 --- a/src/eth/types/block/block.rs +++ b/src/eth/types/block/block.rs @@ -11,7 +11,6 @@ use crate::alias::JsonValue; use crate::eth::executor::TransactionExecution; use crate::eth::types::BlockHeader; use crate::eth::types::BlockNumber; -use crate::eth::types::ExternalBlock; use crate::eth::types::Hash; use crate::eth::types::Index; use crate::eth::types::LogMessage; @@ -92,12 +91,12 @@ impl Block { } } - pub fn apply_external(&mut self, external_block: &ExternalBlock) { - self.header.hash = external_block.hash(); - assert!(*self.header.timestamp == external_block.header.timestamp); + pub fn apply_imported(&mut self, imported_block: &Block) { + self.header.hash = imported_block.hash(); + assert!(*self.header.timestamp == *imported_block.header.timestamp); for transaction in self.transactions.iter_mut() { assert!(transaction.input.block_timestamp == self.header.timestamp); - transaction.mined_data.block_hash = external_block.hash(); + transaction.mined_data.block_hash = imported_block.hash(); } } } diff --git a/src/eth/types/block/block_header.rs b/src/eth/types/block/block_header.rs index 201cc04c5..e1c0de8f6 100644 --- a/src/eth/types/block/block_header.rs +++ b/src/eth/types/block/block_header.rs @@ -26,7 +26,6 @@ use crate::eth::types::Address; use crate::eth::types::BlockNumber; use crate::eth::types::Bytes; use crate::eth::types::Difficulty; -use crate::eth::types::ExternalBlock; use crate::eth::types::Gas; use crate::eth::types::Hash; use crate::eth::types::LogsBloom; @@ -177,36 +176,6 @@ impl From for AlloyBlock { } } -// ----------------------------------------------------------------------------- -// Conversions: Other -> Self -// ----------------------------------------------------------------------------- - -impl TryFrom<&ExternalBlock> for BlockHeader { - type Error = anyhow::Error; - fn try_from(value: &ExternalBlock) -> Result { - Ok(Self { - number: BlockNumber::from(value.0.header.inner.number), - hash: Hash::from(value.0.header.hash), - transactions_root: Hash::from(value.0.header.inner.transactions_root), - gas_used: Gas::from(value.0.header.inner.gas_used), - gas_limit: Gas::from(value.0.header.inner.gas_limit), - bloom: LogsBloom::from(value.0.header.inner.logs_bloom), - timestamp: UnixTime::from(value.0.header.inner.timestamp), - parent_hash: Hash::from(value.0.header.inner.parent_hash), - author: Address::from(value.0.header.inner.beneficiary), - extra_data: Bytes::from(value.0.header.inner.extra_data.clone()), - miner: Address::from(value.0.header.inner.beneficiary), - difficulty: Difficulty::from(value.0.header.inner.difficulty), - receipts_root: Hash::from(value.0.header.inner.receipts_root), - uncle_hash: Hash::from(value.0.header.inner.ommers_hash), - size: Size::try_from(value.0.header.size.unwrap_or_default())?, - state_root: Hash::from(value.0.header.inner.state_root), - total_difficulty: Difficulty::from(value.0.header.total_difficulty.unwrap_or_default()), - nonce: MinerNonce::from(value.0.header.inner.nonce.0), - }) - } -} - impl From for SubscriptionMessage { fn from(value: BlockHeader) -> Self { serde_json::value::RawValue::from_string(serde_json::to_string(&AlloyBlockVoid::from(value)).expect_infallible()) diff --git a/src/eth/types/external/external_block.rs b/src/eth/types/external/external_block.rs deleted file mode 100644 index eb4be6198..000000000 --- a/src/eth/types/external/external_block.rs +++ /dev/null @@ -1,131 +0,0 @@ -#[cfg(test)] -use alloy_eips::eip4895::Withdrawals; -#[cfg(test)] -use alloy_primitives::B64; -#[cfg(test)] -use alloy_primitives::B256; -#[cfg(test)] -use alloy_primitives::Bloom; -#[cfg(test)] -use alloy_primitives::Bytes; -#[cfg(test)] -use alloy_primitives::U256; -#[cfg(test)] -use fake::Dummy; -#[cfg(test)] -use fake::Fake; -#[cfg(test)] -use fake::Faker; -use serde::Deserialize; - -use crate::alias::AlloyBlockExternalTransaction; -use crate::alias::JsonValue; -use crate::eth::types::Address; -use crate::eth::types::Block; -use crate::eth::types::BlockNumber; -#[cfg(test)] -use crate::eth::types::ExternalTransaction; -use crate::eth::types::Hash; -use crate::eth::types::UnixTime; -use crate::log_and_err; - -#[derive(Debug, Clone, PartialEq, derive_more::Deref, derive_more::DerefMut, serde::Serialize, serde::Deserialize)] -#[serde(transparent)] -pub struct ExternalBlock(#[deref] pub AlloyBlockExternalTransaction); - -impl ExternalBlock { - /// Returns the block hash. - #[allow(clippy::expect_used)] - pub fn hash(&self) -> Hash { - Hash::from(self.0.header.hash) - } - - /// Returns the block number. - #[allow(clippy::expect_used)] - pub fn number(&self) -> BlockNumber { - BlockNumber::from(self.0.header.inner.number) - } - - /// Returns the block timestamp. - pub fn timestamp(&self) -> UnixTime { - self.0.header.inner.timestamp.into() - } - - /// Returns the block author. - pub fn author(&self) -> Address { - self.0.header.inner.beneficiary.into() - } -} - -impl PartialEq for ExternalBlock { - fn eq(&self, other: &Block) -> bool { - self.number() == other.number() && self.timestamp() == other.header.timestamp && self.hash() == other.header.hash - } -} - -#[cfg(test)] -impl Dummy for ExternalBlock { - fn dummy_with_rng(faker: &Faker, rng: &mut R) -> Self { - let mut addr_bytes = [0u8; 20]; - let mut hash_bytes = [0u8; 32]; - let mut nonce_bytes = [0u8; 8]; - rng.fill_bytes(&mut addr_bytes); - rng.fill_bytes(&mut hash_bytes); - rng.fill_bytes(&mut nonce_bytes); - - let transaction: ExternalTransaction = faker.fake_with_rng(rng); - - let block = alloy_rpc_types_eth::Block { - header: alloy_rpc_types_eth::Header { - hash: B256::from_slice(&hash_bytes), - inner: alloy_consensus::Header { - parent_hash: B256::from_slice(&hash_bytes), - ommers_hash: B256::from_slice(&hash_bytes), - beneficiary: alloy_primitives::Address::from_slice(&addr_bytes), - state_root: B256::from_slice(&hash_bytes), - transactions_root: B256::from_slice(&hash_bytes), - receipts_root: B256::from_slice(&hash_bytes), - withdrawals_root: Some(B256::from_slice(&hash_bytes)), - number: rng.next_u64(), - gas_used: rng.next_u64(), - gas_limit: rng.next_u64(), - extra_data: Bytes::default(), - logs_bloom: Bloom::default(), - timestamp: rng.next_u64(), - difficulty: U256::from(rng.next_u64()), - mix_hash: B256::from_slice(&hash_bytes), - nonce: B64::from_slice(&nonce_bytes), - base_fee_per_gas: Some(rng.next_u64()), - blob_gas_used: None, - excess_blob_gas: None, - parent_beacon_block_root: None, - requests_hash: None, - block_access_list_hash: None, - slot_number: None, - }, - total_difficulty: Some(U256::from(rng.next_u64())), - size: Some(U256::from(rng.next_u64())), - }, - uncles: vec![B256::from_slice(&hash_bytes)], - transactions: alloy_rpc_types_eth::BlockTransactions::Full(vec![transaction]), - withdrawals: Some(Withdrawals::default()), - }; - - ExternalBlock(block) - } -} - -// ----------------------------------------------------------------------------- -// Conversions: Other -> Self -// ----------------------------------------------------------------------------- - -impl TryFrom for ExternalBlock { - type Error = anyhow::Error; - - fn try_from(value: JsonValue) -> Result { - match ExternalBlock::deserialize(&value) { - Ok(v) => Ok(v), - Err(e) => log_and_err!(reason = e, payload = value, "failed to convert payload value to ExternalBlock"), - } - } -} diff --git a/src/eth/types/external/external_block_with_receipts.rs b/src/eth/types/external/external_block_with_receipts.rs deleted file mode 100644 index af80a36e1..000000000 --- a/src/eth/types/external/external_block_with_receipts.rs +++ /dev/null @@ -1,47 +0,0 @@ -#[cfg(test)] -use fake::Dummy; -#[cfg(test)] -use fake::Faker; -use serde::Deserialize; -use serde::Serialize; - -use crate::alias::JsonValue; -use crate::eth::types::ExternalBlock; -use crate::eth::types::ExternalReceipt; -use crate::log_and_err; - -#[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize)] -pub struct ExternalBlockWithReceipts { - pub block: ExternalBlock, - pub receipts: Vec, -} - -#[cfg(test)] -impl Dummy for ExternalBlockWithReceipts { - fn dummy_with_rng(faker: &Faker, rng: &mut R) -> Self { - let block = ExternalBlock::dummy_with_rng(faker, rng); - - let receipts = match &block.transactions { - alloy_rpc_types_eth::BlockTransactions::Full(txs) => txs.iter().map(|_| ExternalReceipt::dummy_with_rng(faker, rng)).collect(), - alloy_rpc_types_eth::BlockTransactions::Hashes(_) => Vec::new(), - alloy_rpc_types_eth::BlockTransactions::Uncle => Vec::new(), - }; - - Self { block, receipts } - } -} - -// ----------------------------------------------------------------------------- -// Conversions: Other -> Self -// ----------------------------------------------------------------------------- - -impl TryFrom for ExternalBlockWithReceipts { - type Error = anyhow::Error; - - fn try_from(value: JsonValue) -> Result { - match ExternalBlockWithReceipts::deserialize(&value) { - Ok(v) => Ok(v), - Err(e) => log_and_err!(reason = e, payload = value, "failed to convert payload value to ExternalBlockWithReceipts"), - } - } -} diff --git a/src/eth/types/external/external_receipt.rs b/src/eth/types/external/external_receipt.rs deleted file mode 100644 index 64cab5270..000000000 --- a/src/eth/types/external/external_receipt.rs +++ /dev/null @@ -1,236 +0,0 @@ -#[cfg(test)] -use alloy_consensus::ReceiptEnvelope; -#[cfg(test)] -use alloy_primitives::B256; -#[cfg(test)] -use alloy_primitives::Bloom; -#[cfg(test)] -use alloy_primitives::Bytes; -use alloy_primitives::U256; -#[cfg(test)] -use fake::Dummy; -#[cfg(test)] -use fake::Faker; -use serde::Deserialize; - -use crate::alias::AlloyReceipt; -use crate::alias::JsonValue; -use crate::eth::types::BlockNumber; -use crate::eth::types::Hash; -use crate::eth::types::Wei; -use crate::log_and_err; - -#[derive(Debug, Clone, PartialEq, derive_more::Deref, serde::Serialize)] -#[serde(transparent)] -pub struct ExternalReceipt(#[deref] pub AlloyReceipt); - -impl ExternalReceipt { - /// Returns the transaction hash. - pub fn hash(&self) -> Hash { - Hash::from(self.0.transaction_hash.0) - } - - /// Returns the block number. - #[allow(clippy::expect_used)] - pub fn block_number(&self) -> BlockNumber { - self.0.block_number.expect("external receipt must have block number").into() - } - - /// Returns the block hash. - #[allow(clippy::expect_used)] - pub fn block_hash(&self) -> Hash { - Hash::from(self.0.block_hash.expect("external receipt must have block hash").0) - } - - /// Retuns the effective price the sender had to pay to execute the transaction. - pub fn execution_cost(&self) -> Wei { - let gas_price = U256::from(self.0.effective_gas_price); - let gas_used = U256::from(self.0.gas_used); - (gas_price * gas_used).into() - } - - /// Checks if the transaction was completed with success. - pub fn is_success(&self) -> bool { - self.0.inner.status() - } -} - -#[cfg(test)] -impl Dummy for ExternalReceipt { - fn dummy_with_rng(_faker: &Faker, rng: &mut R) -> Self { - let mut addr_bytes = [0u8; 20]; - let mut hash_bytes = [0u8; 32]; - rng.fill_bytes(&mut addr_bytes); - rng.fill_bytes(&mut hash_bytes); - - let log = alloy_rpc_types_eth::Log { - inner: alloy_primitives::Log { - address: alloy_primitives::Address::from_slice(&addr_bytes), - data: alloy_primitives::LogData::new_unchecked(vec![B256::from_slice(&hash_bytes)], Bytes::default()), - }, - block_hash: Some(B256::from_slice(&hash_bytes)), - block_number: Some(rng.next_u64()), - transaction_hash: Some(B256::from_slice(&hash_bytes)), - transaction_index: Some(rng.next_u64()), - log_index: Some(rng.next_u64()), - removed: false, - block_timestamp: Some(rng.next_u64()), - }; - - let receipt = alloy_consensus::Receipt { - status: alloy_consensus::Eip658Value::Eip658(true), - cumulative_gas_used: rng.next_u64(), - logs: vec![log], - }; - - let receipt_envelope = ReceiptEnvelope::Legacy(alloy_consensus::ReceiptWithBloom { - receipt, - logs_bloom: Bloom::default(), - }); - - let receipt = alloy_rpc_types_eth::TransactionReceipt { - inner: receipt_envelope, - transaction_hash: B256::from_slice(&hash_bytes), - transaction_index: Some(rng.next_u64()), - block_hash: Some(B256::from_slice(&hash_bytes)), - block_number: Some(rng.next_u64()), - from: alloy_primitives::Address::from_slice(&addr_bytes), - to: Some(alloy_primitives::Address::from_slice(&addr_bytes)), - contract_address: None, - gas_used: rng.next_u64(), - effective_gas_price: rng.next_u64() as u128, - blob_gas_used: None, - blob_gas_price: None, - }; - - ExternalReceipt(receipt) - } -} - -// ----------------------------------------------------------------------------- -// Serialization / Deserialization -// ----------------------------------------------------------------------------- - -impl<'de> serde::Deserialize<'de> for ExternalReceipt { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - // During migration from ethers to alloy, we need to handle receipts from both libraries. - // Ethers receipts do not include `effectiveGasPrice` and `type` fields which are - // required by alloy. - let mut value = JsonValue::deserialize(deserializer)?; - - if let Some(obj) = value.as_object_mut() { - if !obj.contains_key("effectiveGasPrice") { - obj.insert("effectiveGasPrice".to_string(), serde_json::json!("0x0")); - } - if !obj.contains_key("type") { - obj.insert("type".to_string(), serde_json::json!("0x0")); - } - } else { - return Err(serde::de::Error::custom("ExternalReceipt must be a JSON object, received invalid type")); - } - - let receipt = serde_json::from_value(value).map_err(|e| serde::de::Error::custom(format!("Failed to deserialize ExternalReceipt: {e}")))?; - - Ok(ExternalReceipt(receipt)) - } -} - -// ----------------------------------------------------------------------------- -// Conversions: Other -> Self -// ----------------------------------------------------------------------------- - -impl TryFrom for ExternalReceipt { - type Error = anyhow::Error; - - fn try_from(value: JsonValue) -> Result { - match ExternalReceipt::deserialize(&value) { - Ok(v) => Ok(v), - Err(e) => log_and_err!(reason = e, payload = value, "failed to convert payload value to ExternalReceipt"), - } - } -} - -#[cfg(test)] -mod tests { - - use alloy_consensus::TxType; - - use super::*; - - #[test] - fn test_deserialize_ethers_receipt() { - let ethers_receipt = r#"{ - "blockHash": "0xc05ff25c9e4bcfb57a5bab271a38b46a8c8b2d5d9ef815ba449d6e211da42251", - "blockNumber": "0x20", - "contractAddress": null, - "cumulativeGasUsed": "0x0", - "from": "0x4fe666531f4a27d0cf5e3d2e73d9122a7f03777b", - "gasUsed": "0xe19c", - "logs": [{ - "address": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512", - "blockHash": "0xc05ff25c9e4bcfb57a5bab271a38b46a8c8b2d5d9ef815ba449d6e211da42251", - "blockNumber": "0x20", - "data": "0x000000000000000000000000000000000000000000000000000000000000000a", - "logIndex": "0x0", - "removed": false, - "topics": [ - "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - "0x0000000000000000000000004fe666531f4a27d0cf5e3d2e73d9122a7f03777b", - "0x000000000000000000000000673dfa23201c98b7a3bfb48fc5cc4011d6759869" - ], - "transactionHash": "0x1c9b122e1321398ac869512b121f97c057e28e0e2fa96e9a8df1ecbfa9824faf", - "transactionIndex": "0x20" - }], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000004200000000000000000000000000000008000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000800000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000002000000000000000000001000000000000000000000000000080000000000000000000000000000000000000000000000000000800000000000000000", - "status": "0x1", - "to": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512", - "transactionHash": "0x1c9b122e1321398ac869512b121f97c057e28e0e2fa96e9a8df1ecbfa9824faf", - "transactionIndex": "0x20" - }"#; - - let receipt: ExternalReceipt = serde_json::from_str(ethers_receipt).unwrap(); - assert_eq!(receipt.0.effective_gas_price, 0); - assert_eq!(receipt.0.transaction_type(), TxType::Legacy); - } - - #[test] - fn test_deserialize_alloy_receipt() { - let alloy_receipt = r#"{ - "blockHash": "0x20dd72172e4bd9c99a919c217dd8c0154cbe0f9e305e67c5247f2ee8ae987c06", - "blockNumber": "0x16", - "contractAddress": null, - "cumulativeGasUsed": "0xe19c", - "effectiveGasPrice": "0x0", - "from": "0x08ea581a1da0e4c8a3e494501102c1cb16a89d1d", - "gasUsed": "0xe19c", - "logs": [{ - "address": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512", - "blockHash": "0x20dd72172e4bd9c99a919c217dd8c0154cbe0f9e305e67c5247f2ee8ae987c06", - "blockNumber": "0x16", - "data": "0x0000000000000000000000000000000000000000000000000000000000000002", - "logIndex": "0x0", - "removed": false, - "topics": [ - "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - "0x00000000000000000000000008ea581a1da0e4c8a3e494501102c1cb16a89d1d", - "0x0000000000000000000000008259d2809ea92d5fad80c279ea11d2e371b8e33c" - ], - "transactionHash": "0x8eef471d6dad6584888af17b80f01f25f79875a0e0a1cbd17809c74093381bbc", - "transactionIndex": "0x26" - }], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000010000000000000000000000000010000000000000000000000000008000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000002000004000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000", - "status": "0x1", - "to": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512", - "transactionHash": "0x8eef471d6dad6584888af17b80f01f25f79875a0e0a1cbd17809c74093381bbc", - "transactionIndex": "0x26", - "type": "0x0" - }"#; - - let receipt: ExternalReceipt = serde_json::from_str(alloy_receipt).unwrap(); - assert_eq!(receipt.0.effective_gas_price, 0); - assert_eq!(receipt.0.transaction_type(), TxType::Legacy); - } -} diff --git a/src/eth/types/external/external_receipts.rs b/src/eth/types/external/external_receipts.rs deleted file mode 100644 index 238a5d766..000000000 --- a/src/eth/types/external/external_receipts.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::collections::HashMap; - -use anyhow::anyhow; -#[cfg(test)] -use fake::Dummy; -#[cfg(test)] -use fake::Faker; - -use crate::eth::types::ExternalReceipt; -use crate::eth::types::Hash; - -/// A collection of [`ExternalReceipt`]. -#[derive(Debug, Clone, PartialEq, serde::Serialize)] -#[cfg_attr(test, derive(serde::Deserialize))] -pub struct ExternalReceipts(HashMap); - -impl ExternalReceipts { - /// Tries to remove a receipt by its hash. - pub fn try_remove(&mut self, tx_hash: Hash) -> anyhow::Result { - match self.0.remove(&tx_hash) { - Some(receipt) => Ok(receipt), - None => { - tracing::error!(%tx_hash, "receipt is missing for hash"); - Err(anyhow!("receipt missing for hash {tx_hash}")) - } - } - } -} - -#[cfg(test)] -impl Dummy for ExternalReceipts { - fn dummy_with_rng(faker: &Faker, rng: &mut R) -> Self { - let count = (rng.next_u32() % 5 + 1) as usize; - let receipts = (0..count).map(|_| ExternalReceipt::dummy_with_rng(faker, rng)).collect::>(); - - Self::from(receipts) - } -} - -// ----------------------------------------------------------------------------- -// Conversions: Other -> Self -// ----------------------------------------------------------------------------- - -impl From> for ExternalReceipts { - fn from(receipts: Vec) -> Self { - let mut receipts_by_hash = HashMap::with_capacity_and_hasher(receipts.len(), hash_hasher::HashBuildHasher::default()); - for receipt in receipts { - receipts_by_hash.insert(receipt.hash(), receipt); - } - Self(receipts_by_hash) - } -} diff --git a/src/eth/types/external/external_transaction.rs b/src/eth/types/external/external_transaction.rs deleted file mode 100644 index 91ae1d7bc..000000000 --- a/src/eth/types/external/external_transaction.rs +++ /dev/null @@ -1,280 +0,0 @@ -#[cfg(test)] -use alloy_consensus::Signed; -#[cfg(test)] -use alloy_consensus::TxEnvelope; -#[cfg(test)] -use alloy_consensus::TxLegacy; -#[cfg(test)] -use alloy_consensus::transaction::Recovered; -#[cfg(test)] -use alloy_primitives::Bytes; -#[cfg(test)] -use alloy_primitives::Signature; -#[cfg(test)] -use alloy_primitives::TxKind; -#[cfg(test)] -use alloy_primitives::U256; -use anyhow::Context; -use anyhow::Result; -#[cfg(test)] -use fake::Dummy; -#[cfg(test)] -use fake::Fake; -#[cfg(test)] -use fake::Faker; - -use crate::alias::AlloyTransaction; -#[cfg(test)] -use crate::eth::types::Address; -use crate::eth::types::BlockNumber; -use crate::eth::types::Hash; -#[cfg(test)] -use crate::eth::types::UnixTime; -#[cfg(test)] -use crate::eth::types::Wei; - -#[derive(Debug, Clone, PartialEq, derive_more::Deref, serde::Serialize)] -#[serde(transparent)] -pub struct ExternalTransaction(#[deref] pub AlloyTransaction); - -impl<'de> serde::Deserialize<'de> for ExternalTransaction { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - use serde::de::Error; - use serde_json::Value; - - let mut value = Value::deserialize(deserializer)?; - - if let Value::Object(ref mut map) = value { - // If v is 0x0 or 0x1, this is a type 2 (EIP-1559) transaction - if let Some(Value::String(v_value)) = map.get("v") - && (v_value == "0x0" || v_value == "0x1") - && !map.contains_key("type") - { - map.insert("type".to_string(), Value::String("0x2".to_string())); - } - - // Check if this is a type 2 transaction - if let Some(Value::String(type_value)) = map.get("type") - && type_value == "0x2" - { - let gas_price = map.get("gasPrice").cloned().unwrap_or(Value::String("0x0".to_string())); - // For EIP-1559 transactions, ensure max_fee_per_gas and max_priority_fee_per_gas are present - if !map.contains_key("maxFeePerGas") { - map.insert("maxFeePerGas".to_string(), gas_price.clone()); - } - if !map.contains_key("maxPriorityFeePerGas") { - map.insert("maxPriorityFeePerGas".to_string(), gas_price); - } - if !map.contains_key("accessList") { - map.insert("accessList".to_string(), Value::Array(Vec::new())); - } - } - // Check if this is a type 1 transaction - if let Some(Value::String(type_value)) = map.get("type") - && type_value == "0x1" - { - // For EIP-2930 transactions, ensure accessList is present - if !map.contains_key("accessList") { - map.insert("accessList".to_string(), Value::Array(Vec::new())); - } - } - } - - // Use the inner type's deserialization - let transaction = AlloyTransaction::deserialize(value).map_err(D::Error::custom)?; - - Ok(ExternalTransaction(transaction)) - } -} - -impl ExternalTransaction { - /// Returns the block number where the transaction was mined. - pub fn block_number(&self) -> Result { - Ok(self.0.block_number.context("ExternalTransaction has no block_number")?.into()) - } - - /// Returns the transaction hash. - pub fn hash(&self) -> Hash { - Hash::from(*self.0.inner.tx_hash()) - } -} - -#[cfg(test)] -impl Dummy for ExternalTransaction { - fn dummy_with_rng(faker: &Faker, rng: &mut R) -> Self { - let from: Address = faker.fake_with_rng(rng); - let to: Address = faker.fake_with_rng(rng); - - let block_hash: Hash = faker.fake_with_rng(rng); - let block_timestamp: UnixTime = faker.fake_with_rng(rng); - - let gas_price: u128 = faker.fake_with_rng(rng); - let value: Wei = Wei::from(rng.next_u64()); - - let tx = TxLegacy { - chain_id: Some(1), - nonce: rng.next_u64(), - gas_price, - gas_limit: rng.next_u64(), - to: TxKind::Call(from.into()), - value: value.into(), - input: Bytes::default(), - }; - - let r = U256::from(rng.next_u64()); - let s = U256::from(rng.next_u64()); - let v = rng.next_u64().is_multiple_of(2); - let signature = Signature::new(r, s, v); - - let hash: Hash = faker.fake_with_rng(rng); - let inner_tx = TxEnvelope::Legacy(Signed::new_unchecked(tx, signature, hash.into())); - - let inner = alloy_rpc_types_eth::Transaction { - inner: Recovered::new_unchecked(inner_tx, to.into()), - block_hash: Some(block_hash.into()), - block_number: Some(rng.next_u64()), - block_timestamp: Some(*block_timestamp), - transaction_index: Some(rng.next_u64()), - effective_gas_price: Some(gas_price), - }; - - ExternalTransaction(inner) - } -} - -// ----------------------------------------------------------------------------- -// Conversions: Other -> Self -// ----------------------------------------------------------------------------- -impl From for ExternalTransaction { - fn from(value: AlloyTransaction) -> Self { - ExternalTransaction(value) - } -} - -// ----------------------------------------------------------------------------- -// Tests -// ----------------------------------------------------------------------------- - -#[cfg(test)] -mod tests { - use serde_json::json; - - use super::*; - - #[test] - fn test_deserialize_type0_transaction() { - let json = json!({ - "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "type": "0x0", - "from": "0x1234567890123456789012345678901234567890", - "to": "0x0987654321098765432109876543210987654321", - "gas": "0x76c0", - "gasPrice": "0x9184e72a000", - "nonce": "0x1", - "value": "0x9184e72a", - "input": "0x", - "chainId": "0x1", - "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - "v": "0x1b" - }); - - let tx: ExternalTransaction = serde_json::from_value(json).unwrap(); - - assert!(matches!(tx.0.inner.inner(), TxEnvelope::Legacy(_))); - } - - #[test] - fn test_deserialize_type1_transaction_with_missing_access_list() { - let json = json!({ - "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "type": "0x1", - "from": "0x1234567890123456789012345678901234567890", - "to": "0x0987654321098765432109876543210987654321", - "gas": "0x76c0", - "gasPrice": "0x9184e72a000", - "nonce": "0x1", - "value": "0x9184e72a", - "input": "0x", - "chainId": "0x1", - "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - "v": "0x0" - // accessList is missing - }); - - let tx: ExternalTransaction = serde_json::from_value(json).unwrap(); - - assert!(matches!(tx.0.inner.inner(), TxEnvelope::Eip2930(_))); - } - - #[test] - fn test_deserialize_type2_transaction_with_missing_fields() { - let json = json!({ - "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "type": "0x2", - "from": "0x1234567890123456789012345678901234567890", - "to": "0x0987654321098765432109876543210987654321", - "gas": "0x76c0", - "nonce": "0x1", - "value": "0x9184e72a", - "input": "0x", - "chainId": "0x1", - "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - "v": "0x1" - // maxFeePerGas, maxPriorityFeePerGas, and accessList are missing - }); - - let tx: ExternalTransaction = serde_json::from_value(json).unwrap(); - - assert!(matches!(tx.0.inner.inner(), TxEnvelope::Eip1559(_))); - } - - #[test] - fn test_deserialize_type2_inferred_from_v_value() { - let json = json!({ - "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "from": "0x1234567890123456789012345678901234567890", - "to": "0x0987654321098765432109876543210987654321", - "gas": "0x76c0", - "gasPrice": "0x9184e72a000", - "nonce": "0x1", - "value": "0x9184e72a", - "input": "0x", - "chainId": "0x1", - "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - "v": "0x0" - // type field is missing, but v is 0x0 so it should be inferred as type 2 - }); - - let tx: ExternalTransaction = serde_json::from_value(json).unwrap(); - - assert!(matches!(tx.0.inner.inner(), TxEnvelope::Eip1559(_))); - - // Test with v = 0x1 as well - let json = json!({ - "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "from": "0x1234567890123456789012345678901234567890", - "to": "0x0987654321098765432109876543210987654321", - "gas": "0x76c0", - "gasPrice": "0x9184e72a000", - "nonce": "0x1", - "value": "0x9184e72a", - "input": "0x", - "chainId": "0x1", - "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - "v": "0x1" - // type field is missing, but v is 0x1 so it should be inferred as type 2 - }); - - let tx: ExternalTransaction = serde_json::from_value(json).unwrap(); - - assert!(matches!(tx.0.inner.inner(), TxEnvelope::Eip1559(_))); - } -} diff --git a/src/eth/types/external/mod.rs b/src/eth/types/external/mod.rs deleted file mode 100644 index 982ae87da..000000000 --- a/src/eth/types/external/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod external_block; -mod external_block_with_receipts; -mod external_receipt; -mod external_receipts; -mod external_transaction; - -pub use external_block::ExternalBlock; -pub use external_block_with_receipts::ExternalBlockWithReceipts; -pub use external_receipt::ExternalReceipt; -pub use external_receipts::ExternalReceipts; -pub use external_transaction::ExternalTransaction; diff --git a/src/eth/types/mod.rs b/src/eth/types/mod.rs index 170618781..4bdd7f77e 100644 --- a/src/eth/types/mod.rs +++ b/src/eth/types/mod.rs @@ -1,7 +1,6 @@ pub mod block; mod error; pub mod execution_kind; -pub mod external; pub mod primitives; pub mod transaction; pub use block::Block; @@ -14,11 +13,6 @@ pub use error::StateError; pub use error::StratusError; pub use error::UnexpectedError; pub use execution_kind::ExecutionKind; -pub use external::ExternalBlock; -pub use external::ExternalBlockWithReceipts; -pub use external::ExternalReceipt; -pub use external::ExternalReceipts; -pub use external::ExternalTransaction; pub use primitives::Account; pub use primitives::Address; pub use primitives::BlockNumber; @@ -66,27 +60,15 @@ mod tests { use crate::eth::rpc::LogFilter; use crate::eth::rpc::LogFilterInput; use crate::eth::rpc::LogFilterInputTopic; - use crate::gen_test_json; use crate::gen_test_serde; - // TODO: Test external structs and internal structs that contain external strtucts that do no implement faker::Dummy + // TODO: Test internal structs that do no implement faker::Dummy // gen_test_serde!(ExecutionConflicts); // gen_test_serde!(ExecutionConflictsBuilder); - // gen_test_serde!(ExternalBlock); - // gen_test_serde!(ExternalReceipt); - // gen_test_serde!(ExternalReceipts); - // gen_test_serde!(ExternalTransaction); - // gen_test_serde!(ExternalTransactionExecution); // gen_test_serde!(PendingBlock); // gen_test_serde!(TransactionExecution); // gen_test_serde!(TransactionStage); - gen_test_json!(ExternalBlock); - gen_test_json!(ExternalBlockWithReceipts); - gen_test_json!(ExternalReceipt); - gen_test_json!(ExternalReceipts); - gen_test_json!(ExternalTransaction); - gen_test_serde!(Account); gen_test_serde!(Address); gen_test_serde!(BlockFilter); diff --git a/src/eth/types/transaction/transaction_input.rs b/src/eth/types/transaction/transaction_input.rs index 5bf2dd24b..880112591 100644 --- a/src/eth/types/transaction/transaction_input.rs +++ b/src/eth/types/transaction/transaction_input.rs @@ -1,5 +1,4 @@ use alloy_consensus::Signed; -use alloy_consensus::Transaction; use alloy_consensus::TxEip1559; use alloy_consensus::TxEip2930; use alloy_consensus::TxEip4844; @@ -32,7 +31,6 @@ use crate::eth::rpc::TransactionDecodeError; use crate::eth::types::Address; use crate::eth::types::Bytes; use crate::eth::types::ChainId; -use crate::eth::types::ExternalTransaction; use crate::eth::types::Gas; use crate::eth::types::Hash; use crate::eth::types::Nonce; @@ -331,7 +329,7 @@ impl TransactionInput { } /// Recovers the signer address from the transaction fields already stored in this input. - fn recover_signer_address(&self) -> anyhow::Result
{ + pub(crate) fn recover_signer_address(&self) -> anyhow::Result
{ let prehash = self.signature_hash(); let signature: AlloySignature = self.signature.into(); let signer = signature @@ -649,63 +647,6 @@ impl RlpDecodable for TransactionInput { // ----------------------------------------------------------------------------- // Conversion: Other -> Self // ----------------------------------------------------------------------------- -impl TryFrom for TransactionInput { - type Error = anyhow::Error; - - fn try_from(value: ExternalTransaction) -> anyhow::Result { - let envelope = value.0.inner.inner(); - - // Reject fields that were used to sign the transaction but are not stored in `TransactionInput`. - ensure_supported(envelope.access_list().is_none_or(|list| list.is_empty()), "accessList")?; - ensure_supported( - envelope.max_priority_fee_per_gas().is_none_or(|fee| fee == envelope.max_fee_per_gas()), - "maxPriorityFeePerGas", - )?; - ensure_supported(envelope.max_fee_per_blob_gas().is_none_or(|fee| fee == 0), "maxFeePerBlobGas")?; - ensure_supported(envelope.blob_versioned_hashes().is_none_or(|hashes| hashes.is_empty()), "blobVersionedHashes")?; - ensure_supported(envelope.authorization_list().is_none_or(|list| list.is_empty()), "authorizationList")?; - - // Get signature components from the envelope - let signature = envelope.signature(); - let signature = Signature { - r: signature.r(), - s: signature.s(), - v: if signature.v() { U64::ONE } else { U64::ZERO }, - }; - - // Build the TransactionInput from the fields we currently support, leaving the - // signer unrecovered. We intentionally ignore any signer that may - // already be present in the source transaction so that the leader and the follower always derive the same address - // from the same set of saved fields. - let mut tx_input = TransactionInput { - transaction_info: TransactionInfo { - tx_type: Some(U64::from(envelope.tx_type() as u8)), - hash: Hash::from(*envelope.tx_hash()), - }, - execution_info: ExecutionInfo { - chain_id: envelope.chain_id().map(Into::into), - nonce: Nonce::from(envelope.nonce()), - signer: Signer::Unrecovered, - to: match envelope.kind() { - TxKind::Call(addr) => Some(Address::from(addr)), - TxKind::Create => None, - }, - value: Wei::from(envelope.value()), - input: Bytes::from(envelope.input().clone()), - gas_limit: Gas::from(envelope.gas_limit()), - gas_price: envelope.max_fee_per_gas(), - }, - signature, - }; - - // Recover the signer directly from the saved fields. - let recovered_signer = tx_input.recover_signer_address()?; - tx_input.execution_info.signer = Signer::Recovered(recovered_signer); - - Ok(tx_input) - } -} - impl From for ExecutionInfo { fn from(value: TransactionExecutionInput) -> Self { Self {