Skip to content

Instantly share code, notes, and snippets.

@alisinabh
Created March 13, 2024 01:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alisinabh/eeb72821155d7d6ecade722949914ba2 to your computer and use it in GitHub Desktop.
Save alisinabh/eeb72821155d7d6ecade722949914ba2 to your computer and use it in GitHub Desktop.
Recover Ethereum legacy transaction public key in Elixir

Recover Ethereum transaction public key

Mix.install([:ethers, :ex_secp256k1])

Procedure for legacy transactions

raw_transaction =
  "0xf86b35850861c4680082520894dec50599773e005112663e4e35528460319c5507875f53e91d066f0b8025a07bbf52ea4ee9b12ca3f1e5d63e93eee550d111225457e0b59d7698324b04993aa06ab577a1f38941cc08d7345d8aee9fc62024bca37da2115d8420baa7e3fcc300"
"0xf86b35850861c4680082520894dec50599773e005112663e4e35528460319c5507875f53e91d066f0b8025a07bbf52ea4ee9b12ca3f1e5d63e93eee550d111225457e0b59d7698324b04993aa06ab577a1f38941cc08d7345d8aee9fc62024bca37da2115d8420baa7e3fcc300"
decoded_raw_transaction = Ethers.Utils.hex_decode!(raw_transaction)
<<248, 107, 53, 133, 8, 97, 196, 104, 0, 130, 82, 8, 148, 222, 197, 5, 153, 119, 62, 0, 81, 18, 102,
  62, 78, 53, 82, 132, 96, 49, 156, 85, 7, 135, 95, 83, 233, 29, 6, 111, 11, 128, 37, 160, 123, 191,
  82, 234, 78, 233, ...>>

This is the structure of legacy ethereum transactions.

[nonce, gas_price, gas, to, value, data, v, r, s] = ExRLP.decode(decoded_raw_transaction)
[
  "5",
  <<8, 97, 196, 104, 0>>,
  "R\b",
  <<222, 197, 5, 153, 119, 62, 0, 81, 18, 102, 62, 78, 53, 82, 132, 96, 49, 156, 85, 7>>,
  <<95, 83, 233, 29, 6, 111, 11>>,
  "",
  "%",
  <<123, 191, 82, 234, 78, 233, 177, 44, 163, 241, 229, 214, 62, 147, 238, 229, 80, 209, 17, 34, 84,
    87, 224, 181, 157, 118, 152, 50, 75, 4, 153, 58>>,
  <<106, 181, 119, 161, 243, 137, 65, 204, 8, 215, 52, 93, 138, 238, 159, 198, 32, 36, 188, 163,
    125, 162, 17, 93, 132, 32, 186, 167, 227, 252, 195, 0>>
]

Re-construct transaction body and calculate recovery_id based on EIP-155

v_int = :binary.decode_unsigned(v)

{tx_body, parity} =
  if v_int > 27 do
    m = v_int - 35
    parity = rem(m, 2)
    chain_id = div(m - parity, 2)

    {[nonce, gas_price, gas, to, value, data, chain_id, 0, 0], parity}
  else
    {[nonce, gas_price, gas, to, value, data], v_int - 27}
  end
{[
   "5",
   <<8, 97, 196, 104, 0>>,
   "R\b",
   <<222, 197, 5, 153, 119, 62, 0, 81, 18, 102, 62, 78, 53, 82, 132, 96, 49, 156, 85, 7>>,
   <<95, 83, 233, 29, 6, 111, 11>>,
   "",
   1,
   0,
   0
 ], 0}

RLP encode and Hash the body

tx_body_hash = ExRLP.encode(tx_body) |> ExKeccak.hash_256()
<<152, 121, 142, 245, 217, 136, 37, 192, 238, 92, 189, 151, 145, 126, 37, 43, 195, 38, 80, 60, 92,
  47, 60, 44, 197, 2, 131, 195, 179, 173, 212, 165>>

Recover public key

{:ok, pub_key} = ExSecp256k1.recover(tx_body_hash, r, s, parity)
{:ok,
 <<4, 151, 206, 146, 242, 45, 215, 166, 141, 181, 30, 205, 84, 73, 151, 234, 173, 231, 63, 182, 120,
   35, 117, 51, 226, 227, 195, 92, 141, 221, 98, 121, 152, 231, 183, 11, 129, 58, 154, 14, 48, 102,
   215, 221, 133, 5, 87, 44, ...>>}

Calculate address of a public key

Ethers.Utils.public_key_to_address(pub_key)
"0xb412E9D7c64638bDD554EcaDf2520633147Eaa44"
@robrobbins
Copy link

robrobbins commented Mar 13, 2024

i think the if should be > 28 as i just hit that case in testing with a type:0 transaction signed in ethers.js . >27 produces a -7 in that case..

could likely be more like >=35 as there's no case we want a signed int there

uuugh. these fkn magic number holders from btc.. silly.. anyway since we also subtract 27 should likely put this in a cond where if v >= 35 do w/chain_id etc.. if >=27 do w/o and :else throw maybe? or just return with the v_int being whatever it is...

i'll likely assign some const-type things like @155_magic_number 35 and @legacy_magic_number 27

@alisinabh

@robrobbins
Copy link

looks to be working however ++++

@robrobbins
Copy link

some interesting points to add along with the above for the upcoming PR:
ethersjs may send 0x01<sig> or 0x02<sig> in addition to the "type 0" 0x<sig> . in the case of the first two we'll need to remove those prefixes before calling hex decode on them. not terribly hard but something to note.

more interesting is that in the "type 1" case there are more fields in the initial RLP decode so knowing what to expect when assigning [nonce, gas...] or if its [nonce, maxFee...] will be critical. I'm fairly certain we can use the detected type to dictate the expected array but it may be worth introducing them in separate PRS.

i.e PR for "legacy" (type 0). then 1 (1559) and 2 etc... there's some unknowns in there for me (access lists etc)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment