diff --git a/e2e/test/automine/e2e-json-rpc.test.ts b/e2e/test/automine/e2e-json-rpc.test.ts index 8c999539a..0b15c2b6d 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,69 @@ 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); + + // 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); + + // 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); + + // 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); + + // 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"); + }); + }); }); 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); +} 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..e95d4a698 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: TxKind::from(value.to.map(Into::into)), + 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: TxKind::from(value.to.map(Into::into)), + 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: TxKind::from(value.to.map(Into::into)), + 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 {