Current section
Files
Jump to
Current section
Files
lib/ethers/transaction/signed.ex
defmodule Ethers.Transaction.Signed do
@moduledoc """
A struct that wraps a transaction and its signature values.
"""
alias Ethers.Transaction
alias Ethers.Transaction.Legacy
alias Ethers.Transaction.Metadata
@enforce_keys [:payload, :signature_r, :signature_s, :signature_y_parity_or_v]
defstruct [
:payload,
:signature_r,
:signature_s,
:signature_y_parity_or_v,
metadata: nil
]
@typedoc """
A transaction signature envelope that wraps transaction data with its signature components.
This type supports both Legacy (pre-EIP-155), EIP-155 Legacy, and EIP-1559 transaction formats.
The signature components consist of:
- `signature_r`, `signature_s`: The ECDSA signature values as defined in Ethereum's Yellow Paper
- `signature_y_parity_or_v`: The recovery value that varies by transaction type:
- For pre-EIP-155 Legacy transactions: v = recovery_id + 27
- For EIP-155 Legacy transactions: v = recovery_id + chain_id * 2 + 35
- For EIP-1559 transactions: Just the recovery_id (0 or 1) as specified in EIP-2930
Related EIPs:
- [EIP-155](https://eips.ethereum.org/EIPS/eip-155): Simple replay attack protection
- [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559): Fee market change for ETH 1.0 chain
- [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930): Optional access lists
"""
@type t :: %__MODULE__{
payload: Transaction.t_payload(),
signature_r: binary(),
signature_s: binary(),
signature_y_parity_or_v: non_neg_integer(),
metadata: Metadata.t() | nil
}
@legacy_parity_magic_number 27
@legacy_parity_with_chain_magic_number 35
@doc false
def new(params) do
{:ok,
%__MODULE__{
payload: params.payload,
signature_r: params.signature_r,
signature_s: params.signature_s,
signature_y_parity_or_v: params.signature_y_parity_or_v,
metadata: Metadata.new!(params)
}}
end
@doc false
def from_rlp_list(rlp_list, payload) do
case rlp_list do
[signature_y_parity_or_v, signature_r, signature_s] ->
signed_tx =
maybe_add_chain_id(%__MODULE__{
payload: payload,
signature_r: signature_r,
signature_s: signature_s,
signature_y_parity_or_v: :binary.decode_unsigned(signature_y_parity_or_v)
})
{:ok, signed_tx}
[] ->
{:error, :no_signature}
_rlp_list ->
{:error, :signature_decode_failed}
end
end
defp maybe_add_chain_id(%__MODULE__{payload: %Legacy{chain_id: nil} = legacy_tx} = signed_tx) do
{chain_id, _recovery_id} = extract_chain_id_and_recovery_id(signed_tx)
%{signed_tx | payload: %{legacy_tx | chain_id: chain_id}}
end
defp maybe_add_chain_id(%__MODULE__{} = tx), do: tx
@doc """
Calculates the from address of a signed transaction using its signature.
The from address is inferred from the signature of the transaction rather than being explicitly
specified. This is done by recovering the signer's public key from the signature and then
deriving the corresponding Ethereum address.
## Returns
- `{:ok, address}` - Successfully recovered from address
- `{:error, reason}` - Failed to recover address
"""
@spec from_address(t()) :: {:ok, Ethers.Types.t_address()} | {:error, atom()}
def from_address(%__MODULE__{} = transaction) do
hash_bin = Transaction.transaction_hash(transaction.payload, :bin)
{_chain_id, recovery_id} = extract_chain_id_and_recovery_id(transaction)
case Ethers.secp256k1_module().recover(
hash_bin,
transaction.signature_r,
transaction.signature_s,
recovery_id
) do
{:ok, pubkey} -> Ethers.Utils.public_key_to_address(pubkey)
{:error, reason} -> {:error, reason}
end
end
@doc """
Calculates the y-parity or v value for transaction signatures.
Handles both legacy and EIP-1559 transaction types according to their specifications.
## Parameters
- `tx` - Transaction struct
- `recovery_id` - Recovery ID from the signature
## Returns
- `integer` - Calculated y-parity or v value
"""
@spec calculate_y_parity_or_v(Transaction.t_payload(), binary() | non_neg_integer()) ::
non_neg_integer()
def calculate_y_parity_or_v(tx, recovery_id) do
case tx do
%Legacy{chain_id: nil} ->
# EIP-155
recovery_id + @legacy_parity_magic_number
%Legacy{chain_id: chain_id} ->
# EIP-155
recovery_id + chain_id * 2 + @legacy_parity_with_chain_magic_number
_tx ->
# EIP-1559
recovery_id
end
end
@spec extract_chain_id_and_recovery_id(t()) :: {non_neg_integer() | nil, non_neg_integer()}
defp extract_chain_id_and_recovery_id(%__MODULE__{payload: tx, signature_y_parity_or_v: v}) do
case tx do
%Legacy{} ->
if v >= @legacy_parity_with_chain_magic_number do
chain_id = div(v - @legacy_parity_with_chain_magic_number, 2)
recovery_id = v - chain_id * 2 - @legacy_parity_with_chain_magic_number
{chain_id, recovery_id}
else
{nil, v - @legacy_parity_magic_number}
end
_tx ->
{tx.chain_id, v}
end
end
defimpl Transaction.Protocol do
import Ethers.Utils, only: [remove_leading_zeros: 1]
def type_id(signed_tx), do: Transaction.Protocol.type_id(signed_tx.payload)
def type_envelope(signed_tx), do: Transaction.Protocol.type_envelope(signed_tx.payload)
def to_rlp_list(signed_tx, mode) do
base_list = Transaction.Protocol.to_rlp_list(signed_tx.payload, mode)
base_list ++ signature_fields(signed_tx)
end
defp signature_fields(signed_tx) do
[
signed_tx.signature_y_parity_or_v,
remove_leading_zeros(signed_tx.signature_r),
remove_leading_zeros(signed_tx.signature_s)
]
end
end
end