Skip to content

Commit

Permalink
Add EIP-2930 support (#179)
Browse files Browse the repository at this point in the history
* Dynamically generate t_payload typespec

* Add EIP-2930 support

* Fix typedoc

* Improve tests

* Update CHANGELOG
  • Loading branch information
alisinabh authored Jan 10, 2025
1 parent 820e3f2 commit 314e911
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Support [EIP-1191](https://eips.ethereum.org/EIPS/eip-1191): Add chain id to mixed-case checksum address encoding
- Add [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) transaction support
- Add [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930) transaction support

## v0.6.1 (2025-01-02)

Expand Down
52 changes: 30 additions & 22 deletions lib/ethers/transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,33 @@ defmodule Ethers.Transaction do
"""

alias Ethers.Transaction.Eip1559
alias Ethers.Transaction.Eip2930
alias Ethers.Transaction.Eip4844
alias Ethers.Transaction.Legacy
alias Ethers.Transaction.Protocol, as: TxProtocol
alias Ethers.Transaction.Signed
alias Ethers.Utils

@default_transaction_types [Eip1559, Eip2930, Eip4844, Legacy]

@transaction_types Application.compile_env(
:ethers,
:transaction_types,
@default_transaction_types
)

@default_transaction_type Eip1559

@rpc_fields %{
access_list: :accessList,
blob_versioned_hashes: :blobVersionedHashes,
chain_id: :chainId,
gas_price: :gasPrice,
max_fee_per_blob_gas: :maxFeePerBlobGas,
max_fee_per_gas: :maxFeePerGas,
max_priority_fee_per_gas: :maxPriorityFeePerGas
}

@typedoc """
EVM Transaction type
"""
Expand All @@ -23,7 +44,12 @@ defmodule Ethers.Transaction do
@typedoc """
EVM Transaction payload type
"""
@type t_payload :: Eip4844.t() | Eip1559.t() | Legacy.t()
@type t_payload ::
unquote(
@transaction_types
|> Enum.map(&{{:., [], [{:__aliases__, [alias: false], [&1]}, :t]}, [], []})
|> Enum.reduce(&{:|, [], [&1, &2]})
)

@doc "Creates a new transaction struct with the given parameters."
@callback new(map()) :: {:ok, t()} | {:error, reason :: atom()}
Expand All @@ -41,24 +67,6 @@ defmodule Ethers.Transaction do
@callback from_rlp_list([binary() | [binary()]]) ::
{:ok, t(), rest :: [binary() | [binary()]]} | {:error, reason :: term()}

@default_transaction_type Eip1559

@transaction_type_modules Application.compile_env(:ethers, :transaction_types, [
Eip4844,
Eip1559,
Legacy
])

@rpc_fields %{
access_list: :accessList,
blob_versioned_hashes: :blobVersionedHashes,
chain_id: :chainId,
gas_price: :gasPrice,
max_fee_per_blob_gas: :maxFeePerBlobGas,
max_fee_per_gas: :maxFeePerGas,
max_priority_fee_per_gas: :maxPriorityFeePerGas
}

@doc """
Creates a new transaction struct with the given parameters.
Expand All @@ -72,7 +80,7 @@ defmodule Ethers.Transaction do
@spec new(map()) :: {:ok, t()} | {:error, reason :: term()}
def new(params) do
case Map.fetch(params, :type) do
{:ok, type} when type in @transaction_type_modules ->
{:ok, type} when type in @transaction_types ->
input =
params
|> Map.get(:input, Map.get(params, :data))
Expand Down Expand Up @@ -190,7 +198,7 @@ defmodule Ethers.Transaction do
end
end

Enum.each(@transaction_type_modules, fn module ->
Enum.each(@transaction_types, fn module ->
type_envelope = module.type_envelope()

defp decode_transaction_data(<<unquote(type_envelope)::binary, rest::binary>>) do
Expand Down Expand Up @@ -375,7 +383,7 @@ defmodule Ethers.Transaction do

defp decode_type("0x" <> _ = type), do: decode_type(Utils.hex_decode!(type))

Enum.each(@transaction_type_modules, fn module ->
Enum.each(@transaction_types, fn module ->
type_envelope = module.type_envelope()
defp decode_type(unquote(type_envelope)), do: {:ok, unquote(module)}
end)
Expand Down
126 changes: 126 additions & 0 deletions lib/ethers/transaction/eip2930.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
defmodule Ethers.Transaction.Eip2930 do
@moduledoc """
Transaction struct and protocol implementation for Ethereum Improvement Proposal (EIP) 2930
transactions. EIP-2930 introduced a new transaction type that includes an access list,
allowing transactions to pre-specify and pre-pay for account and storage access to mitigate
gas cost changes from EIP-2929 and prevent contract breakage. The access list format also
enables future use cases like block-wide witnesses and static state access patterns.
See: https://eips.ethereum.org/EIPS/eip-2930
"""

alias Ethers.Types
alias Ethers.Utils

@behaviour Ethers.Transaction

@type_id 1

@enforce_keys [:chain_id, :nonce, :gas_price, :gas]
defstruct [
:chain_id,
:nonce,
:gas_price,
:gas,
:to,
:value,
:input,
access_list: []
]

@typedoc """
A transaction type following EIP-2930 (Type-1) and incorporating the following fields:
- `chain_id` - chain ID of network where the transaction is to be executed
- `nonce` - sequence number for the transaction from this sender
- `gas_price`: Price willing to pay for each unit of gas (in wei)
- `gas` - maximum amount of gas allowed for transaction execution
- `to` - destination address for transaction, nil for contract creation
- `value` - amount of ether (in wei) to transfer
- `input` - data payload of the transaction
- `access_list` - list of addresses and storage keys to warm up (introduced in EIP-2930)
"""
@type t :: %__MODULE__{
chain_id: non_neg_integer(),
nonce: non_neg_integer(),
gas_price: non_neg_integer(),
gas: non_neg_integer(),
to: Types.t_address() | nil,
value: non_neg_integer(),
input: binary(),
access_list: [{binary(), [binary()]}]
}

@impl Ethers.Transaction
def new(params) do
to = params[:to]

{:ok,
%__MODULE__{
chain_id: params.chain_id,
nonce: params.nonce,
gas_price: params.gas_price,
gas: params.gas,
to: to && Utils.to_checksum_address(to),
value: params[:value] || 0,
input: params[:input] || params[:data] || "",
access_list: params[:access_list] || []
}}
end

@impl Ethers.Transaction
def auto_fetchable_fields do
[:chain_id, :nonce, :gas_price, :gas]
end

@impl Ethers.Transaction
def type_envelope, do: <<type_id()>>

@impl Ethers.Transaction
def type_id, do: @type_id

@impl Ethers.Transaction
def from_rlp_list([
chain_id,
nonce,
gas_price,
gas,
to,
value,
input,
access_list | rest
]) do
{:ok,
%__MODULE__{
chain_id: :binary.decode_unsigned(chain_id),
nonce: :binary.decode_unsigned(nonce),
gas_price: :binary.decode_unsigned(gas_price),
gas: :binary.decode_unsigned(gas),
to: (to != "" && Utils.encode_address!(to)) || nil,
value: :binary.decode_unsigned(value),
input: input,
access_list: access_list
}, rest}
end

def from_rlp_list(_rlp_list), do: {:error, :transaction_decode_failed}

defimpl Ethers.Transaction.Protocol do
def type_id(_transaction), do: @for.type_id()

def type_envelope(_transaction), do: @for.type_envelope()

def to_rlp_list(tx, _mode) do
# Eip2930 does not discriminate in RLP encoding between payload and hash
[
tx.chain_id,
tx.nonce,
tx.gas_price,
tx.gas,
(tx.to && Utils.decode_address!(tx.to)) || "",
tx.value,
tx.input,
tx.access_list || []
]
end
end
end
41 changes: 38 additions & 3 deletions test/ethers/transaction_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ defmodule Ethers.TransactionTest do
end

describe "decode/1" do
test "decodes raw EIP-4844 transaction correctly" do
test "decodes raw EIP-4844 transaction and re-encodes it correctly" do
raw_tx =
"0x03f9043c01830b3444847d2b75008519a4418ab283036fd5941c479675ad559dc151f6ec7ed3fbf8cee79582b680b8a43e5aa08200000000000000000000000000000000000000000000000000000000000bfc5200000000000000000000000000000000000000000000000000000000001bd614000000000000000000000000e64a54e2533fd126c2e452c5fab544d80e2e4eb500000000000000000000000000000000000000000000000000000000101868220000000000000000000000000000000000000000000000000000000010186a47f902c0f8dd941c479675ad559dc151f6ec7ed3fbf8cee79582b6f8c6a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000000000000000aa0b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103a0360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbca0a10aa54071443520884ed767b0684edf43acec528b7da83ab38ce60126562660f90141948315177ab297ba92a06054ce80a67ed4dbd7ed3af90129a00000000000000000000000000000000000000000000000000000000000000006a00000000000000000000000000000000000000000000000000000000000000007a00000000000000000000000000000000000000000000000000000000000000009a0000000000000000000000000000000000000000000000000000000000000000aa0b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103a0360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbca0a66cc928b5edb82af9bd49922954155ab7b0942694bea4ce44661d9a8742c2d9a0a66cc928b5edb82af9bd49922954155ab7b0942694bea4ce44661d9a8742c2daa0f652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f3797e352f89b94e64a54e2533fd126c2e452c5fab544d80e2e4eb5f884a00000000000000000000000000000000000000000000000000000000000000004a00000000000000000000000000000000000000000000000000000000000000005a0e85fd79f89ff278fc57d40aecb7947873df9f0beac531c8f71a98f630e1eab62a07686888b19bb7b75e46bb1aa328b65150743f4899443d722f0adf8e252ccda410af863a001e74519daf1b03d40e76d557588db2e9b21396f7aeb6086bd794cc4357083efa00169766b1aff3508331a39e7081e591a3ff3bacf957788571269797db7ff3ccca0017045639ffe91febe66cc4427fcf6331980dd9a0dab4af3e81c5514b918ed6180a036a73bf3fe4b9a375c2564b2b1a4a795c82b3923225af0a2ab5d7a561b0c4b92a0366ac3b831ece20f95d1eac369b1c8d4c2c5ac730655d89c005fe310d1db2086"

Expand Down Expand Up @@ -145,9 +145,11 @@ defmodule Ethers.TransactionTest do
"0x0169766b1aff3508331a39e7081e591a3ff3bacf957788571269797db7ff3ccc",
"0x017045639ffe91febe66cc4427fcf6331980dd9a0dab4af3e81c5514b918ed61"
]

assert Ethers.Utils.hex_encode(Transaction.encode(decoded_tx)) == raw_tx
end

test "decodes raw EIP-1559 transaction correctly" do
test "decodes raw EIP-1559 transaction and re-encodes it correctly" do
raw_tx =
"0x02f8af0177837a12008502c4bfbc3282f88c948881562783028f5c1bcb985d2283d5e170d8888880b844a9059cbb0000000000000000000000002ef7f5c7c727d8845e685f462a5b4f8ac4972a6700000000000000000000000000000000000000000000051ab2ea6fbbb7420000c001a007280557e86f690290f9ea9e26cc17e0cf09a17f6c2d041e95b33be4b81888d0a06c7a24e8fba5cceb455b19950849b9733f0deb92d7e8c2a919f4a82df9c6036a"

Expand All @@ -172,9 +174,40 @@ defmodule Ethers.TransactionTest do
assert decoded_tx.payload.max_priority_fee_per_gas == 8_000_000
assert decoded_tx.payload.to == "0x8881562783028F5c1BCB985d2283D5E170D88888"
assert decoded_tx.payload.value == 0

assert Ethers.Utils.hex_encode(Transaction.encode(decoded_tx)) == raw_tx
end

test "decodes raw legacy transaction correctly" do
test "decodes raw EIP-2930 transaction and re-encodes it correctly" do
raw_tx =
"0x01f903640182dd688503a656ac80830623c4944a137fd5e7a256ef08a7de531a17d0be0cc7b6b680b901a46dbf2fa0000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000104414bf3890000000000000000000000007d1afa7b718fb893db30a3abc0cfc608aacfebb0000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000004a137fd5e7a256ef08a7de531a17d0be0cc7b6b60000000000000000000000000000000000000000000000000000000060bda78e0000000000000000000000000000000000000000000000cc223b921be6800000000000000000000000000000000000000000000000000000000000017dd4e6ca000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f90153f87a9407a6e955ba4345bae83ac2a6faa771fddd8a2011f863a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000008f87a947d1afa7b718fb893db30a3abc0cfc608aacfebb0f863a014d5312942240e565c56aec11806ce58e3c0e38c96269d759c5d35a2a2e4a449a02701fd0b2638f33db225d91c6adbdad46590a86a09a2b2c386405c2f742af842a037b0b82ee5d8a88672df3895a46af48bbcd30d6efcc908136e29456fa30604bbf85994a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48f842a037570cf18c6d95744a154fa2b19b7e958c78ef68b8c60a80dc527fc15e2ceb8fa06e89d31e3fd8d2bf0b411c458e98c7463bf723878c3ce8a845bcf9dc3b2e391780a01d40605de92c503219631e625ca0d023df8dfef9058896804fb1952d386b64e1a00e0ec0714b7956fe29820cb62998936b78ca4b8a3b05291db90e475244d5c63f"

expected_from = "0x005FdE5294199d5C3Eb5Eb7a6E51954123b74b1c"
expected_hash = "0xdb32a678b6c5855eb3c5ff47513e136a85a391469755d045d8846e37fc99d774"

assert {:ok, decoded_tx} = Transaction.decode(raw_tx)
assert %Transaction.Signed{payload: %Transaction.Eip2930{}} = decoded_tx

# Verify transaction hash matches
assert Transaction.transaction_hash(decoded_tx) == expected_hash

# Verify recovered from address
recovered_from = Transaction.Signed.from_address(decoded_tx)
assert String.downcase(recovered_from) == String.downcase(expected_from)

# Verify other transaction fields
assert decoded_tx.payload.chain_id == 1
assert decoded_tx.payload.gas == 402_372
assert decoded_tx.payload.gas_price == 15_675_600_000
assert decoded_tx.payload.nonce == 56_680
assert decoded_tx.payload.to == "0x4A137FD5e7a256eF08A7De531A17D0BE0cc7B6b6"
assert decoded_tx.payload.value == 0
assert Enum.count(decoded_tx.payload.access_list) == 3

assert Ethers.Utils.hex_encode(Transaction.encode(decoded_tx)) == raw_tx
end

test "decodes raw legacy transaction and re-encodes it correctly" do
raw_tx =
"0xf86c81c6850c92a69c0082520894e48c9a989438606a79a7560cfba3d34bafbac38e87596f744abf34368025a0ee0b54a64cf8130e36cd1d19395d6d434c285c832a7908873a24610ec32896dfa070b5e779cdcaf5c661c1df44e80895f6ab68463d3ede2cf4955855bc3c6edebb"

Expand All @@ -198,6 +231,8 @@ defmodule Ethers.TransactionTest do
assert decoded_tx.payload.gas_price == 54_000_000_000
assert decoded_tx.payload.to == "0xe48C9A989438606a79a7560cfba3d34BAfBAC38E"
assert decoded_tx.payload.value == 25_173_818_188_182_582

assert Ethers.Utils.hex_encode(Transaction.encode(decoded_tx)) == raw_tx
end
end
end

0 comments on commit 314e911

Please sign in to comment.