Skip to content

Commit 7dd1305

Browse files
authored
Add EIP-4844 transaction reading (#177)
* add eip4844 transaction struct * test eip4844 transaction encoding
1 parent 7a4528c commit 7dd1305

File tree

3 files changed

+270
-2
lines changed

3 files changed

+270
-2
lines changed

lib/ethers/transaction.ex

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule Ethers.Transaction do
99
"""
1010

1111
alias Ethers.Transaction.Eip1559
12+
alias Ethers.Transaction.Eip4844
1213
alias Ethers.Transaction.Legacy
1314
alias Ethers.Transaction.Protocol, as: TxProtocol
1415
alias Ethers.Transaction.Signed
@@ -22,7 +23,7 @@ defmodule Ethers.Transaction do
2223
@typedoc """
2324
EVM Transaction payload type
2425
"""
25-
@type t_payload :: Eip1559.t() | Legacy.t()
26+
@type t_payload :: Eip4844.t() | Eip1559.t() | Legacy.t()
2627

2728
@doc "Creates a new transaction struct with the given parameters."
2829
@callback new(map()) :: {:ok, t()} | {:error, reason :: atom()}
@@ -42,7 +43,11 @@ defmodule Ethers.Transaction do
4243

4344
@default_transaction_type Eip1559
4445

45-
@transaction_type_modules Application.compile_env(:ethers, :transaction_types, [Eip1559, Legacy])
46+
@transaction_type_modules Application.compile_env(:ethers, :transaction_types, [
47+
Eip4844,
48+
Eip1559,
49+
Legacy
50+
])
4651

4752
@rpc_fields %{
4853
access_list: :accessList,
@@ -251,6 +256,7 @@ defmodule Ethers.Transaction do
251256
# Convert from RPC-style field names to EVM field names.
252257
new(%{
253258
access_list: from_map_value(tx, :accessList),
259+
blob_versioned_hashes: from_map_value(tx, :blobVersionedHashes),
254260
block_hash: from_map_value(tx, :blockHash),
255261
block_number: from_map_value_int(tx, :blockNumber),
256262
chain_id: from_map_value_int(tx, :chainId),
@@ -259,6 +265,7 @@ defmodule Ethers.Transaction do
259265
gas: from_map_value_int(tx, :gas),
260266
gas_price: from_map_value_int(tx, :gasPrice),
261267
hash: from_map_value(tx, :hash),
268+
max_fee_per_blob_gas: from_map_value_int(tx, :maxFeePerBlobGas),
262269
max_fee_per_gas: from_map_value_int(tx, :maxFeePerGas),
263270
max_priority_fee_per_gas: from_map_value_int(tx, :maxPriorityFeePerGas),
264271
nonce: from_map_value_int(tx, :nonce),

lib/ethers/transaction/eip4844.ex

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
defmodule Ethers.Transaction.Eip4844 do
2+
@moduledoc """
3+
Transaction struct and protocol implementation for Ethereum Improvement Proposal (EIP) 4844
4+
transactions. EIP-4844 introduced "blob-carrying transactions" which contain a large amount
5+
of data that cannot be accessed by EVM execution, but whose commitment can be accessed.
6+
7+
See: https://eips.ethereum.org/EIPS/eip-4844
8+
"""
9+
10+
alias Ethers.Types
11+
alias Ethers.Utils
12+
13+
@behaviour Ethers.Transaction
14+
15+
@type_id 3
16+
17+
@enforce_keys [
18+
:chain_id,
19+
:nonce,
20+
:max_priority_fee_per_gas,
21+
:max_fee_per_gas,
22+
:gas,
23+
:max_fee_per_blob_gas
24+
]
25+
defstruct [
26+
:chain_id,
27+
:nonce,
28+
:max_priority_fee_per_gas,
29+
:max_fee_per_gas,
30+
:gas,
31+
:to,
32+
:value,
33+
:input,
34+
:max_fee_per_blob_gas,
35+
access_list: [],
36+
blob_versioned_hashes: []
37+
]
38+
39+
@typedoc """
40+
A transaction type following EIP-4844 (Type-3) and incorporating the following fields:
41+
- `chain_id` - chain ID of network where the transaction is to be executed
42+
- `nonce` - sequence number for the transaction from this sender
43+
- `max_priority_fee_per_gas` - maximum fee per gas (in wei) to give to validators as priority fee (introduced in EIP-1559)
44+
- `max_fee_per_gas` - maximum total fee per gas (in wei) willing to pay (introduced in EIP-1559)
45+
- `gas` - maximum amount of gas allowed for transaction execution
46+
- `to` - destination address for transaction, nil for contract creation
47+
- `value` - amount of ether (in wei) to transfer
48+
- `input` - data payload of the transaction
49+
- `access_list` - list of addresses and storage keys to warm up (introduced in EIP-2930)
50+
- `max_fee_per_blob_gas` - maximum fee per blob gas (in wei) willing to pay (introduced in EIP-4844)
51+
- `blob_versioned_hashes` - list of versioned hashes of the blobs (introduced in EIP-4844)
52+
"""
53+
@type t :: %__MODULE__{
54+
chain_id: non_neg_integer(),
55+
nonce: non_neg_integer(),
56+
max_priority_fee_per_gas: non_neg_integer(),
57+
max_fee_per_gas: non_neg_integer(),
58+
gas: non_neg_integer(),
59+
to: Types.t_address() | nil,
60+
value: non_neg_integer(),
61+
input: binary(),
62+
access_list: [{binary(), [binary()]}],
63+
max_fee_per_blob_gas: non_neg_integer(),
64+
blob_versioned_hashes: [{binary(), [binary()]}]
65+
}
66+
67+
@impl Ethers.Transaction
68+
def new(params) do
69+
to = params[:to]
70+
71+
{:ok,
72+
%__MODULE__{
73+
chain_id: params.chain_id,
74+
nonce: params.nonce,
75+
max_priority_fee_per_gas: params.max_priority_fee_per_gas,
76+
max_fee_per_gas: params.max_fee_per_gas,
77+
gas: params.gas,
78+
to: to && Utils.to_checksum_address(to),
79+
value: params[:value] || 0,
80+
input: params[:input] || params[:data] || "",
81+
access_list: params[:access_list] || [],
82+
max_fee_per_blob_gas: params.max_fee_per_blob_gas,
83+
blob_versioned_hashes: params[:blob_versioned_hashes] || []
84+
}}
85+
end
86+
87+
@impl Ethers.Transaction
88+
def auto_fetchable_fields do
89+
[:chain_id, :nonce, :max_priority_fee_per_gas, :max_fee_per_gas, :gas, :max_fee_per_blob_gas]
90+
end
91+
92+
@impl Ethers.Transaction
93+
def type_envelope, do: <<type_id()>>
94+
95+
@impl Ethers.Transaction
96+
def type_id, do: @type_id
97+
98+
@impl Ethers.Transaction
99+
def from_rlp_list([
100+
chain_id,
101+
nonce,
102+
max_priority_fee_per_gas,
103+
max_fee_per_gas,
104+
gas,
105+
to,
106+
value,
107+
input,
108+
access_list,
109+
max_fee_per_blob_gas,
110+
blob_versioned_hashes
111+
| rest
112+
]) do
113+
{:ok,
114+
%__MODULE__{
115+
chain_id: :binary.decode_unsigned(chain_id),
116+
nonce: :binary.decode_unsigned(nonce),
117+
max_priority_fee_per_gas: :binary.decode_unsigned(max_priority_fee_per_gas),
118+
max_fee_per_gas: :binary.decode_unsigned(max_fee_per_gas),
119+
gas: :binary.decode_unsigned(gas),
120+
to: (to != "" && Utils.encode_address!(to)) || nil,
121+
value: :binary.decode_unsigned(value),
122+
input: input,
123+
access_list: access_list,
124+
max_fee_per_blob_gas: :binary.decode_unsigned(max_fee_per_blob_gas),
125+
blob_versioned_hashes: blob_versioned_hashes
126+
}, rest}
127+
end
128+
129+
def from_rlp_list(_rlp_list), do: {:error, :transaction_decode_failed}
130+
131+
defimpl Ethers.Transaction.Protocol do
132+
def type_id(_transaction), do: @for.type_id()
133+
134+
def type_envelope(_transaction), do: @for.type_envelope()
135+
136+
def to_rlp_list(tx, _mode) do
137+
# Eip4844 requires Eip1559 fields
138+
[
139+
tx.chain_id,
140+
tx.nonce,
141+
tx.max_priority_fee_per_gas,
142+
tx.max_fee_per_gas,
143+
tx.gas,
144+
(tx.to && Utils.decode_address!(tx.to)) || "",
145+
tx.value,
146+
tx.input,
147+
tx.access_list || [],
148+
tx.max_fee_per_blob_gas,
149+
tx.blob_versioned_hashes || []
150+
]
151+
end
152+
end
153+
end

test/ethers/transaction_test.exs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,117 @@ defmodule Ethers.TransactionTest do
3636
assert "0x02e68205390180851448baf2f58212349400008fdee72ac11b5c542428b35eef5769c409f08080c0" ==
3737
Transaction.encode(transaction) |> Ethers.Utils.hex_encode()
3838
end
39+
40+
test "encodes a transaction with a blob" do
41+
transaction = %Ethers.Transaction.Eip4844{
42+
blob_versioned_hashes: [
43+
Utils.hex_decode!("0x01bb9dc6ee48ae6a6f7ffd69a75196a4d49723beedf35981106e8da0efd8f796")
44+
],
45+
chain_id: 1,
46+
gas: 5_000_000,
47+
input:
48+
Utils.hex_decode!(
49+
"0x0c8f4a10000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000633b68f5d8d3a86593ebb815b4663bcbe0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000116680000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"
50+
),
51+
max_fee_per_blob_gas: 5_372_124_052,
52+
max_fee_per_gas: 42_499_154_466,
53+
max_priority_fee_per_gas: 3_000_000_000,
54+
nonce: 625_972,
55+
to: "0x68d30f47F19c07bCCEf4Ac7FAE2Dc12FCa3e0dC9",
56+
value: 0
57+
}
58+
59+
assert "0x03f9025a0183098d3484b2d05e008509e525a222834c4b409468d30f47f19c07bccef4ac7fae2dc12fca3e0dc980b902040c8f4a10000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000633b68f5d8d3a86593ebb815b4663bcbe0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000116680000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000c0850140341b94e1a001bb9dc6ee48ae6a6f7ffd69a75196a4d49723beedf35981106e8da0efd8f796" ==
60+
Transaction.encode(transaction) |> Ethers.Utils.hex_encode()
61+
end
3962
end
4063

4164
describe "decode/1" do
65+
test "decodes raw EIP-4844 transaction correctly" do
66+
raw_tx =
67+
"0x03f9043c01830b3444847d2b75008519a4418ab283036fd5941c479675ad559dc151f6ec7ed3fbf8cee79582b680b8a43e5aa08200000000000000000000000000000000000000000000000000000000000bfc5200000000000000000000000000000000000000000000000000000000001bd614000000000000000000000000e64a54e2533fd126c2e452c5fab544d80e2e4eb500000000000000000000000000000000000000000000000000000000101868220000000000000000000000000000000000000000000000000000000010186a47f902c0f8dd941c479675ad559dc151f6ec7ed3fbf8cee79582b6f8c6a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000000000000000aa0b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103a0360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbca0a10aa54071443520884ed767b0684edf43acec528b7da83ab38ce60126562660f90141948315177ab297ba92a06054ce80a67ed4dbd7ed3af90129a00000000000000000000000000000000000000000000000000000000000000006a00000000000000000000000000000000000000000000000000000000000000007a00000000000000000000000000000000000000000000000000000000000000009a0000000000000000000000000000000000000000000000000000000000000000aa0b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103a0360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbca0a66cc928b5edb82af9bd49922954155ab7b0942694bea4ce44661d9a8742c2d9a0a66cc928b5edb82af9bd49922954155ab7b0942694bea4ce44661d9a8742c2daa0f652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f3797e352f89b94e64a54e2533fd126c2e452c5fab544d80e2e4eb5f884a00000000000000000000000000000000000000000000000000000000000000004a00000000000000000000000000000000000000000000000000000000000000005a0e85fd79f89ff278fc57d40aecb7947873df9f0beac531c8f71a98f630e1eab62a07686888b19bb7b75e46bb1aa328b65150743f4899443d722f0adf8e252ccda410af863a001e74519daf1b03d40e76d557588db2e9b21396f7aeb6086bd794cc4357083efa00169766b1aff3508331a39e7081e591a3ff3bacf957788571269797db7ff3ccca0017045639ffe91febe66cc4427fcf6331980dd9a0dab4af3e81c5514b918ed6180a036a73bf3fe4b9a375c2564b2b1a4a795c82b3923225af0a2ab5d7a561b0c4b92a0366ac3b831ece20f95d1eac369b1c8d4c2c5ac730655d89c005fe310d1db2086"
68+
69+
expected_from = "0xC1b634853Cb333D3aD8663715b08f41A3Aec47cc"
70+
expected_hash = "0x2a7522ff8773f484123ff169d5a42b4d917d3da5af65baae71f32ab7aeb3dc29"
71+
72+
assert {:ok, decoded_tx} = Transaction.decode(raw_tx)
73+
assert %Transaction.Signed{payload: %Transaction.Eip4844{}} = decoded_tx
74+
75+
# Verify transaction hash matches
76+
assert Transaction.transaction_hash(decoded_tx) == expected_hash
77+
78+
# Verify recovered from address
79+
recovered_from = Transaction.Signed.from_address(decoded_tx)
80+
assert String.downcase(recovered_from) == String.downcase(expected_from)
81+
82+
# Verify other transaction fields
83+
assert decoded_tx.payload.chain_id == 1
84+
assert decoded_tx.payload.gas == 225_237
85+
assert decoded_tx.payload.max_fee_per_gas == 110_129_941_170
86+
assert decoded_tx.payload.nonce == 734_276
87+
assert decoded_tx.payload.max_priority_fee_per_gas == 2_100_000_000
88+
assert decoded_tx.payload.to == "0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6"
89+
assert decoded_tx.payload.value == 0
90+
assert decoded_tx.payload.max_fee_per_blob_gas == 10
91+
92+
# Verify access list
93+
access_list =
94+
Enum.map(decoded_tx.payload.access_list, fn [address, storage_keys] ->
95+
%{
96+
"address" => Utils.hex_encode(address),
97+
"storageKeys" => Enum.map(storage_keys, &Utils.hex_encode(&1, :address))
98+
}
99+
end)
100+
101+
assert access_list ==
102+
[
103+
%{
104+
"address" => "0x1c479675ad559dc151f6ec7ed3fbf8cee79582b6",
105+
"storageKeys" => [
106+
"0x0000000000000000000000000000000000000000000000000000000000000000",
107+
"0x0000000000000000000000000000000000000000000000000000000000000001",
108+
"0x000000000000000000000000000000000000000000000000000000000000000a",
109+
"0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103",
110+
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
111+
"0xa10aa54071443520884ed767b0684edf43acec528b7da83ab38ce60126562660"
112+
]
113+
},
114+
%{
115+
"address" => "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a",
116+
"storageKeys" => [
117+
"0x0000000000000000000000000000000000000000000000000000000000000006",
118+
"0x0000000000000000000000000000000000000000000000000000000000000007",
119+
"0x0000000000000000000000000000000000000000000000000000000000000009",
120+
"0x000000000000000000000000000000000000000000000000000000000000000a",
121+
"0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103",
122+
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
123+
"0xa66cc928b5edb82af9bd49922954155ab7b0942694bea4ce44661d9a8742c2d9",
124+
"0xa66cc928b5edb82af9bd49922954155ab7b0942694bea4ce44661d9a8742c2da",
125+
"0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f3797e352"
126+
]
127+
},
128+
%{
129+
"address" => "0xe64a54e2533fd126c2e452c5fab544d80e2e4eb5",
130+
"storageKeys" => [
131+
"0x0000000000000000000000000000000000000000000000000000000000000004",
132+
"0x0000000000000000000000000000000000000000000000000000000000000005",
133+
"0xe85fd79f89ff278fc57d40aecb7947873df9f0beac531c8f71a98f630e1eab62",
134+
"0x7686888b19bb7b75e46bb1aa328b65150743f4899443d722f0adf8e252ccda41"
135+
]
136+
}
137+
]
138+
139+
# Verify blob versioned hashes
140+
blob_versioned_hashes =
141+
Enum.map(decoded_tx.payload.blob_versioned_hashes, &Utils.hex_encode(&1))
142+
143+
assert blob_versioned_hashes == [
144+
"0x01e74519daf1b03d40e76d557588db2e9b21396f7aeb6086bd794cc4357083ef",
145+
"0x0169766b1aff3508331a39e7081e591a3ff3bacf957788571269797db7ff3ccc",
146+
"0x017045639ffe91febe66cc4427fcf6331980dd9a0dab4af3e81c5514b918ed61"
147+
]
148+
end
149+
42150
test "decodes raw EIP-1559 transaction correctly" do
43151
raw_tx =
44152
"0x02f8af0177837a12008502c4bfbc3282f88c948881562783028f5c1bcb985d2283d5e170d8888880b844a9059cbb0000000000000000000000002ef7f5c7c727d8845e685f462a5b4f8ac4972a6700000000000000000000000000000000000000000000051ab2ea6fbbb7420000c001a007280557e86f690290f9ea9e26cc17e0cf09a17f6c2d041e95b33be4b81888d0a06c7a24e8fba5cceb455b19950849b9733f0deb92d7e8c2a919f4a82df9c6036a"

0 commit comments

Comments
 (0)