Skip to content

Commit e500612

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): allow verifying replay protection signatures with keys
Add support for verifying replay protection signatures using a wallet key directly through the main verifySignature method, rather than requiring the deprecated verifyReplayProtectionSignature method. This adds cleaner support for replay protection verification with the same API as normal signature verification. Issue: BTC-2786 Co-authored-by: llm-git <[email protected]>
1 parent 9d0c9d5 commit e500612

File tree

4 files changed

+56
-15
lines changed

4 files changed

+56
-15
lines changed

packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ export class BitGoPsbt {
152152
}
153153

154154
/**
155+
* @deprecated - use verifySignature with the replay protection key instead
156+
*
155157
* Verify if a replay protection input has a valid signature.
156158
*
157159
* This method checks if a given input is a replay protection input (like P2shP2pk) and verifies

packages/wasm-utxo/test/fixedScript/fixtureUtil.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import assert from "node:assert";
12
import * as fs from "node:fs";
23
import * as path from "node:path";
34
import { fileURLToPath } from "node:url";
45
import { dirname } from "node:path";
56
import type { IWalletKeys } from "../../js/fixedScriptWallet/RootWalletKeys.js";
67
import { BIP32, type BIP32Interface } from "../../js/bip32.js";
78
import { RootWalletKeys } from "../../js/fixedScriptWallet/RootWalletKeys.js";
9+
import { ECPair } from "../../js/ecpair.js";
810

911
const __filename = fileURLToPath(import.meta.url);
1012
const __dirname = dirname(__filename);
@@ -119,17 +121,7 @@ export function loadPsbtFixture(network: string, signatureState: string): Fixtur
119121
/**
120122
* Load wallet keys from fixture
121123
*/
122-
export function loadWalletKeysFromFixture(network: string): RootWalletKeys {
123-
const fixturePath = path.join(
124-
__dirname,
125-
"..",
126-
"fixtures",
127-
"fixed-script",
128-
`psbt-lite.${network}.fullsigned.json`,
129-
);
130-
const fixtureContent = fs.readFileSync(fixturePath, "utf-8");
131-
const fixture = JSON.parse(fixtureContent) as Fixture;
132-
124+
export function loadWalletKeysFromFixture(fixture: Fixture): RootWalletKeys {
133125
// Parse xprvs and convert to xpubs
134126
const xpubs = fixture.walletKeys.map((xprv) => {
135127
const key = BIP32.fromBase58(xprv);
@@ -144,6 +136,14 @@ export function loadWalletKeysFromFixture(network: string): RootWalletKeys {
144136
return RootWalletKeys.from(walletKeysLike);
145137
}
146138

139+
export function loadReplayProtectionKeyFromFixture(fixture: Fixture): ECPair {
140+
// underived user key
141+
const userBip32 = BIP32.fromBase58(fixture.walletKeys[0]);
142+
assert(userBip32.privateKey);
143+
const userECPair = ECPair.fromPrivateKey(Buffer.from(userBip32.privateKey));
144+
return userECPair;
145+
}
146+
147147
/**
148148
* Get extracted transaction hex from fixture
149149
*/

packages/wasm-utxo/test/fixedScript/parseTransactionWithWalletKeys.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe("parseTransactionWithWalletKeys", function () {
6666
fixture = loadPsbtFixture(networkName, "fullsigned");
6767
fullsignedPsbtBytes = getPsbtBuffer(fixture);
6868
bitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(fullsignedPsbtBytes, networkName);
69-
rootWalletKeys = loadWalletKeysFromFixture(networkName);
69+
rootWalletKeys = loadWalletKeysFromFixture(fixture);
7070
});
7171

7272
it("should have matching unsigned transaction ID", function () {

packages/wasm-utxo/test/fixedScript/verifySignature.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import assert from "node:assert";
22
import * as utxolib from "@bitgo/utxo-lib";
3-
import { fixedScriptWallet, BIP32 } from "../../js/index.js";
4-
import { BitGoPsbt, RootWalletKeys } from "../../js/fixedScriptWallet/index.js";
3+
import { fixedScriptWallet, BIP32, ECPair } from "../../js/index.js";
4+
import { BitGoPsbt, RootWalletKeys, ParsedTransaction } from "../../js/fixedScriptWallet/index.js";
55
import {
66
loadPsbtFixture,
77
loadWalletKeysFromFixture,
88
getPsbtBuffer,
99
type Fixture,
10+
loadReplayProtectionKeyFromFixture,
1011
} from "./fixtureUtil.js";
1112

1213
type SignatureStage = "unsigned" | "halfsigned" | "fullsigned";
@@ -61,7 +62,9 @@ function getExpectedSignatures(
6162
*/
6263
function verifyInputSignatures(
6364
bitgoPsbt: BitGoPsbt,
65+
parsed: ParsedTransaction,
6466
rootWalletKeys: RootWalletKeys,
67+
replayProtectionKey: ECPair,
6568
inputIndex: number,
6669
expectedSignatures: ExpectedSignatures,
6770
): void {
@@ -82,6 +85,20 @@ function verifyInputSignatures(
8285
return;
8386
}
8487

88+
if (parsed.inputs[inputIndex].scriptType === "p2shP2pk") {
89+
const hasReplaySig = bitgoPsbt.verifySignature(inputIndex, replayProtectionKey);
90+
assert.ok(
91+
"hasReplayProtectionSignature" in expectedSignatures,
92+
"Expected hasReplayProtectionSignature to be present",
93+
);
94+
assert.strictEqual(
95+
hasReplaySig,
96+
expectedSignatures.hasReplayProtectionSignature,
97+
`Input ${inputIndex} replay protection signature mismatch`,
98+
);
99+
return;
100+
}
101+
85102
// Handle standard multisig inputs
86103
const hasUserSig = bitgoPsbt.verifySignature(inputIndex, rootWalletKeys.userKey());
87104
const hasBackupSig = bitgoPsbt.verifySignature(inputIndex, rootWalletKeys.backupKey());
@@ -121,18 +138,25 @@ describe("verifySignature", function () {
121138

122139
describe(`network: ${networkName}`, function () {
123140
let rootWalletKeys: RootWalletKeys;
141+
let replayProtectionKey: ECPair;
124142
let unsignedFixture: Fixture;
125143
let halfsignedFixture: Fixture;
126144
let fullsignedFixture: Fixture;
127145
let unsignedBitgoPsbt: BitGoPsbt;
128146
let halfsignedBitgoPsbt: BitGoPsbt;
129147
let fullsignedBitgoPsbt: BitGoPsbt;
148+
let replayProtectionScript: Uint8Array;
130149

131150
before(function () {
132-
rootWalletKeys = loadWalletKeysFromFixture(networkName);
133151
unsignedFixture = loadPsbtFixture(networkName, "unsigned");
134152
halfsignedFixture = loadPsbtFixture(networkName, "halfsigned");
135153
fullsignedFixture = loadPsbtFixture(networkName, "fullsigned");
154+
rootWalletKeys = loadWalletKeysFromFixture(fullsignedFixture);
155+
replayProtectionKey = loadReplayProtectionKeyFromFixture(fullsignedFixture);
156+
replayProtectionScript = Buffer.from(
157+
"a91420b37094d82a513451ff0ccd9db23aba05bc5ef387",
158+
"hex",
159+
);
136160
unsignedBitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(
137161
getPsbtBuffer(unsignedFixture),
138162
networkName,
@@ -149,11 +173,16 @@ describe("verifySignature", function () {
149173

150174
describe("unsigned PSBT", function () {
151175
it("should return false for unsigned inputs", function () {
176+
const parsed = unsignedBitgoPsbt.parseTransactionWithWalletKeys(rootWalletKeys, {
177+
outputScripts: [replayProtectionScript],
178+
});
152179
// Verify all xpubs return false for all inputs
153180
unsignedFixture.psbtInputs.forEach((input, index) => {
154181
verifyInputSignatures(
155182
unsignedBitgoPsbt,
183+
parsed,
156184
rootWalletKeys,
185+
replayProtectionKey,
157186
index,
158187
getExpectedSignatures(input.type, "unsigned"),
159188
);
@@ -163,10 +192,15 @@ describe("verifySignature", function () {
163192

164193
describe("half-signed PSBT", function () {
165194
it("should return true for signed xpubs and false for unsigned", function () {
195+
const parsed = halfsignedBitgoPsbt.parseTransactionWithWalletKeys(rootWalletKeys, {
196+
outputScripts: [replayProtectionScript],
197+
});
166198
halfsignedFixture.psbtInputs.forEach((input, index) => {
167199
verifyInputSignatures(
168200
halfsignedBitgoPsbt,
201+
parsed,
169202
rootWalletKeys,
203+
replayProtectionKey,
170204
index,
171205
getExpectedSignatures(input.type, "halfsigned"),
172206
);
@@ -177,10 +211,15 @@ describe("verifySignature", function () {
177211
describe("fully signed PSBT", function () {
178212
it("should have 2 signatures (2-of-3 multisig)", function () {
179213
// In fullsigned fixtures, verify 2 signatures exist per multisig input
214+
const parsed = fullsignedBitgoPsbt.parseTransactionWithWalletKeys(rootWalletKeys, {
215+
outputScripts: [replayProtectionScript],
216+
});
180217
fullsignedFixture.psbtInputs.forEach((input, index) => {
181218
verifyInputSignatures(
182219
fullsignedBitgoPsbt,
220+
parsed,
183221
rootWalletKeys,
222+
replayProtectionKey,
184223
index,
185224
getExpectedSignatures(input.type, "fullsigned"),
186225
);

0 commit comments

Comments
 (0)