From 43996651fdfb29b308f11e97077c2611a9940d67 Mon Sep 17 00:00:00 2001 From: James Duncombe Date: Wed, 31 Jan 2024 11:23:52 +0000 Subject: [PATCH] Refactors transaction types to use new wallet handling. --- lib/tt_eth/behaviours/transaction.ex | 4 +- .../transactions/eip1559_transaction.ex | 67 +++++++++---------- lib/tt_eth/transactions/legacy_transaction.ex | 66 +++++++++--------- .../transactions/eip1559_transaction_test.exs | 22 ++++-- 4 files changed, 80 insertions(+), 79 deletions(-) diff --git a/lib/tt_eth/behaviours/transaction.ex b/lib/tt_eth/behaviours/transaction.ex index f297def..810f1e6 100644 --- a/lib/tt_eth/behaviours/transaction.ex +++ b/lib/tt_eth/behaviours/transaction.ex @@ -9,9 +9,9 @@ defmodule TTEth.Behaviours.Transaction do @type opts :: Keyword.t() @type transaction :: struct() - @type private_key :: binary() + @type wallet :: TTEth.Wallet.t() @callback new(to_address, abi_data, nonce, opts) :: transaction - @callback build(transaction, private_key) :: binary() + @callback build(transaction, wallet) :: binary() end diff --git a/lib/tt_eth/transactions/eip1559_transaction.ex b/lib/tt_eth/transactions/eip1559_transaction.ex index 9cf2243..3875019 100644 --- a/lib/tt_eth/transactions/eip1559_transaction.ex +++ b/lib/tt_eth/transactions/eip1559_transaction.ex @@ -4,8 +4,9 @@ defmodule TTEth.Transactions.EIP1559Transaction do SEE: https://eips.ethereum.org/EIPS/eip-1559 """ - alias TTEth.{BitHelper, Secp256k1} + alias TTEth.{BitHelper, Wallet} alias TTEth.Behaviours.Transaction + alias TTEth.Type.Signature, as: EthSignature import BitHelper, only: [encode_unsigned: 1] @behaviour Transaction @@ -30,6 +31,8 @@ defmodule TTEth.Transactions.EIP1559Transaction do # SEE: https://eips.ethereum.org/EIPS/eip-1559 @transaction_type 2 + @base_recovery_id 27 + @doc """ Creates a new type 2 transaction that can be signed. """ @@ -53,24 +56,22 @@ defmodule TTEth.Transactions.EIP1559Transaction do This will return a binary which can then be base16 encoded etc. """ @impl Transaction - def build(%__MODULE__{} = trx, private_key), + def build(%__MODULE__{} = trx, %Wallet{} = wallet), do: trx - |> sign_transaction(private_key) + |> sign_transaction(wallet) |> serialize(_include_signature = true) |> rlp_encode() |> put_transaction_envelope(trx) - @doc """ - Delegate to ExRLP to RLP encode values. - """ - def rlp_encode(data), + ## Private. + + # Delegate to ExRLP to RLP encode values. + defp rlp_encode(data), do: data |> ExRLP.encode() - @doc """ - Encodes a transaction such that it can be RLP-encoded. - """ - def serialize(%__MODULE__{} = trx, include_vrs \\ true), + # Encodes a transaction such that it can be RLP-encoded. + defp serialize(%__MODULE__{} = trx, include_vrs \\ true), do: [ trx.chain_id |> encode_unsigned(), @@ -85,19 +86,16 @@ defmodule TTEth.Transactions.EIP1559Transaction do ] |> maybe_add_yrs(trx, include_vrs) - @doc """ - Returns a ECDSA signature (v,r,s) for a given hashed value. - """ - def sign_hash(hash, private_key) do - {:ok, {<>, v}} = Secp256k1.ecdsa_sign_compact(hash, private_key) - - {v, r, s} - end + # Returns a ECDSA signature (v,r,s) for a given hashed value. + defp sign_hash(hash, %Wallet{} = wallet), + do: + wallet + |> Wallet.sign(hash) + |> EthSignature.compact_to_components!() + |> recovery_id_to_parity!() - @doc """ - Returns a hash of a given transaction. - """ - def transaction_hash(%__MODULE__{} = trx), + # Returns a hash of a given transaction. + defp transaction_hash(%__MODULE__{} = trx), do: trx |> serialize(_include_signature = false) @@ -105,27 +103,21 @@ defmodule TTEth.Transactions.EIP1559Transaction do |> put_transaction_envelope(trx) |> TTEth.keccak() - @doc """ - Takes a given transaction and returns a version signed with the given private key. - """ - def sign_transaction(%__MODULE__{} = trx, private_key) when is_binary(private_key) do + # Takes a given transaction and returns a version signed with the given private key. + defp sign_transaction(%__MODULE__{} = trx, %Wallet{} = wallet) do {y_parity, r, s} = trx |> transaction_hash() - |> sign_hash(private_key) + |> sign_hash(wallet) %{trx | y_parity: y_parity, r: r, s: s} end - @doc """ - Wraps the RLP encoded transaction in a transaction envelope. - SEE: https://eips.ethereum.org/EIPS/eip-2718 - """ - def put_transaction_envelope(encoded, %__MODULE__{} = trx) when is_binary(encoded), + # Wraps the RLP encoded transaction in a transaction envelope. + # SEE: https://eips.ethereum.org/EIPS/eip-2718 + defp put_transaction_envelope(encoded, %__MODULE__{} = trx) when is_binary(encoded), do: <> <> encoded - ## Private. - # Optionally add the YRS values. defp maybe_add_yrs(base, %__MODULE__{} = trx, _include_vrs = true), do: @@ -137,4 +129,9 @@ defmodule TTEth.Transactions.EIP1559Transaction do ] defp maybe_add_yrs(base, %__MODULE__{}, _dont_include_vrs), do: base + + # Switch the recovery ID to the Y parity. + # SEE: https://eips.ethereum.org/EIPS/eip-1559#specification + defp recovery_id_to_parity!({v, r, s}) when v in [27, 28], + do: {v - @base_recovery_id, r, s} end diff --git a/lib/tt_eth/transactions/legacy_transaction.ex b/lib/tt_eth/transactions/legacy_transaction.ex index 5cd3071..87ba176 100644 --- a/lib/tt_eth/transactions/legacy_transaction.ex +++ b/lib/tt_eth/transactions/legacy_transaction.ex @@ -2,13 +2,13 @@ defmodule TTEth.Transactions.LegacyTransaction do @moduledoc """ Ported from [`Blockchain`](https://hex.pm/packages/blockchain). """ - alias TTEth.{BitHelper, Secp256k1} + alias TTEth.{BitHelper, Wallet} alias TTEth.Behaviours.Transaction + alias TTEth.Type.Signature, as: EthSignature + import BitHelper, only: [encode_unsigned: 1] @behaviour Transaction - @type private_key :: <<_::256>> - @type hash_v :: integer() @type hash_r :: integer() @type hash_s :: integer() @@ -60,10 +60,10 @@ defmodule TTEth.Transactions.LegacyTransaction do } @impl Transaction - def build(%__MODULE__{} = trx, private_key), + def build(%__MODULE__{} = trx, %Wallet{} = wallet), do: trx - |> sign_transaction(private_key) + |> sign_transaction(wallet) |> serialize(_include_signature = true) |> rlp_encode() @@ -95,20 +95,20 @@ defmodule TTEth.Transactions.LegacyTransaction do @spec serialize(t) :: ExRLP.t() def serialize(trx, include_vrs \\ true) do base = [ - trx.nonce |> BitHelper.encode_unsigned(), - trx.gas_price |> BitHelper.encode_unsigned(), - trx.gas_limit |> BitHelper.encode_unsigned(), + trx.nonce |> encode_unsigned(), + trx.gas_price |> encode_unsigned(), + trx.gas_limit |> encode_unsigned(), trx.to, - trx.value |> BitHelper.encode_unsigned(), + trx.value |> encode_unsigned(), if(trx.to == <<>>, do: trx.init, else: trx.data) ] if include_vrs do base ++ [ - trx.v |> BitHelper.encode_unsigned(), - trx.r |> BitHelper.encode_unsigned(), - trx.s |> BitHelper.encode_unsigned() + trx.v |> encode_unsigned(), + trx.r |> encode_unsigned(), + trx.s |> encode_unsigned() ] else base @@ -122,12 +122,14 @@ defmodule TTEth.Transactions.LegacyTransaction do ## Examples - iex> LegacyTransaction.sign_hash(<<2::256>>, <<1::256>>) + iex> wallet = TTEth.Wallet.from_private_key(_private_key = <<1::256>>) + iex> LegacyTransaction.sign_hash(<<2::256>>, wallet) {28, 38938543279057362855969661240129897219713373336787331739561340553100525404231, 23772455091703794797226342343520955590158385983376086035257995824653222457926} - iex> LegacyTransaction.sign_hash(<<5::256>>, <<1::256>>) + iex> wallet = TTEth.Wallet.from_private_key(_private_key = <<1::256>>) + iex> LegacyTransaction.sign_hash(<<5::256>>, wallet) {27, 74927840775756275467012999236208995857356645681540064312847180029125478834483, 56037731387691402801139111075060162264934372456622294904359821823785637523849} @@ -135,26 +137,18 @@ defmodule TTEth.Transactions.LegacyTransaction do iex> data = "ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080" |> TTEth.BitHelper.from_hex iex> hash = data |> TTEth.keccak() iex> private_key = "4646464646464646464646464646464646464646464646464646464646464646" |> TTEth.BitHelper.from_hex - iex> LegacyTransaction.sign_hash(hash, private_key, 1) + iex> wallet = TTEth.Wallet.from_private_key(private_key) + iex> LegacyTransaction.sign_hash(hash, wallet, 1) { 37, 18515461264373351373200002665853028612451056578545711640558177340181847433846, 46948507304638947509940763649030358759909902576025900602547168820602576006531 } """ - @spec sign_hash(BitHelper.keccak_hash(), private_key, integer() | nil) :: + @spec sign_hash(BitHelper.keccak_hash(), Wallet.t(), integer() | nil) :: {hash_v, hash_r, hash_s} - def sign_hash(hash, private_key, chain_id \\ nil) do - {:ok, {<>, recovery_id}} = - Secp256k1.ecdsa_sign_compact(hash, private_key) - - # Fork Ψ EIP-155 - recovery_id = - if chain_id do - chain_id * 2 + @base_recovery_id_eip_155 + recovery_id - else - @base_recovery_id + recovery_id - end - - {recovery_id, r, s} - end + def sign_hash(hash, %Wallet{} = wallet, chain_id \\ nil), + do: + wallet + |> Wallet.sign(hash) + |> EthSignature.compact_to_components!(chain_id) @doc """ Returns a hash of a given transaction according to the @@ -192,19 +186,21 @@ defmodule TTEth.Transactions.LegacyTransaction do ## Examples - iex> LegacyTransaction.sign_transaction(%LegacyTransaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 5, init: <<1>>}, <<1::256>>) + iex> wallet = TTEth.Wallet.from_private_key(_private_key = <<1::256>>) + iex> LegacyTransaction.sign_transaction(%LegacyTransaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 5, init: <<1>>}, wallet) %LegacyTransaction{data: <<>>, gas_limit: 7, gas_price: 6, init: <<1>>, nonce: 5, r: 97037709922803580267279977200525583527127616719646548867384185721164615918250, s: 31446571475787755537574189222065166628755695553801403547291726929250860527755, to: "", v: 27, value: 5} - iex> LegacyTransaction.sign_transaction(%LegacyTransaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 5, init: <<1>>}, <<1::256>>, 1) + iex> wallet = TTEth.Wallet.from_private_key(_private_key = <<1::256>>) + iex> LegacyTransaction.sign_transaction(%LegacyTransaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<>>, value: 5, init: <<1>>}, wallet, 1) %LegacyTransaction{data: <<>>, gas_limit: 7, gas_price: 6, init: <<1>>, nonce: 5, r: 25739987953128435966549144317523422635562973654702886626580606913510283002553, s: 41423569377768420285000144846773344478964141018753766296386430811329935846420, to: "", v: 38, value: 5} """ - @spec sign_transaction(__MODULE__.t(), private_key, integer() | nil) :: __MODULE__.t() - def sign_transaction(trx, private_key, chain_id \\ nil) do + @spec sign_transaction(__MODULE__.t(), Wallet.t(), integer() | nil) :: __MODULE__.t() + def sign_transaction(trx, %Wallet{} = wallet, chain_id \\ nil) do {v, r, s} = trx |> transaction_hash(chain_id) - |> sign_hash(private_key, chain_id) + |> sign_hash(wallet, chain_id) %{trx | v: v, r: r, s: s} end diff --git a/test/tt_eth/transactions/eip1559_transaction_test.exs b/test/tt_eth/transactions/eip1559_transaction_test.exs index dd31a1c..d22f10d 100644 --- a/test/tt_eth/transactions/eip1559_transaction_test.exs +++ b/test/tt_eth/transactions/eip1559_transaction_test.exs @@ -2,12 +2,12 @@ defmodule TTEth.Transactions.EIP1559TransactionTest do use TTEth.Case alias TTEth.Transactions.EIP1559Transaction alias TTEth.Type.{Address, PrivateKey, PublicKey} + alias TTEth.Wallet # Polygon Mumbai. @chain_id 80001 @private_key_human "0x62aa6ec41b56439d2c5df352c45a00389cef262b3761e13c6481e35ab027d262" - @private_key @private_key_human |> PrivateKey.from_human!() @to_address_human "0x38f153fdd399ff2cf64704c6a4b16d3fd9ddcd69" @to_address @to_address_human |> Address.from_human!() # transfer(address,uint256) @@ -51,27 +51,32 @@ defmodule TTEth.Transactions.EIP1559TransactionTest do end describe "build/2" do - setup :build_trx + setup [ + :build_trx, + :build_wallet + ] - test "builds a signed transaction", %{trx: trx} do + test "builds a signed transaction", %{trx: trx, wallet: wallet} do trx - |> EIP1559Transaction.build(@private_key) + |> EIP1559Transaction.build(wallet) |> encode_and_pad() |> assert_match(@valid_transaction_data) end - test "from address is correct when checking signature", %{trx: trx} do + test "from address is correct when checking signature", %{trx: trx, wallet: wallet} do # Build the trx_data but randomize the nonce. built_trx_data = %{trx | nonce: Enum.random(10..100)} - |> EIP1559Transaction.build(@private_key) + |> EIP1559Transaction.build(wallet) |> encode_and_pad() # Decode the transaction data. decoded = built_trx_data |> fully_decode_trx_data() # Get signature params. - [y_parity, r, s] = decoded |> Enum.take(_signature_params = -3) + [y_parity, r, s] = + decoded + |> Enum.take(_signature_params = -3) # Get the raw public key from the signature. {:ok, public_raw} = @@ -117,6 +122,9 @@ defmodule TTEth.Transactions.EIP1559TransactionTest do } } + defp build_wallet(_), + do: %{wallet: @private_key_human |> Wallet.from_private_key()} + ## Helpers. defp encode_and_pad(bin),