Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add basic support for multiple transaction types in transaction conversion #2025

Merged
merged 5 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions e2e/test/automine/e2e-json-rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ import {
ONE,
TEST_BALANCE,
ZERO,
createEIP1559Transaction,
createEIP2930Transaction,
createEIP4844Transaction,
createLegacyTransaction,
deployTestContractBalances,
deployTestContractBlockTimestamp,
deployTestRevertReason,
pollReceipt,
prepareSignedTx,
send,
sendAndGetError,
Expand Down Expand Up @@ -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", () => {
Expand Down
79 changes: 76 additions & 3 deletions e2e/test/helpers/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RLP } from "@ethereumjs/rlp";
import axios from "axios";
import { expect } from "chai";
import {
Expand All @@ -6,8 +7,12 @@ import {
Contract,
JsonRpcApiProviderOptions,
JsonRpcProvider,
SigningKey,
TransactionReceipt,
TransactionResponse,
concat,
getBytes,
hexlify,
keccak256,
} from "ethers";
import { config, ethers } from "hardhat";
Expand All @@ -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";

// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
110 changes: 95 additions & 15 deletions src/eth/primitives/transaction_input.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -177,22 +183,96 @@ fn try_from_alloy_transaction(value: alloy_rpc_types_eth::Transaction, compute_s

impl From<TransactionInput> 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,
Expand Down
6 changes: 3 additions & 3 deletions src/eth/primitives/transaction_mined.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ impl From<TransactionMined> 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,
Expand All @@ -128,12 +129,11 @@ impl From<TransactionMined> 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 {
Expand Down
Loading