It essentially comes down to the three methods constructing transactions through the Ethereum history until now.
Below is a piece a tiny bit modified Python script originally published in this gist (full credit should go to the original author) covering the three types.
Explanation:
type_0_keys
— legacy transactions before EIP-155.
type_1_keys
— EIP-155 transactions.
type_2_keys
— EIP-1559 transactions.
Replace NODE_ENDPOINT
and TX_HASH
with your values. Note also tx["chainId"] = 1
set for the Ethereum mainnet.
import web3
from eth_account._utils.signing import extract_chain_id, to_standard_v
from eth_account._utils.legacy_transactions import serializable_unsigned_transaction_from_dict
w3 = web3.Web3(web3.HTTPProvider("NODE_ENDPOINT"))
tx = w3.eth.get_transaction("TX_HASH")
tx = dict(tx)
type_0_keys = ['chainId', 'gas', 'gasPrice', 'nonce', 'to', 'value']
type_1_keys = ["to",
"nonce",
"value",
"gas",
'gasPrice',
"chainId",
"type"]
type_2_keys = ["to",
"nonce",
"value",
"gas",
"chainId",
"maxFeePerGas",
"maxPriorityFeePerGas",
"type"]
s = w3.eth.account._keys.Signature(vrs=(
to_standard_v(extract_chain_id(tx["v"])[1]),
w3.to_int(tx["r"]),
w3.to_int(tx["s"])
))
if tx["type"] == 0:
keys_to_get = type_0_keys
elif tx["type"] == 1:
keys_to_get = type_1_keys
elif tx["type"] == 2:
keys_to_get = type_2_keys
if "chainId" not in tx:
tx["chainId"] = 1
tt = {k:tx[k] for k in keys_to_get}
tt["data"] = tx["input"]
ut = serializable_unsigned_transaction_from_dict(tt)
public_key = s.recover_public_key_from_msg_hash(ut.hash())
if hasattr(public_key, 'to_hex'):
public_key_hex = public_key.to_hex()
else:
public_key_bytes = public_key.to_bytes()
public_key_hex = public_key_bytes.hex()
print(public_key_hex)