From 1b1ed4f5e19a5482f47d6642f0ef4fa0d841eee8 Mon Sep 17 00:00:00 2001 From: Amp Date: Wed, 11 Nov 2026 03:35:56 +0000 Subject: [PATCH] fix(0.x): derive chain ID from `v` in `TxEnvelopeLegacy` presign hash Amp-Thread-ID: https://ampcode.com/threads/T-01a1c85c-51cb-75e9-8269-b23f8649f1c8 Co-authored-by: jxom --- .../tx-envelope-legacy-presign-chain-id.md | 5 +++ src/core/TxEnvelopeLegacy.ts | 7 +++ .../_test/TransactionEnvelopeLegacy.test.ts | 44 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 .changeset/tx-envelope-legacy-presign-chain-id.md diff --git a/.changeset/tx-envelope-legacy-presign-chain-id.md b/.changeset/tx-envelope-legacy-presign-chain-id.md new file mode 100644 index 000000000..a63a867a3 --- /dev/null +++ b/.changeset/tx-envelope-legacy-presign-chain-id.md @@ -0,0 +1,5 @@ +--- +"ox": patch +--- + +Fixed `TxEnvelopeLegacy.hash` (with `presign`) and `TxEnvelopeLegacy.getSignPayload` to derive the chain ID from an EIP-155 `v` when the envelope has no `chainId`. diff --git a/src/core/TxEnvelopeLegacy.ts b/src/core/TxEnvelopeLegacy.ts index 65560ab90..94fb48d5d 100644 --- a/src/core/TxEnvelopeLegacy.ts +++ b/src/core/TxEnvelopeLegacy.ts @@ -380,11 +380,18 @@ export function hash( options: hash.Options = {}, ): hash.ReturnType { const { presign } = options + const { chainId, v } = envelope return Hash.keccak256( serialize({ ...envelope, ...(presign ? { + // An EIP-155 `v` carries the chain ID when `chainId` is absent. + chainId: + chainId ?? + (typeof v === 'number' && v >= 37 + ? Math.floor((v - 35) / 2) + : undefined), r: undefined, s: undefined, yParity: undefined, diff --git a/src/core/_test/TransactionEnvelopeLegacy.test.ts b/src/core/_test/TransactionEnvelopeLegacy.test.ts index 94e387e47..5c30b4ca1 100644 --- a/src/core/_test/TransactionEnvelopeLegacy.test.ts +++ b/src/core/_test/TransactionEnvelopeLegacy.test.ts @@ -509,6 +509,50 @@ describe('hash', () => { `"0x5e427e088ae00b084b41e198c52440aa43b2d6f8f1f01246fd25d4e9b6ebfdab"`, ) }) + + test('options: presign: chain ID carried by `v`', () => { + const unsigned = { + gas: 21000n, + gasPrice: 1000000000n, + nonce: 665n, + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: 1000000000000000000n, + } as const + const payload = TxEnvelopeLegacy.getSignPayload( + TxEnvelopeLegacy.from({ ...unsigned, chainId: 1 }), + ) + const { r, s, yParity } = Secp256k1.sign({ + payload, + privateKey: accounts[0].privateKey, + }) + + // EIP-155 `v`, no `chainId` + const envelope = TxEnvelopeLegacy.from({ + ...unsigned, + r, + s, + v: 37 + yParity, + }) + expect( + TxEnvelopeLegacy.hash(envelope, { presign: true }), + ).toMatchInlineSnapshot( + `"0x4c1dec0d90aa1a17cb0aa735b5550e43a1bd27cd1cacb8987522576fa6220e46"`, + ) + expect(TxEnvelopeLegacy.hash(envelope, { presign: true })).toBe(payload) + expect(TxEnvelopeLegacy.getSignPayload(envelope)).toBe( + TxEnvelopeLegacy.getSignPayload( + TxEnvelopeLegacy.deserialize(TxEnvelopeLegacy.serialize(envelope)), + ), + ) + + // pre-EIP-155 `v`, no `chainId` + expect( + TxEnvelopeLegacy.hash( + TxEnvelopeLegacy.from({ ...unsigned, r, s, v: 27 + yParity }), + { presign: true }, + ), + ).toBe(TxEnvelopeLegacy.getSignPayload(TxEnvelopeLegacy.from(unsigned))) + }) }) describe('serialize', () => {