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

Refactor transaction signature functionality #82

Merged
merged 1 commit into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 8 additions & 1 deletion lib/ethers/signer/local.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,15 @@ defmodule Ethers.Signer.Local do
Transaction.encode(tx)
|> keccak_module().hash_256()
|> secp256k1_module().sign(private_key) do
y_parity_or_v = Transaction.calculate_y_parity_or_v(tx, recovery_id)

signed =
%{tx | signature_r: r, signature_s: s, signature_recovery_id: recovery_id}
%Ethers.Transaction{
tx
| signature_r: r,
signature_s: s,
signature_y_parity_or_v: y_parity_or_v
}
|> Transaction.encode()
|> Utils.hex_encode()

Expand Down
152 changes: 74 additions & 78 deletions lib/ethers/transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,53 +9,52 @@ defmodule Ethers.Transaction do
@enforce_keys [:type]
defstruct [
:type,
access_list: [],
block_hash: nil,
block_number: nil,
chain_id: nil,
nonce: nil,
gas: nil,
from: nil,
to: nil,
value: "0x0",
data: "",
from: nil,
gas: nil,
gas_price: nil,
hash: nil,
max_fee_per_gas: nil,
max_priority_fee_per_gas: "0x0",
access_list: [],
nonce: nil,
signature_r: nil,
signature_s: nil,
signature_v: nil,
signature_recovery_id: nil,
signature_y_parity: nil,
block_hash: nil,
block_number: nil,
hash: nil,
transaction_index: nil
signature_y_parity_or_v: nil,
to: nil,
transaction_index: nil,
value: "0x0"
]

@type t_transaction_type :: :legacy | :eip1559 | :eip2930 | :eip4844
@type t :: %__MODULE__{
type: t_transaction_type(),
access_list: [{binary(), [binary()]}],
block_hash: binary() | nil,
block_number: binary() | nil,
chain_id: binary() | nil,
nonce: binary() | nil,
gas: binary() | nil,
from: Types.t_address() | nil,
to: Types.t_address() | nil,
value: binary(),
data: binary(),
from: Types.t_address() | nil,
gas: binary() | nil,
gas_price: binary() | nil,
hash: binary() | nil,
max_fee_per_gas: binary() | nil,
max_priority_fee_per_gas: binary(),
access_list: [{binary(), [binary()]}],
nonce: binary() | nil,
signature_r: binary() | nil,
signature_s: binary() | nil,
signature_v: binary() | non_neg_integer() | nil,
signature_y_parity: binary() | non_neg_integer() | nil,
signature_recovery_id: binary() | 0 | 1 | nil,
block_hash: binary() | nil,
block_number: binary() | nil,
hash: binary() | nil,
transaction_index: binary() | nil
signature_y_parity_or_v: binary() | non_neg_integer() | nil,
to: Types.t_address() | nil,
transaction_index: binary() | nil,
type: t_transaction_type(),
value: binary()
}

@transaction_envelope_types %{eip1559: <<2>>, legacy: <<>>}
@legacy_parity_magic_number 27
@legacy_parity_with_chain_magic_number 35
@common_fillable_params [:chain_id, :nonce]
@type_fillable_params %{
legacy: [:gas_price],
Expand All @@ -69,9 +68,7 @@ defmodule Ethers.Transaction do
:max_fee_per_gas,
:max_priority_fee_per_gas,
:nonce,
:signature_recovery_id,
:signature_y_parity,
:signature_v,
:signature_y_parity_or_v,
:transaction_index,
:value
]
Expand Down Expand Up @@ -104,40 +101,13 @@ defmodule Ethers.Transaction do
end
end

def encode(%{type: :legacy} = tx) do
[
tx.nonce,
tx.gas_price,
tx.gas,
tx.to,
tx.value,
tx.data
]
|> maybe_add_signature(tx)
def encode(%__MODULE__{type: type} = transaction) do
transaction
|> to_rlp_list()
|> maybe_append_signature(transaction)
|> convert_to_binary()
|> ExRLP.encode()
end

def encode(%{type: :eip1559} = tx) do
[
tx.chain_id,
tx.nonce,
tx.max_priority_fee_per_gas,
tx.max_fee_per_gas,
tx.gas,
tx.to,
tx.value,
tx.data,
tx.access_list
]
|> maybe_add_signature(tx)
|> convert_to_binary()
|> ExRLP.encode()
|> then(&(<<2>> <> &1))
end

def encode(%{type: type}) do
raise "Ethers does not support encoding of #{inspect(type)} transactions"
|> prepend_type_envelope(type)
end

def from_map(tx) do
Expand All @@ -158,9 +128,7 @@ defmodule Ethers.Transaction do
nonce: from_map_value(tx, :nonce),
signature_r: from_map_value(tx, :r),
signature_s: from_map_value(tx, :s),
signature_v: from_map_value(tx, :v),
signature_recovery_id: from_map_value(tx, :v),
signature_y_parity: from_map_value(tx, :yParity),
signature_y_parity_or_v: from_map_value(tx, :yParity) || from_map_value(tx, :v),
to: from_map_value(tx, :to),
transaction_index: from_map_value(tx, :transactionIndex),
value: from_map_value(tx, :value)
Expand Down Expand Up @@ -212,10 +180,11 @@ defmodule Ethers.Transaction do
end)
end

