From 8c084b7df708fdba32268cc052c564701964d52d Mon Sep 17 00:00:00 2001 From: gabriel-aranha-cw Date: Fri, 21 Feb 2025 10:40:16 -0300 Subject: [PATCH 1/4] feat: support multiple transaction types in transaction conversion Enhance transaction input and mined transaction conversion to support various Ethereum transaction types: - Add support for EIP-2930, EIP-1559, EIP-4844, and EIP-7702 transaction types - Improve transaction type handling in conversion logic - Update receipt envelope generation to match transaction types --- justfile | 2 +- src/eth/primitives/transaction_input.rs | 110 ++++++++++++++++++++---- src/eth/primitives/transaction_mined.rs | 6 +- 3 files changed, 99 insertions(+), 19 deletions(-) diff --git a/justfile b/justfile index 571e2d39b..9004d0778 100644 --- a/justfile +++ b/justfile @@ -489,7 +489,7 @@ e2e-importer-offline: just _log "Compare blocks of stratus and importer-offline" pip install -r utils/compare_block/requirements.txt - python utils/compare_block/main.py http://localhost:3000 http://localhost:3001 1 --ignore timestamp --ignore type + python utils/compare_block/main.py http://localhost:3000 http://localhost:3001 1 --ignore timestamp just _log "Killing Stratus" killport 3000 -s sigterm diff --git a/src/eth/primitives/transaction_input.rs b/src/eth/primitives/transaction_input.rs index 2f18a3040..fe94d6ece 100644 --- a/src/eth/primitives/transaction_input.rs +++ b/src/eth/primitives/transaction_input.rs @@ -1,10 +1,16 @@ use alloy_consensus::Signed; use alloy_consensus::Transaction; +use alloy_consensus::TxEip1559; +use alloy_consensus::TxEip2930; +use alloy_consensus::TxEip4844; +use alloy_consensus::TxEip4844Variant; +use alloy_consensus::TxEip7702; use alloy_consensus::TxEnvelope; use alloy_consensus::TxLegacy; use alloy_eips::eip2718::Decodable2718; use alloy_primitives::PrimitiveSignature; use alloy_primitives::TxKind; +use alloy_rpc_types_eth::AccessList; use anyhow::anyhow; use display_json::DebugAsJson; use ethereum_types::U256; @@ -177,22 +183,96 @@ fn try_from_alloy_transaction(value: alloy_rpc_types_eth::Transaction, compute_s impl From for AlloyTransaction { fn from(value: TransactionInput) -> Self { - let inner = TxEnvelope::Legacy(Signed::new_unchecked( - TxLegacy { - chain_id: value.chain_id.map(Into::into), - nonce: value.nonce.into(), - gas_price: value.gas_price.into(), - gas_limit: value.gas_limit.into(), - to: match value.to { - Some(addr) => TxKind::Call(addr.into()), - None => TxKind::Create, + let signature = PrimitiveSignature::new(SignatureComponent(value.r).into(), SignatureComponent(value.s).into(), value.v.as_u64() == 1); + + let tx_type = value.tx_type.map(|t| t.as_u64()).unwrap_or(0); + + let inner = match tx_type { + // EIP-2930 + 1 => TxEnvelope::Eip2930(Signed::new_unchecked( + TxEip2930 { + chain_id: value.chain_id.unwrap_or_default().into(), + nonce: value.nonce.into(), + gas_price: value.gas_price.into(), + gas_limit: value.gas_limit.into(), + to: value.to.map(|a| TxKind::Call(a.into())).unwrap_or(TxKind::Create), + value: value.value.into(), + input: value.input.clone().into(), + access_list: AccessList::default(), + }, + signature, + value.hash.into(), + )), + + // EIP-1559 + 2 => TxEnvelope::Eip1559(Signed::new_unchecked( + TxEip1559 { + chain_id: value.chain_id.unwrap_or_default().into(), + nonce: value.nonce.into(), + max_fee_per_gas: value.gas_price.into(), + max_priority_fee_per_gas: value.gas_price.into(), + gas_limit: value.gas_limit.into(), + to: value.to.map(|a| TxKind::Call(a.into())).unwrap_or(TxKind::Create), + value: value.value.into(), + input: value.input.clone().into(), + access_list: AccessList::default(), + }, + signature, + value.hash.into(), + )), + + // EIP-4844 + 3 => TxEnvelope::Eip4844(Signed::new_unchecked( + TxEip4844Variant::TxEip4844(TxEip4844 { + chain_id: value.chain_id.unwrap_or_default().into(), + nonce: value.nonce.into(), + max_fee_per_gas: value.gas_price.into(), + max_priority_fee_per_gas: value.gas_price.into(), + gas_limit: value.gas_limit.into(), + to: value.to.map(Into::into).unwrap_or_default(), + value: value.value.into(), + input: value.input.clone().into(), + access_list: AccessList::default(), + blob_versioned_hashes: Vec::new(), + max_fee_per_blob_gas: 0u64.into(), + }), + signature, + value.hash.into(), + )), + + // EIP-7702 + 4 => TxEnvelope::Eip7702(Signed::new_unchecked( + TxEip7702 { + chain_id: value.chain_id.unwrap_or_default().into(), + nonce: value.nonce.into(), + gas_limit: value.gas_limit.into(), + max_fee_per_gas: value.gas_price.into(), + max_priority_fee_per_gas: value.gas_price.into(), + to: value.to.map(Into::into).unwrap_or_default(), + value: value.value.into(), + input: value.input.clone().into(), + access_list: AccessList::default(), + authorization_list: Vec::new(), + }, + signature, + value.hash.into(), + )), + + // Legacy (default) + _ => TxEnvelope::Legacy(Signed::new_unchecked( + TxLegacy { + chain_id: value.chain_id.map(Into::into), + nonce: value.nonce.into(), + gas_price: value.gas_price.into(), + gas_limit: value.gas_limit.into(), + to: value.to.map(|a| TxKind::Call(a.into())).unwrap_or(TxKind::Create), + value: value.value.into(), + input: value.input.clone().into(), }, - value: value.value.into(), - input: value.input.clone().into(), - }, - PrimitiveSignature::new(SignatureComponent(value.r).into(), SignatureComponent(value.s).into(), value.v.as_u64() == 1), - value.hash.into(), - )); + signature, + value.hash.into(), + )), + }; Self { inner, diff --git a/src/eth/primitives/transaction_mined.rs b/src/eth/primitives/transaction_mined.rs index 1500f3a69..e7624cc8e 100644 --- a/src/eth/primitives/transaction_mined.rs +++ b/src/eth/primitives/transaction_mined.rs @@ -101,7 +101,8 @@ impl From for AlloyTransaction { fn from(value: TransactionMined) -> Self { let signer = value.input.signer; let gas_price = value.input.gas_price; - let tx: AlloyTransaction = value.input.into(); + + let tx = AlloyTransaction::from(value.input); Self { inner: tx.inner, @@ -128,12 +129,11 @@ impl From for AlloyReceipt { }; let inner = match value.input.tx_type.map(|tx| tx.as_u64()) { - Some(0) | None => ReceiptEnvelope::Legacy(receipt_with_bloom), Some(1) => ReceiptEnvelope::Eip2930(receipt_with_bloom), Some(2) => ReceiptEnvelope::Eip1559(receipt_with_bloom), Some(3) => ReceiptEnvelope::Eip4844(receipt_with_bloom), Some(4) => ReceiptEnvelope::Eip7702(receipt_with_bloom), - Some(_) => ReceiptEnvelope::Legacy(receipt_with_bloom), + _ => ReceiptEnvelope::Legacy(receipt_with_bloom), }; Self { From 28b1068c687b8cc64da71be37687191f03468960 Mon Sep 17 00:00:00 2001 From: gabriel-aranha-cw Date: Fri, 21 Feb 2025 11:01:39 -0300 Subject: [PATCH 2/4] test: add comprehensive transaction type tests for JSON-RPC Add end-to-end tests for different Ethereum transaction types in the JSON-RPC interface: - Legacy (type 0) transactions - EIP-2930 (type 1) transactions - EIP-1559 (type 2) transactions - EIP-4844 (type 3) transactions Implement test helpers to create and send signed transactions for each type, verifying correct transaction type handling --- e2e/test/automine/e2e-json-rpc.test.ts | 47 +++++++++++++++ e2e/test/helpers/rpc.ts | 79 +++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/e2e/test/automine/e2e-json-rpc.test.ts b/e2e/test/automine/e2e-json-rpc.test.ts index 8c999539a..d2fe3f36a 100644 --- a/e2e/test/automine/e2e-json-rpc.test.ts +++ b/e2e/test/automine/e2e-json-rpc.test.ts @@ -14,9 +14,14 @@ import { ONE, TEST_BALANCE, ZERO, + createEIP1559Transaction, + createEIP2930Transaction, + createEIP4844Transaction, + createLegacyTransaction, deployTestContractBalances, deployTestContractBlockTimestamp, deployTestRevertReason, + pollReceipt, prepareSignedTx, send, sendAndGetError, @@ -297,6 +302,48 @@ describe("JSON-RPC", () => { expect(result).to.equal("success"); }); }); + + describe("Transaction Types", () => { + beforeEach(async () => { + await sendReset(); + }); + + it("handles legacy transaction with type 0", async () => { + const signedTx = await createLegacyTransaction(ALICE, 0); + const txHash = await sendRawTransaction(signedTx); + await pollReceipt(txHash); + + const tx = await ETHERJS.getTransaction(txHash); + expect(tx?.type).to.equal(0); + }); + + it("handles EIP-2930 (type 1) transaction", async () => { + const signedTx = await createEIP2930Transaction(ALICE); + const txHash = await sendRawTransaction(signedTx); + await pollReceipt(txHash); + + const tx = await ETHERJS.getTransaction(txHash); + expect(tx?.type).to.equal(1); + }); + + it("handles EIP-1559 (type 2) transaction", async () => { + const signedTx = await createEIP1559Transaction(ALICE); + const txHash = await sendRawTransaction(signedTx); + await pollReceipt(txHash); + + const tx = await ETHERJS.getTransaction(txHash); + expect(tx?.type).to.equal(2); + }); + + it("handles EIP-4844 (type 3) transaction", async function () { + const signedTx = await createEIP4844Transaction(ALICE); + const txHash = await sendRawTransaction(signedTx); + await pollReceipt(txHash); + + const tx = await ETHERJS.getTransaction(txHash); + expect(tx?.type).to.equal(3); + }); + }); }); describe("Call", () => { diff --git a/e2e/test/helpers/rpc.ts b/e2e/test/helpers/rpc.ts index 8e438df8a..0121ca4ff 100644 --- a/e2e/test/helpers/rpc.ts +++ b/e2e/test/helpers/rpc.ts @@ -1,3 +1,4 @@ +import { RLP } from "@ethereumjs/rlp"; import axios from "axios"; import { expect } from "chai"; import { @@ -6,8 +7,12 @@ import { Contract, JsonRpcApiProviderOptions, JsonRpcProvider, + SigningKey, TransactionReceipt, TransactionResponse, + concat, + getBytes, + hexlify, keccak256, } from "ethers"; import { config, ethers } from "hardhat"; @@ -22,7 +27,7 @@ import { TestContractDenseStorage, TestEvmInput, } from "../../typechain-types"; -import { Account, CHARLIE } from "./account"; +import { Account, BOB, CHARLIE } from "./account"; import { currentMiningIntervalInMs, currentNetwork, isStratus } from "./network"; // ----------------------------------------------------------------------------- @@ -477,8 +482,8 @@ export async function pollReceipt( } tx = await tx; const txHash = typeof tx === "string" ? tx : tx.hash; - const [txReceipt] = await pollReceipts([txHash], options); - return txReceipt; + const result = await pollReceipts([txHash], options); + return result.receipts[0]; } // Polls for the block with a given number is minted @@ -532,3 +537,71 @@ function normalizePollingOptions( pollingIntervalInMs, }; } + +// Creates and signs a legacy (type 0 or empty) transaction +export async function createLegacyTransaction(account: Account, type?: number) { + const tx = { + to: BOB.address, + value: toHex(TEST_TRANSFER), + gasPrice: toHex(1), + gasLimit: toHex(21000), + nonce: toHex(await sendGetNonce(account)), + chainId: parseInt(CHAIN_ID, 16), + }; + + if (type === 0) { + (tx as any).type = 0; + } + + return await account.signer().signTransaction(tx); +} + +// Creates and signs an EIP-2930 (type 1) transaction +export async function createEIP2930Transaction(account: Account) { + const tx = { + to: BOB.address, + value: toHex(TEST_TRANSFER), + gasPrice: toHex(1), + gasLimit: toHex(21000), + nonce: toHex(await sendGetNonce(account)), + chainId: parseInt(CHAIN_ID, 16), + accessList: [], + type: 1, + }; + + return await account.signer().signTransaction(tx); +} + +// Creates and signs an EIP-1559 (type 2) transaction +export async function createEIP1559Transaction(account: Account) { + const tx = { + to: BOB.address, + value: toHex(TEST_TRANSFER), + maxFeePerGas: toHex(2), + maxPriorityFeePerGas: toHex(1), + gasLimit: toHex(21000), + nonce: toHex(await sendGetNonce(account)), + chainId: parseInt(CHAIN_ID, 16), + type: 2, + }; + + return await account.signer().signTransaction(tx); +} + +// Creates and signs a EIP-4844 (type 3) transaction +export async function createEIP4844Transaction(account: Account) { + const tx = { + to: BOB.address, + value: toHex(TEST_TRANSFER), + maxFeePerGas: toHex(2), + maxPriorityFeePerGas: toHex(1), + gasLimit: toHex(21000), + nonce: toHex(await sendGetNonce(account)), + chainId: parseInt(CHAIN_ID, 16), + type: 3, + blobVersionedHashes: [], + maxFeePerBlobGas: toHex(1), + }; + + return await account.signer().signTransaction(tx); +} From 758cf5250ec15d98d708e64d2181bb1bb22fd50f Mon Sep 17 00:00:00 2001 From: gabriel-aranha-cw Date: Fri, 21 Feb 2025 11:06:16 -0300 Subject: [PATCH 3/4] test: enhance JSON-RPC transaction type validation Update transaction type tests to validate transaction details using eth_getTransactionByHash and eth_getTransactionReceipt: - Add hex string type checks for transaction types - Verify additional transaction type-specific properties - Remove deprecated polling mechanism - Improve test coverage for transaction type validation --- e2e/test/automine/e2e-json-rpc.test.ts | 45 +++++++++++++++++++------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/e2e/test/automine/e2e-json-rpc.test.ts b/e2e/test/automine/e2e-json-rpc.test.ts index d2fe3f36a..0b15c2b6d 100644 --- a/e2e/test/automine/e2e-json-rpc.test.ts +++ b/e2e/test/automine/e2e-json-rpc.test.ts @@ -311,37 +311,58 @@ describe("JSON-RPC", () => { it("handles legacy transaction with type 0", async () => { const signedTx = await createLegacyTransaction(ALICE, 0); const txHash = await sendRawTransaction(signedTx); - await pollReceipt(txHash); - const tx = await ETHERJS.getTransaction(txHash); - expect(tx?.type).to.equal(0); + // Validate via eth_getTransactionByHash + const tx = await send("eth_getTransactionByHash", [txHash]); + expect(tx.type).to.equal("0x0"); + + // Validate via eth_getTransactionReceipt + const receipt = await send("eth_getTransactionReceipt", [txHash]); + expect(receipt.type).to.equal("0x0"); }); it("handles EIP-2930 (type 1) transaction", async () => { const signedTx = await createEIP2930Transaction(ALICE); const txHash = await sendRawTransaction(signedTx); - await pollReceipt(txHash); - const tx = await ETHERJS.getTransaction(txHash); - expect(tx?.type).to.equal(1); + // Validate via eth_getTransactionByHash + const tx = await send("eth_getTransactionByHash", [txHash]); + expect(tx.type).to.equal("0x1"); + expect(tx.accessList).to.be.an("array"); + + // Validate via eth_getTransactionReceipt + const receipt = await send("eth_getTransactionReceipt", [txHash]); + expect(receipt.type).to.equal("0x1"); }); it("handles EIP-1559 (type 2) transaction", async () => { const signedTx = await createEIP1559Transaction(ALICE); const txHash = await sendRawTransaction(signedTx); - await pollReceipt(txHash); - const tx = await ETHERJS.getTransaction(txHash); - expect(tx?.type).to.equal(2); + // Validate via eth_getTransactionByHash + const tx = await send("eth_getTransactionByHash", [txHash]); + expect(tx.type).to.equal("0x2"); + expect(tx.maxFeePerGas).to.match(HEX_PATTERN); + expect(tx.maxPriorityFeePerGas).to.match(HEX_PATTERN); + + // Validate via eth_getTransactionReceipt + const receipt = await send("eth_getTransactionReceipt", [txHash]); + expect(receipt.type).to.equal("0x2"); }); it("handles EIP-4844 (type 3) transaction", async function () { const signedTx = await createEIP4844Transaction(ALICE); const txHash = await sendRawTransaction(signedTx); - await pollReceipt(txHash); - const tx = await ETHERJS.getTransaction(txHash); - expect(tx?.type).to.equal(3); + // Validate via eth_getTransactionByHash + const tx = await send("eth_getTransactionByHash", [txHash]); + expect(tx.type).to.equal("0x3"); + expect(tx.maxFeePerBlobGas).to.match(HEX_PATTERN); + expect(tx.blobVersionedHashes).to.be.an("array"); + + // Validate via eth_getTransactionReceipt + const receipt = await send("eth_getTransactionReceipt", [txHash]); + expect(receipt.type).to.equal("0x3"); }); }); }); From c7fe4483f861084485b72390255e206d13655532 Mon Sep 17 00:00:00 2001 From: gabriel-aranha-cw Date: Fri, 21 Feb 2025 11:54:39 -0300 Subject: [PATCH 4/4] refactor: simplify TxKind conversion in transaction input mapping Optimize the conversion of transaction input to AlloyTransaction by using a more concise TxKind mapping approach. Replace verbose map and unwrap pattern with a more direct TxKind::from conversion for transaction destination handling. --- src/eth/primitives/transaction_input.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/eth/primitives/transaction_input.rs b/src/eth/primitives/transaction_input.rs index fe94d6ece..e95d4a698 100644 --- a/src/eth/primitives/transaction_input.rs +++ b/src/eth/primitives/transaction_input.rs @@ -195,7 +195,7 @@ impl From for AlloyTransaction { nonce: value.nonce.into(), gas_price: value.gas_price.into(), gas_limit: value.gas_limit.into(), - to: value.to.map(|a| TxKind::Call(a.into())).unwrap_or(TxKind::Create), + to: TxKind::from(value.to.map(Into::into)), value: value.value.into(), input: value.input.clone().into(), access_list: AccessList::default(), @@ -212,7 +212,7 @@ impl From for AlloyTransaction { max_fee_per_gas: value.gas_price.into(), max_priority_fee_per_gas: value.gas_price.into(), gas_limit: value.gas_limit.into(), - to: value.to.map(|a| TxKind::Call(a.into())).unwrap_or(TxKind::Create), + to: TxKind::from(value.to.map(Into::into)), value: value.value.into(), input: value.input.clone().into(), access_list: AccessList::default(), @@ -265,7 +265,7 @@ impl From for AlloyTransaction { nonce: value.nonce.into(), gas_price: value.gas_price.into(), gas_limit: value.gas_limit.into(), - to: value.to.map(|a| TxKind::Call(a.into())).unwrap_or(TxKind::Create), + to: TxKind::from(value.to.map(Into::into)), value: value.value.into(), input: value.input.clone().into(), },