-
Prior to EIP-2718/EIP-2930 there was just one transaction type. At that time a public key could be recovered from transaction data using the process in #700 (comment) Now that there's three transaction types, that no longer is sufficient. Is there an ethers method that handles these differences, or does it need to be implemented manually? I couldn't find a method, so here's my shot at trying to recover a public key from a transaction. From my tests it seems to work properly, though you'll see some of the comments contain questions on things I'm unsure of Note that the import { Signature, recoverPublicKey } from 'noble-secp256k1';
async function recoverPublicKeyFromTransaction(txHash, provider) {
// TODO The `type` field on `tx` is optional, but anecdotally it seems to always be
// present when coming from the response of this call. Is that correct
const tx = await provider.getTransaction(txHash);
// Reconstruct transaction payload that was originally signed. Relevant EIPs:
// - https://eips.ethereum.org/EIPS/eip-155 (EIP-155: Simple replay attack protection)
// - https://eips.ethereum.org/EIPS/eip-2718 (EIP-2718: Typed Transaction Envelope)
// - https://eips.ethereum.org/EIPS/eip-2930 (EIP-2930: Optional access lists)
// - https://eips.ethereum.org/EIPS/eip-1559 (EIP-1559: Fee market change for ETH 1.0 chain)
//
// Any time a new transaction type is added to Ethereum, the below will need to be updated to
// support that transaction type
//
// chainId and type are appended after the `if` blocks when applicable
let txData: UnsignedTransaction;
if (tx.type === 0 || !tx.type) {
// LegacyTransaction is rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s])
txData = {
nonce: tx.nonce,
gasPrice: tx.gasPrice,
gasLimit: tx.gasLimit,
to: tx.to,
value: tx.value,
data: tx.data,
};
} else if (tx.type === 1) {
// 0x01 || rlp([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, v, r, s])
txData = {
nonce: tx.nonce,
gasPrice: tx.gasPrice,
gasLimit: tx.gasLimit,
to: tx.to,
value: tx.value,
data: tx.data,
// TODO will accessList always be present in type 1 transactions? i.e. can you specify
// a transaction as type 1 but not include the access list, or will a node reject that?
accessList: tx.accessList,
};
} else if (tx.type === 2) {
// 0x02 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, v, r, s])
txData = {
// TODO similar question to type 1, verify this is the correct set of fields to sign over
nonce: tx.nonce,
maxPriorityFeePerGas: tx.maxPriorityFeePerGas,
maxFeePerGas: tx.maxFeePerGas,
gasLimit: tx.gasLimit,
to: tx.to,
value: tx.value,
data: tx.data,
accessList: tx.accessList,
};
} else {
throw new Error(`Unsupported transaction type: ${tx.type}`);
}
// Add chainId and type fields if present
if (tx.chainId) {
// TODO verify that `chainId` will not always be present
txData.chainId = tx.chainId;
}
if (tx.type && tx.type >= 0) {
// TODO anecdotally it seems every tx will have a defined type field, but is this true?
txData.type = tx.type;
}
// Properly format transaction payload to get the correct message
const resolvedTx = await resolveProperties(txData);
const rawTx = serializeTransaction(resolvedTx);
const msgHash = keccak256(rawTx);
// Recover sender's public key
// Even though the type definitions say v,r,s are optional, they will always be defined: https://github.com/ethers-io/ethers.js/issues/1181
const signature = new Signature(BigInt(tx.r), BigInt(tx.s));
signature.assertValidity();
const recoveryParam = splitSignature({ r: tx.r as string, s: tx.s, v: tx.v }).recoveryParam;
const publicKey = recoverPublicKey(msgHash.slice(2), signature.toHex(), recoveryParam); // without 0x prefix
if (!publicKey) throw new Error('Could not recover public key');
return `0x${publicKey}`;
} |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 3 replies
-
As long as you pass the |
Beta Was this translation helpful? Give feedback.
As long as you pass the
type
in the transaction, it should automatically serialized the transaction correctly while computing the normalized hash.