defp maybe_add_signature(tx_list, tx) do
defp maybe_append_signature(tx_list, tx) do
case tx do
%{signature_r: r, signature_s: s} when has_value(r) and has_value(s) ->
tx_list ++ [get_y_parity(tx), trim_leading(r), trim_leading(s)]
%{signature_r: r, signature_s: s, signature_y_parity_or_v: y_parity}
when has_value(r) and has_value(s) and has_value(y_parity) ->
tx_list ++ [y_parity, trim_leading(r), trim_leading(s)]

%{type: :legacy, chain_id: chain_id} when not is_nil(chain_id) ->
# EIP-155 encoding for signature mitigation intra-chain replay attack
Expand All @@ -226,6 +195,39 @@ defmodule Ethers.Transaction do
end
end

defp to_rlp_list(%{type: :eip1559} = tx) do
[
tx.chain_id,
tx.nonce,
tx.max_priority_fee_per_gas,
tx.max_fee_per_gas,
tx.gas,
tx.to,
tx.value,
tx.data,
tx.access_list || []
]
end

defp to_rlp_list(%{type: :legacy} = tx) do
[
tx.nonce,
tx.gas_price,
tx.gas,
tx.to,
tx.value,
tx.data
]
end

defp to_rlp_list(%{type: type}) do
raise "Ethers does not support encoding of #{inspect(type)} transactions"
end

defp prepend_type_envelope(tx_data, type) do
Map.fetch!(@transaction_envelope_types, type) <> tx_data
end

defp fill_action(:chain_id, _tx), do: :chain_id
defp fill_action(:nonce, tx), do: {:get_transaction_count, [tx.from, "latest"]}
defp fill_action(:max_fee_per_gas, _tx), do: :gas_price
Expand Down Expand Up @@ -273,29 +275,23 @@ defmodule Ethers.Transaction do
end)
end

defp get_y_parity(%{signature_y_parity: y_parity}) when has_value(y_parity) do
y_parity
end

defp get_y_parity(%{signature_recovery_id: rec_id} = tx) when has_value(rec_id) do
def calculate_y_parity_or_v(tx, recovery_id) when has_value(recovery_id) do
case tx do
%{type: :legacy, chain_id: chain_id} when has_value(chain_id) ->
# EIP-155
chain_id = Utils.hex_to_integer!(chain_id)
rec_id + 35 + chain_id * 2
recovery_id + @legacy_parity_with_chain_magic_number + chain_id * 2

%{type: :legacy} ->
# EIP-155
rec_id + 27
recovery_id + @legacy_parity_magic_number

_ ->
# EIP-1559
rec_id
recovery_id
end
end

defp get_y_parity(%{type: :legacy, signature_v: v}) when has_value(v), do: v

defp trim_leading(<<0, rest::binary>>), do: trim_leading(rest)
defp trim_leading(<<bin::binary>>), do: bin

Expand Down
25 changes: 10 additions & 15 deletions test/ethers/transaction_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ defmodule Ethers.TransactionTest do
access_list: [],
signature_r: "0x639e5b615f34498f3e5a03f4831e4b7a2a1d5b61ed1388181ef7689c01466fc3",
signature_s: "0x34a9311fae88125c4f9df5d0ed61f8e37bbaf62681f3ce96d03899114df8997",
signature_recovery_id: "0x1",
signature_y_parity: "0x1",
signature_v: "0x1",
signature_y_parity_or_v: "0x1",
block_hash: "0xa2b720a9653afd26411e9bc94283cc496cd3d763378a67fd645bf1a4e332f37d",
block_number: "0x595",
hash: "0xdc78c7e7ea3a5980f732e466daf1fdc4f009e973530d7e84f0b2012f1ff2cfc7",
Expand All @@ -48,22 +46,19 @@ defmodule Ethers.TransactionTest do
transaction_index: 0,
max_priority_fee_per_gas: 0,
access_list: [],
signature_recovery_id: 1,
signature_y_parity: 1,
signature_v: 1
} = decoded

assert is_binary(decoded.data)
assert is_binary(decoded.signature_r)
assert is_binary(decoded.signature_s)
signature_y_parity_or_v: 1,
signature_r: Ethers.Utils.hex_decode!(@transaction_fixture.signature_r),
signature_s: Ethers.Utils.hex_decode!(@transaction_fixture.signature_s),
data: Ethers.Utils.hex_decode!(@transaction_fixture.data)
} == decoded
end

test "does not fail with missing values" do
assert %{signature_recovery_id: nil} =
Transaction.decode_values(%{@transaction_fixture | signature_recovery_id: nil})
assert %{signature_y_parity_or_v: nil} =
Transaction.decode_values(%{@transaction_fixture | signature_y_parity_or_v: nil})

assert %{signature_recovery_id: nil} =
Transaction.decode_values(%{@transaction_fixture | signature_recovery_id: ""})
assert %{signature_y_parity_or_v: nil} =
Transaction.decode_values(%{@transaction_fixture | signature_y_parity_or_v: ""})
end
end
end
Loading