@bitgo/wasm-utxo 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/js/fixedScriptWallet/BitGoPsbt.d.ts +5 -0
- package/dist/cjs/js/wasm/wasm_utxo.js +14 -14
- package/dist/cjs/js/wasm/wasm_utxo_bg.wasm +0 -0
- package/dist/cjs/js/wasm/wasm_utxo_bg.wasm.d.ts +25 -25
- package/dist/esm/js/fixedScriptWallet/BitGoPsbt.d.ts +5 -0
- package/dist/esm/js/wasm/wasm_utxo_bg.js +14 -14
- package/dist/esm/js/wasm/wasm_utxo_bg.wasm +0 -0
- package/dist/esm/js/wasm/wasm_utxo_bg.wasm.d.ts +25 -25
- package/package.json +3 -3
- package/dist/cjs/package.json +0 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +0 -1
- package/dist/esm/test/address/utxolibCompat.d.ts +0 -1
- package/dist/esm/test/address/utxolibCompat.js +0 -107
- package/dist/esm/test/ast/formatNode.d.ts +0 -1
- package/dist/esm/test/ast/formatNode.js +0 -15
- package/dist/esm/test/bip32.d.ts +0 -1
- package/dist/esm/test/bip32.js +0 -242
- package/dist/esm/test/descriptorFixtures.d.ts +0 -25
- package/dist/esm/test/descriptorFixtures.js +0 -605
- package/dist/esm/test/descriptorUtil.d.ts +0 -13
- package/dist/esm/test/descriptorUtil.js +0 -52
- package/dist/esm/test/ecpair.d.ts +0 -1
- package/dist/esm/test/ecpair.js +0 -137
- package/dist/esm/test/fixedScript/address.d.ts +0 -1
- package/dist/esm/test/fixedScript/address.js +0 -66
- package/dist/esm/test/fixedScript/finalizeExtract.d.ts +0 -1
- package/dist/esm/test/fixedScript/finalizeExtract.js +0 -66
- package/dist/esm/test/fixedScript/fixtureUtil.d.ts +0 -103
- package/dist/esm/test/fixedScript/fixtureUtil.js +0 -65
- package/dist/esm/test/fixedScript/musig2Nonces.d.ts +0 -1
- package/dist/esm/test/fixedScript/musig2Nonces.js +0 -77
- package/dist/esm/test/fixedScript/parseTransactionWithWalletKeys.d.ts +0 -1
- package/dist/esm/test/fixedScript/parseTransactionWithWalletKeys.js +0 -168
- package/dist/esm/test/fixedScript/signAndVerifySignature.d.ts +0 -1
- package/dist/esm/test/fixedScript/signAndVerifySignature.js +0 -268
- package/dist/esm/test/fixedScript/walletKeys.util.d.ts +0 -12
- package/dist/esm/test/fixedScript/walletKeys.util.js +0 -17
- package/dist/esm/test/fixedScriptToDescriptor.d.ts +0 -1
- package/dist/esm/test/fixedScriptToDescriptor.js +0 -93
- package/dist/esm/test/fixtures.d.ts +0 -1
- package/dist/esm/test/fixtures.js +0 -16
- package/dist/esm/test/opdrop.d.ts +0 -1
- package/dist/esm/test/opdrop.js +0 -85
- package/dist/esm/test/psbt.util.d.ts +0 -8
- package/dist/esm/test/psbt.util.js +0 -113
- package/dist/esm/test/psbtFixedScriptCompat.d.ts +0 -1
- package/dist/esm/test/psbtFixedScriptCompat.js +0 -116
- package/dist/esm/test/psbtFixedScriptCompatFixtures.d.ts +0 -10
- package/dist/esm/test/psbtFixedScriptCompatFixtures.js +0 -53
- package/dist/esm/test/psbtFromDescriptor.d.ts +0 -1
- package/dist/esm/test/psbtFromDescriptor.js +0 -104
- package/dist/esm/test/psbtFromDescriptor.util.d.ts +0 -63
- package/dist/esm/test/psbtFromDescriptor.util.js +0 -101
- package/dist/esm/test/test.d.ts +0 -1
- package/dist/esm/test/test.js +0 -123
- package/dist/esm/tsconfig.tsbuildinfo +0 -1
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
|
-
import * as utxolib from "@bitgo/utxo-lib";
|
|
3
|
-
import { fixedScriptWallet } from "../../js/index.js";
|
|
4
|
-
import { loadPsbtFixture, loadWalletKeysFromFixture, getPsbtBuffer, loadReplayProtectionKeyFromFixture, } from "./fixtureUtil.js";
|
|
5
|
-
function getExpectedInputScriptType(fixtureScriptType) {
|
|
6
|
-
// Map fixture types to InputScriptType values
|
|
7
|
-
// Based on the Rust mapping in src/fixed_script_wallet/test_utils/fixtures.rs
|
|
8
|
-
switch (fixtureScriptType) {
|
|
9
|
-
case "p2shP2pk":
|
|
10
|
-
case "p2sh":
|
|
11
|
-
case "p2shP2wsh":
|
|
12
|
-
case "p2wsh":
|
|
13
|
-
return fixtureScriptType;
|
|
14
|
-
case "p2tr":
|
|
15
|
-
return "p2trLegacy";
|
|
16
|
-
case "p2trMusig2":
|
|
17
|
-
return "p2trMusig2ScriptPath";
|
|
18
|
-
case "taprootKeyPathSpend":
|
|
19
|
-
return "p2trMusig2KeyPath";
|
|
20
|
-
default:
|
|
21
|
-
throw new Error(`Unknown fixture script type: ${fixtureScriptType}`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function getOtherWalletKeys() {
|
|
25
|
-
const otherWalletKeys = utxolib.testutil.getKeyTriple("too many secrets");
|
|
26
|
-
return new utxolib.bitgo.RootWalletKeys(otherWalletKeys);
|
|
27
|
-
}
|
|
28
|
-
describe("parseTransactionWithWalletKeys", function () {
|
|
29
|
-
const supportedNetworks = utxolib.getNetworkList().filter((network) => {
|
|
30
|
-
return (utxolib.isMainnet(network) &&
|
|
31
|
-
network !== utxolib.networks.bitcoincash &&
|
|
32
|
-
network !== utxolib.networks.bitcoingold &&
|
|
33
|
-
network !== utxolib.networks.bitcoinsv &&
|
|
34
|
-
network !== utxolib.networks.ecash &&
|
|
35
|
-
network !== utxolib.networks.zcash);
|
|
36
|
-
});
|
|
37
|
-
supportedNetworks.forEach((network) => {
|
|
38
|
-
const networkName = utxolib.getNetworkName(network);
|
|
39
|
-
describe(`network: ${networkName}`, function () {
|
|
40
|
-
let fullsignedPsbtBytes;
|
|
41
|
-
let bitgoPsbt;
|
|
42
|
-
let rootWalletKeys;
|
|
43
|
-
let replayProtectionKey;
|
|
44
|
-
let fixture;
|
|
45
|
-
before(function () {
|
|
46
|
-
fixture = loadPsbtFixture(networkName, "fullsigned");
|
|
47
|
-
fullsignedPsbtBytes = getPsbtBuffer(fixture);
|
|
48
|
-
bitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(fullsignedPsbtBytes, networkName);
|
|
49
|
-
rootWalletKeys = loadWalletKeysFromFixture(fixture);
|
|
50
|
-
replayProtectionKey = loadReplayProtectionKeyFromFixture(fixture);
|
|
51
|
-
});
|
|
52
|
-
it("should have matching unsigned transaction ID", function () {
|
|
53
|
-
const unsignedTxid = bitgoPsbt.unsignedTxid();
|
|
54
|
-
const expectedUnsignedTxid = utxolib.bitgo
|
|
55
|
-
.createPsbtFromBuffer(fullsignedPsbtBytes, network)
|
|
56
|
-
.getUnsignedTx()
|
|
57
|
-
.getId();
|
|
58
|
-
assert.strictEqual(unsignedTxid, expectedUnsignedTxid);
|
|
59
|
-
});
|
|
60
|
-
it("should parse transaction and identify internal/external outputs", function () {
|
|
61
|
-
const parsed = bitgoPsbt.parseTransactionWithWalletKeys(rootWalletKeys, {
|
|
62
|
-
publicKeys: [replayProtectionKey],
|
|
63
|
-
});
|
|
64
|
-
// Verify all inputs have addresses and values
|
|
65
|
-
parsed.inputs.forEach((input, i) => {
|
|
66
|
-
assert.ok(input.address, `Input ${i} should have an address`);
|
|
67
|
-
assert.ok(typeof input.value === "bigint", `Input ${i} value should be bigint`);
|
|
68
|
-
assert.ok(input.value > 0n, `Input ${i} value should be > 0`);
|
|
69
|
-
});
|
|
70
|
-
// Validate outputs
|
|
71
|
-
assert.ok(parsed.outputs.length > 0, "Should have at least one output");
|
|
72
|
-
// Count internal outputs (scriptId is defined and not null)
|
|
73
|
-
const internalOutputs = parsed.outputs.filter((o) => o.scriptId);
|
|
74
|
-
// Count external outputs (scriptId is null or undefined)
|
|
75
|
-
const externalOutputs = parsed.outputs.filter((o) => o.scriptId === null);
|
|
76
|
-
assert.ok(externalOutputs.every((o) => o.address || o.script));
|
|
77
|
-
const nonAddressOutputs = externalOutputs.filter((o) => o.address === null);
|
|
78
|
-
assert.strictEqual(nonAddressOutputs.length, 1);
|
|
79
|
-
const [opReturnOutput] = nonAddressOutputs;
|
|
80
|
-
const expectedOpReturn = utxolib.payments.embed({
|
|
81
|
-
data: [Buffer.from("setec astronomy")],
|
|
82
|
-
}).output;
|
|
83
|
-
assert.strictEqual(Buffer.from(opReturnOutput.script).toString("hex"), expectedOpReturn.toString("hex"));
|
|
84
|
-
// Fixtures now have 3 external outputs
|
|
85
|
-
assert.ok(internalOutputs.length > 0, "Should have internal outputs (have scriptId)");
|
|
86
|
-
assert.strictEqual(externalOutputs.length, 3, "Should have 3 external outputs in test fixture");
|
|
87
|
-
// Verify all outputs have proper structure
|
|
88
|
-
parsed.outputs.forEach((output, i) => {
|
|
89
|
-
assert.ok(output.script instanceof Uint8Array, `Output ${i} script should be Uint8Array`);
|
|
90
|
-
assert.ok(typeof output.value === "bigint", `Output ${i} value should be bigint`);
|
|
91
|
-
assert.ok(output.value > 0n, `Output ${i} value should be > 0`);
|
|
92
|
-
// Address is optional for non-standard scripts
|
|
93
|
-
});
|
|
94
|
-
// Verify spend amount (should be > 0 since there are external outputs)
|
|
95
|
-
assert.strictEqual(parsed.spendAmount, 900n * 3n);
|
|
96
|
-
// Verify miner fee calculation
|
|
97
|
-
const totalInputValue = parsed.inputs.reduce((sum, i) => sum + i.value, 0n);
|
|
98
|
-
const totalOutputValue = parsed.outputs.reduce((sum, o) => sum + o.value, 0n);
|
|
99
|
-
assert.strictEqual(parsed.minerFee, totalInputValue - totalOutputValue, "Miner fee should equal inputs minus outputs");
|
|
100
|
-
assert.ok(parsed.minerFee > 0n, "Miner fee should be > 0");
|
|
101
|
-
// Verify virtual size
|
|
102
|
-
assert.ok(typeof parsed.virtualSize === "number", "Virtual size should be a number");
|
|
103
|
-
assert.ok(parsed.virtualSize > 0, "Virtual size should be > 0");
|
|
104
|
-
});
|
|
105
|
-
it("should parse inputs with correct scriptType", function () {
|
|
106
|
-
const parsed = bitgoPsbt.parseTransactionWithWalletKeys(rootWalletKeys, {
|
|
107
|
-
publicKeys: [replayProtectionKey],
|
|
108
|
-
});
|
|
109
|
-
// Verify all inputs have scriptType matching fixture
|
|
110
|
-
parsed.inputs.forEach((input, i) => {
|
|
111
|
-
const fixtureInput = fixture.psbtInputs[i];
|
|
112
|
-
const expectedScriptType = getExpectedInputScriptType(fixtureInput.type);
|
|
113
|
-
assert.strictEqual(input.scriptType, expectedScriptType, `Input ${i} scriptType should be ${expectedScriptType}, got ${input.scriptType}`);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
it("should fail to parse with other wallet keys", function () {
|
|
117
|
-
assert.throws(() => {
|
|
118
|
-
bitgoPsbt.parseTransactionWithWalletKeys(getOtherWalletKeys(), {
|
|
119
|
-
publicKeys: [replayProtectionKey],
|
|
120
|
-
});
|
|
121
|
-
}, (error) => {
|
|
122
|
-
return error.message.includes("Failed to parse transaction: Input 0: wallet validation failed");
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
it("should recognize output for other wallet keys", function () {
|
|
126
|
-
const parsedOutputs = bitgoPsbt.parseOutputsWithWalletKeys(getOtherWalletKeys());
|
|
127
|
-
// Should return an array of parsed outputs
|
|
128
|
-
assert.ok(Array.isArray(parsedOutputs), "Should return an array");
|
|
129
|
-
assert.ok(parsedOutputs.length > 0, "Should have at least one output");
|
|
130
|
-
// Verify all outputs have proper structure
|
|
131
|
-
parsedOutputs.forEach((output, i) => {
|
|
132
|
-
assert.ok(output.script instanceof Uint8Array, `Output ${i} script should be Uint8Array`);
|
|
133
|
-
assert.ok(typeof output.value === "bigint", `Output ${i} value should be bigint`);
|
|
134
|
-
assert.ok(output.value > 0n, `Output ${i} value should be > 0`);
|
|
135
|
-
// Address can be null for non-standard scripts
|
|
136
|
-
assert.ok(typeof output.address === "string" || output.address === null, `Output ${i} address should be string or null`);
|
|
137
|
-
// scriptId can be null for external outputs
|
|
138
|
-
assert.ok(output.scriptId === null ||
|
|
139
|
-
(typeof output.scriptId === "object" &&
|
|
140
|
-
typeof output.scriptId.chain === "number" &&
|
|
141
|
-
typeof output.scriptId.index === "number"), `Output ${i} scriptId should be null or an object with chain and index`);
|
|
142
|
-
});
|
|
143
|
-
// Compare with the original wallet keys to verify we get different results
|
|
144
|
-
const originalParsedOutputs = bitgoPsbt.parseOutputsWithWalletKeys(rootWalletKeys);
|
|
145
|
-
// Should have the same number of outputs
|
|
146
|
-
assert.strictEqual(parsedOutputs.length, originalParsedOutputs.length, "Should parse the same number of outputs");
|
|
147
|
-
// Find outputs that belong to the other wallet keys (scriptId !== null)
|
|
148
|
-
const otherWalletOutputs = parsedOutputs.filter((o) => o.scriptId !== null);
|
|
149
|
-
// Should have exactly one output for the other wallet keys
|
|
150
|
-
assert.strictEqual(otherWalletOutputs.length, 1, "Should have exactly one output belonging to the other wallet keys");
|
|
151
|
-
// Verify that this output is marked as external (scriptId === null) under regular wallet keys
|
|
152
|
-
const otherWalletOutputIndex = parsedOutputs.findIndex((o) => o.scriptId !== null);
|
|
153
|
-
const sameOutputWithRegularKeys = originalParsedOutputs[otherWalletOutputIndex];
|
|
154
|
-
assert.strictEqual(sameOutputWithRegularKeys.scriptId, null, "The output belonging to other wallet keys should be marked as external (scriptId === null) when parsed with regular wallet keys");
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
describe("error handling", function () {
|
|
159
|
-
it("should throw error for invalid PSBT bytes", function () {
|
|
160
|
-
const invalidBytes = new Uint8Array([0x00, 0x01, 0x02]);
|
|
161
|
-
assert.throws(() => {
|
|
162
|
-
fixedScriptWallet.BitGoPsbt.fromBytes(invalidBytes, "bitcoin");
|
|
163
|
-
}, (error) => {
|
|
164
|
-
return error.message.includes("Failed to deserialize PSBT");
|
|
165
|
-
}, "Should throw error for invalid PSBT bytes");
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
|
-
import * as utxolib from "@bitgo/utxo-lib";
|
|
3
|
-
import { BIP32 } from "../../js/index.js";
|
|
4
|
-
import { loadPsbtFixture, loadWalletKeysFromFixture, getBitGoPsbt, loadReplayProtectionKeyFromFixture, } from "./fixtureUtil.js";
|
|
5
|
-
/**
|
|
6
|
-
* Load xprivs from a fixture
|
|
7
|
-
* @param fixture - The test fixture
|
|
8
|
-
* @returns The xprivs for user, backup, and bitgo keys
|
|
9
|
-
*/
|
|
10
|
-
function loadXprivsFromFixture(fixture) {
|
|
11
|
-
const [userXpriv, backupXpriv, bitgoXpriv] = fixture.walletKeys.map((xprv) => BIP32.fromBase58(xprv));
|
|
12
|
-
return {
|
|
13
|
-
user: userXpriv,
|
|
14
|
-
backup: backupXpriv,
|
|
15
|
-
bitgo: bitgoXpriv,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Get expected signature state for an input based on type and signing stage
|
|
20
|
-
* @param inputType - The type of input (e.g., "p2shP2pk", "p2trMusig2", "taprootKeyPathSpend")
|
|
21
|
-
* @param signatureStage - The signing stage (unsigned, halfsigned, fullsigned)
|
|
22
|
-
* @returns Expected signature state for replay protection OR multi-key signatures
|
|
23
|
-
*/
|
|
24
|
-
function getExpectedSignatures(inputType, signatureStage) {
|
|
25
|
-
// p2shP2pk inputs use replay protection signature verification
|
|
26
|
-
if (inputType === "p2shP2pk") {
|
|
27
|
-
return {
|
|
28
|
-
hasReplayProtectionSignature: signatureStage === "halfsigned" || signatureStage === "fullsigned",
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
switch (signatureStage) {
|
|
32
|
-
case "unsigned":
|
|
33
|
-
return { user: false, backup: false, bitgo: false };
|
|
34
|
-
case "halfsigned":
|
|
35
|
-
// User signs first
|
|
36
|
-
return { user: true, backup: false, bitgo: false };
|
|
37
|
-
case "fullsigned":
|
|
38
|
-
// p2trMusig2 uses user + backup for 2-of-2 MuSig2
|
|
39
|
-
if (inputType === "p2trMusig2") {
|
|
40
|
-
return { user: true, backup: true, bitgo: false };
|
|
41
|
-
}
|
|
42
|
-
// Regular multisig uses user + bitgo
|
|
43
|
-
return { user: true, backup: false, bitgo: true };
|
|
44
|
-
default:
|
|
45
|
-
throw new Error(`Unknown signature stage: ${String(signatureStage)}`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Verify signature state for a specific input in a PSBT
|
|
50
|
-
* @param bitgoPsbt - The PSBT to verify
|
|
51
|
-
* @param rootWalletKeys - Wallet keys for verification
|
|
52
|
-
* @param inputIndex - The input index to verify
|
|
53
|
-
* @param inputType - The type of input (for replay protection handling)
|
|
54
|
-
* @param expectedSignatures - Expected signature state for each key or replay protection
|
|
55
|
-
*/
|
|
56
|
-
function verifyInputSignatures(bitgoPsbt, parsed, rootWalletKeys, replayProtectionKey, inputIndex, expectedSignatures) {
|
|
57
|
-
// Handle replay protection inputs (P2shP2pk)
|
|
58
|
-
if ("hasReplayProtectionSignature" in expectedSignatures) {
|
|
59
|
-
const hasReplaySig = bitgoPsbt.verifyReplayProtectionSignature(inputIndex, {
|
|
60
|
-
publicKeys: [replayProtectionKey],
|
|
61
|
-
});
|
|
62
|
-
assert.strictEqual(hasReplaySig, expectedSignatures.hasReplayProtectionSignature, `Input ${inputIndex} replay protection signature mismatch`);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (parsed.inputs[inputIndex].scriptType === "p2shP2pk") {
|
|
66
|
-
const hasReplaySig = bitgoPsbt.verifySignature(inputIndex, replayProtectionKey);
|
|
67
|
-
assert.ok("hasReplayProtectionSignature" in expectedSignatures, "Expected hasReplayProtectionSignature to be present");
|
|
68
|
-
assert.strictEqual(hasReplaySig, expectedSignatures.hasReplayProtectionSignature, `Input ${inputIndex} replay protection signature mismatch`);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
// Handle standard multisig inputs
|
|
72
|
-
const hasUserSig = bitgoPsbt.verifySignature(inputIndex, rootWalletKeys.userKey());
|
|
73
|
-
const hasBackupSig = bitgoPsbt.verifySignature(inputIndex, rootWalletKeys.backupKey());
|
|
74
|
-
const hasBitGoSig = bitgoPsbt.verifySignature(inputIndex, rootWalletKeys.bitgoKey());
|
|
75
|
-
const scriptType = parsed.inputs[inputIndex].scriptType;
|
|
76
|
-
assert.strictEqual(hasUserSig, expectedSignatures.user, `Input ${inputIndex} user key signature mismatch type=${scriptType}`);
|
|
77
|
-
assert.strictEqual(hasBackupSig, expectedSignatures.backup, `Input ${inputIndex} backup key signature mismatch type=${scriptType}`);
|
|
78
|
-
assert.strictEqual(hasBitGoSig, expectedSignatures.bitgo, `Input ${inputIndex} BitGo key signature mismatch type=${scriptType}`);
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Helper to verify signatures for all inputs in a PSBT
|
|
82
|
-
* @param bitgoPsbt - The PSBT to verify
|
|
83
|
-
* @param fixture - The test fixture containing input metadata
|
|
84
|
-
* @param rootWalletKeys - Wallet keys for verification
|
|
85
|
-
* @param replayProtectionKey - Key for replay protection inputs
|
|
86
|
-
* @param signatureStage - The signing stage (unsigned, halfsigned, fullsigned)
|
|
87
|
-
*/
|
|
88
|
-
function verifyAllInputSignatures(bitgoPsbt, fixture, rootWalletKeys, replayProtectionKey, signatureStage) {
|
|
89
|
-
const parsed = bitgoPsbt.parseTransactionWithWalletKeys(rootWalletKeys, {
|
|
90
|
-
publicKeys: [replayProtectionKey],
|
|
91
|
-
});
|
|
92
|
-
fixture.psbtInputs.forEach((input, index) => {
|
|
93
|
-
verifyInputSignatures(bitgoPsbt, parsed, rootWalletKeys, replayProtectionKey, index, getExpectedSignatures(input.type, signatureStage));
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
function signInputAndVerify(bitgoPsbt, index, key, keyName, inputType) {
|
|
97
|
-
bitgoPsbt.sign(index, key);
|
|
98
|
-
assert.strictEqual(bitgoPsbt.verifySignature(index, key), true, `Input ${index} signature mismatch key=${keyName} type=${inputType}`);
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Sign all inputs in a PSBT according to the signature stage
|
|
102
|
-
* @param bitgoPsbt - The PSBT to sign
|
|
103
|
-
* @param rootWalletKeys - Wallet keys for parsing the transaction
|
|
104
|
-
* @param xprivs - The xprivs to use for signing
|
|
105
|
-
* @param replayProtectionKey - The ECPair for signing replay protection (p2shP2pk) inputs
|
|
106
|
-
*/
|
|
107
|
-
function signAllInputs(bitgoPsbt, rootWalletKeys, xprivs, replayProtectionKey) {
|
|
108
|
-
// Parse transaction to get input types
|
|
109
|
-
const parsed = bitgoPsbt.parseTransactionWithWalletKeys(rootWalletKeys, {
|
|
110
|
-
publicKeys: [replayProtectionKey],
|
|
111
|
-
});
|
|
112
|
-
// Generate MuSig2 nonces for user and backup keys (MuSig2 uses 2-of-2 with user+backup)
|
|
113
|
-
bitgoPsbt.generateMusig2Nonces(xprivs.user);
|
|
114
|
-
bitgoPsbt.generateMusig2Nonces(xprivs.bitgo);
|
|
115
|
-
// First pass: sign with user key (skip p2shP2pk inputs)
|
|
116
|
-
parsed.inputs.forEach((input, index) => {
|
|
117
|
-
switch (input.scriptType) {
|
|
118
|
-
case "p2shP2pk":
|
|
119
|
-
break;
|
|
120
|
-
default:
|
|
121
|
-
signInputAndVerify(bitgoPsbt, index, xprivs.user, "user", input.scriptType);
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
// Second pass: sign with appropriate second key
|
|
126
|
-
parsed.inputs.forEach((input, index) => {
|
|
127
|
-
switch (input.scriptType) {
|
|
128
|
-
case "p2shP2pk":
|
|
129
|
-
signInputAndVerify(bitgoPsbt, index, replayProtectionKey, "replayProtection", input.scriptType);
|
|
130
|
-
break;
|
|
131
|
-
case "p2trMusig2ScriptPath":
|
|
132
|
-
// MuSig2 script path inputs use backup key for second signature
|
|
133
|
-
signInputAndVerify(bitgoPsbt, index, xprivs.backup, "backup", input.scriptType);
|
|
134
|
-
break;
|
|
135
|
-
default:
|
|
136
|
-
// Regular multisig uses bitgo key
|
|
137
|
-
signInputAndVerify(bitgoPsbt, index, xprivs.bitgo, "bitgo", input.scriptType);
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Run tests for a fixture: load PSBT, verify, sign, and verify again
|
|
144
|
-
* @param fixture - The test fixture
|
|
145
|
-
* @param networkName - The network name for deserializing the PSBT
|
|
146
|
-
* @param rootWalletKeys - Wallet keys for verification
|
|
147
|
-
* @param replayProtectionKey - Key for replay protection inputs
|
|
148
|
-
* @param xprivs - The xprivs to use for signing
|
|
149
|
-
* @param signatureStage - The current signing stage
|
|
150
|
-
*/
|
|
151
|
-
function runTestsForFixture(fixture, networkName, rootWalletKeys, replayProtectionKey, xprivs, signatureStage) {
|
|
152
|
-
// Load PSBT from fixture
|
|
153
|
-
const bitgoPsbt = getBitGoPsbt(fixture, networkName);
|
|
154
|
-
// Verify current state
|
|
155
|
-
verifyAllInputSignatures(bitgoPsbt, fixture, rootWalletKeys, replayProtectionKey, signatureStage);
|
|
156
|
-
// Sign inputs (if not already fully signed)
|
|
157
|
-
if (signatureStage !== "unsigned") {
|
|
158
|
-
signAllInputs(bitgoPsbt, rootWalletKeys, xprivs, replayProtectionKey);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
describe("verifySignature", function () {
|
|
162
|
-
const supportedNetworks = utxolib.getNetworkList().filter((network) => {
|
|
163
|
-
return (utxolib.isMainnet(network) &&
|
|
164
|
-
network !== utxolib.networks.bitcoincash &&
|
|
165
|
-
network !== utxolib.networks.bitcoingold &&
|
|
166
|
-
network !== utxolib.networks.bitcoinsv &&
|
|
167
|
-
network !== utxolib.networks.ecash &&
|
|
168
|
-
network !== utxolib.networks.zcash);
|
|
169
|
-
});
|
|
170
|
-
supportedNetworks.forEach((network) => {
|
|
171
|
-
const networkName = utxolib.getNetworkName(network);
|
|
172
|
-
describe(`network: ${networkName}`, function () {
|
|
173
|
-
let rootWalletKeys;
|
|
174
|
-
let replayProtectionKey;
|
|
175
|
-
let xprivs;
|
|
176
|
-
let unsignedFixture;
|
|
177
|
-
let halfsignedFixture;
|
|
178
|
-
let fullsignedFixture;
|
|
179
|
-
before(function () {
|
|
180
|
-
unsignedFixture = loadPsbtFixture(networkName, "unsigned");
|
|
181
|
-
halfsignedFixture = loadPsbtFixture(networkName, "halfsigned");
|
|
182
|
-
fullsignedFixture = loadPsbtFixture(networkName, "fullsigned");
|
|
183
|
-
rootWalletKeys = loadWalletKeysFromFixture(fullsignedFixture);
|
|
184
|
-
replayProtectionKey = loadReplayProtectionKeyFromFixture(fullsignedFixture);
|
|
185
|
-
xprivs = loadXprivsFromFixture(fullsignedFixture);
|
|
186
|
-
});
|
|
187
|
-
describe("unsigned PSBT", function () {
|
|
188
|
-
it("should return false for unsigned inputs, then sign and verify", function () {
|
|
189
|
-
runTestsForFixture(unsignedFixture, networkName, rootWalletKeys, replayProtectionKey, xprivs, "unsigned");
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
describe("half-signed PSBT", function () {
|
|
193
|
-
it("should return true for signed xpubs and false for unsigned, then sign and verify", function () {
|
|
194
|
-
runTestsForFixture(halfsignedFixture, networkName, rootWalletKeys, replayProtectionKey, xprivs, "halfsigned");
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
describe("fully signed PSBT", function () {
|
|
198
|
-
it("should have 2 signatures (2-of-3 multisig)", function () {
|
|
199
|
-
runTestsForFixture(fullsignedFixture, networkName, rootWalletKeys, replayProtectionKey, xprivs, "fullsigned");
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
describe("error handling", function () {
|
|
203
|
-
it("should throw error for out of bounds input index", function () {
|
|
204
|
-
const psbt = getBitGoPsbt(fullsignedFixture, networkName);
|
|
205
|
-
assert.throws(() => {
|
|
206
|
-
psbt.verifySignature(999, rootWalletKeys.userKey());
|
|
207
|
-
}, (error) => {
|
|
208
|
-
return error.message.includes("Input index 999 out of bounds");
|
|
209
|
-
}, "Should throw error for out of bounds input index");
|
|
210
|
-
});
|
|
211
|
-
it("should throw error for invalid xpub", function () {
|
|
212
|
-
const psbt = getBitGoPsbt(fullsignedFixture, networkName);
|
|
213
|
-
assert.throws(() => {
|
|
214
|
-
psbt.verifySignature(0, "invalid-xpub");
|
|
215
|
-
}, (error) => {
|
|
216
|
-
return error.message.includes("Invalid");
|
|
217
|
-
}, "Should throw error for invalid xpub");
|
|
218
|
-
});
|
|
219
|
-
it("should return false for xpub not in derivation path", function () {
|
|
220
|
-
const psbt = getBitGoPsbt(fullsignedFixture, networkName);
|
|
221
|
-
// Create a different xpub that's not in the wallet
|
|
222
|
-
// Use a proper 32-byte seed (256 bits)
|
|
223
|
-
const differentSeed = Buffer.alloc(32, 0xaa); // 32 bytes filled with 0xaa
|
|
224
|
-
const differentKey = BIP32.fromSeed(differentSeed);
|
|
225
|
-
const differentXpub = differentKey.neutered();
|
|
226
|
-
const result = psbt.verifySignature(0, differentXpub);
|
|
227
|
-
assert.strictEqual(result, false, "Should return false for xpub not in PSBT derivation paths");
|
|
228
|
-
});
|
|
229
|
-
it("should verify signature with raw public key (Uint8Array)", function () {
|
|
230
|
-
const psbt = getBitGoPsbt(fullsignedFixture, networkName);
|
|
231
|
-
// Verify that xpub-based verification works
|
|
232
|
-
const userKey = rootWalletKeys.userKey();
|
|
233
|
-
const hasXpubSig = psbt.verifySignature(0, userKey);
|
|
234
|
-
// This test specifically checks that raw public key verification works
|
|
235
|
-
// We test the underlying WASM API by ensuring both xpub and raw pubkey
|
|
236
|
-
// calls reach the correct methods
|
|
237
|
-
// Use a random public key that's not in the PSBT to test the API works
|
|
238
|
-
const randomSeed = Buffer.alloc(32, 0xcc);
|
|
239
|
-
const randomKey = BIP32.fromSeed(randomSeed);
|
|
240
|
-
const randomPubkey = randomKey.publicKey;
|
|
241
|
-
// This should return false (no signature for this key)
|
|
242
|
-
const result = psbt.verifySignature(0, randomPubkey);
|
|
243
|
-
assert.strictEqual(result, false, "Should return false for public key not in PSBT");
|
|
244
|
-
// Verify the xpub check still works (regression test)
|
|
245
|
-
assert.strictEqual(hasXpubSig, true, "Should still verify with xpub");
|
|
246
|
-
});
|
|
247
|
-
it("should return false for raw public key with no signature", function () {
|
|
248
|
-
const psbt = getBitGoPsbt(fullsignedFixture, networkName);
|
|
249
|
-
// Create a random public key that's not in the PSBT
|
|
250
|
-
const randomSeed = Buffer.alloc(32, 0xbb);
|
|
251
|
-
const randomKey = BIP32.fromSeed(randomSeed);
|
|
252
|
-
const randomPubkey = randomKey.publicKey;
|
|
253
|
-
const result = psbt.verifySignature(0, randomPubkey);
|
|
254
|
-
assert.strictEqual(result, false, "Should return false for public key not in PSBT signatures");
|
|
255
|
-
});
|
|
256
|
-
it("should throw error for invalid key length", function () {
|
|
257
|
-
const psbt = getBitGoPsbt(fullsignedFixture, networkName);
|
|
258
|
-
const invalidKey = Buffer.alloc(31); // Invalid length (should be 32 for private key or 33 for public key)
|
|
259
|
-
assert.throws(() => {
|
|
260
|
-
psbt.verifySignature(0, invalidKey);
|
|
261
|
-
}, (error) => {
|
|
262
|
-
return error.message.includes("Invalid key length");
|
|
263
|
-
}, "Should throw error for invalid key length");
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import * as utxolib from "@bitgo/utxo-lib";
|
|
2
|
-
import type { Triple } from "../../js/triple.js";
|
|
3
|
-
/**
|
|
4
|
-
* Convert utxolib BIP32 keys to WASM wallet keys format (Triple<string>)
|
|
5
|
-
*/
|
|
6
|
-
export declare function toWasmWalletKeys(keys: [utxolib.BIP32Interface, utxolib.BIP32Interface, utxolib.BIP32Interface]): Triple<string>;
|
|
7
|
-
/**
|
|
8
|
-
* Get standard replay protection configuration
|
|
9
|
-
*/
|
|
10
|
-
export declare function getStandardReplayProtection(): {
|
|
11
|
-
outputScripts: Uint8Array[];
|
|
12
|
-
};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Convert utxolib BIP32 keys to WASM wallet keys format (Triple<string>)
|
|
3
|
-
*/
|
|
4
|
-
export function toWasmWalletKeys(keys) {
|
|
5
|
-
return [
|
|
6
|
-
keys[0].neutered().toBase58(),
|
|
7
|
-
keys[1].neutered().toBase58(),
|
|
8
|
-
keys[2].neutered().toBase58(),
|
|
9
|
-
];
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Get standard replay protection configuration
|
|
13
|
-
*/
|
|
14
|
-
export function getStandardReplayProtection() {
|
|
15
|
-
const replayProtectionScript = Buffer.from("a91420b37094d82a513451ff0ccd9db23aba05bc5ef387", "hex");
|
|
16
|
-
return { outputScripts: [replayProtectionScript] };
|
|
17
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import * as assert from "assert";
|
|
2
|
-
import * as utxolib from "@bitgo/utxo-lib";
|
|
3
|
-
import { Descriptor } from "../js/index.js";
|
|
4
|
-
import { getDescriptorForScriptType } from "./descriptorUtil.js";
|
|
5
|
-
const rootWalletKeys = new utxolib.bitgo.RootWalletKeys(utxolib.testutil.getKeyTriple("wasm"));
|
|
6
|
-
const scriptTypes = ["p2sh", "p2shP2wsh", "p2wsh"];
|
|
7
|
-
const scope = ["external", "internal"];
|
|
8
|
-
const index = [0, 1, 2];
|
|
9
|
-
/** Get the expected max weight to satisfy the descriptor */
|
|
10
|
-
function getExpectedMaxWeightToSatisfy(scriptType) {
|
|
11
|
-
switch (scriptType) {
|
|
12
|
-
case "p2sh":
|
|
13
|
-
return 256;
|
|
14
|
-
case "p2shP2wsh":
|
|
15
|
-
return 99;
|
|
16
|
-
case "p2wsh":
|
|
17
|
-
return 64;
|
|
18
|
-
default:
|
|
19
|
-
throw new Error("unexpected script type");
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
/** Compute the total size of the input, including overhead */
|
|
23
|
-
function getTotalInputSize(vSize) {
|
|
24
|
-
const sizeOpPushData1 = 1;
|
|
25
|
-
const sizeOpPushData2 = 2;
|
|
26
|
-
return (32 /* txid */ +
|
|
27
|
-
4 /* vout */ +
|
|
28
|
-
4 /* nSequence */ +
|
|
29
|
-
(vSize < 255 ? sizeOpPushData1 : sizeOpPushData2) +
|
|
30
|
-
vSize);
|
|
31
|
-
}
|
|
32
|
-
/** Get the full expected vSize of the input including overhead */
|
|
33
|
-
function getExpectedVSize(scriptType) {
|
|
34
|
-
// https://github.com/BitGo/BitGoJS/blob/master/modules/unspents/docs/input-costs.md
|
|
35
|
-
switch (scriptType) {
|
|
36
|
-
case "p2sh":
|
|
37
|
-
return 298;
|
|
38
|
-
case "p2shP2wsh":
|
|
39
|
-
return 140;
|
|
40
|
-
case "p2wsh":
|
|
41
|
-
return 105;
|
|
42
|
-
default:
|
|
43
|
-
throw new Error("unexpected script type");
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
function runTest(scriptType, index, scope) {
|
|
47
|
-
describe(`scriptType=${scriptType}, index=${index}, scope=${scope}`, function () {
|
|
48
|
-
const chainCode = scope === "external"
|
|
49
|
-
? utxolib.bitgo.getExternalChainCode(scriptType)
|
|
50
|
-
: utxolib.bitgo.getInternalChainCode(scriptType);
|
|
51
|
-
const derivedKeys = rootWalletKeys.deriveForChainAndIndex(chainCode, index);
|
|
52
|
-
const scriptUtxolib = utxolib.bitgo.outputScripts.createOutputScript2of3(derivedKeys.publicKeys, scriptType).scriptPubKey;
|
|
53
|
-
let descriptor;
|
|
54
|
-
before(function () {
|
|
55
|
-
descriptor = Descriptor.fromString(getDescriptorForScriptType(rootWalletKeys, scriptType, scope), "derivable");
|
|
56
|
-
});
|
|
57
|
-
it("descriptor should have expected format", function () {
|
|
58
|
-
const [x1, x2, x3] = rootWalletKeys.triple.map((xpub) => xpub.neutered().toBase58());
|
|
59
|
-
if (scriptType === "p2sh" && scope === "external") {
|
|
60
|
-
// spot check
|
|
61
|
-
assert.ok(descriptor
|
|
62
|
-
.toString()
|
|
63
|
-
.startsWith(`sh(multi(2,${x1}/0/0/0/*,${x2}/0/0/0/*,${x3}/0/0/0/*))`));
|
|
64
|
-
}
|
|
65
|
-
if (scriptType === "p2shP2wsh" && scope === "internal") {
|
|
66
|
-
// spot check
|
|
67
|
-
assert.ok(descriptor
|
|
68
|
-
.toString()
|
|
69
|
-
.startsWith(`sh(wsh(multi(2,${x1}/0/0/11/*,${x2}/0/0/11/*,${x3}/0/0/11/*)))`));
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
it("address should match descriptor", function () {
|
|
73
|
-
const scriptFromDescriptor = Buffer.from(descriptor.atDerivationIndex(index).scriptPubkey());
|
|
74
|
-
assert.deepStrictEqual(scriptUtxolib.toString("hex"), scriptFromDescriptor.toString("hex"));
|
|
75
|
-
});
|
|
76
|
-
it("should have expected weights", function () {
|
|
77
|
-
assert.ok(Number.isInteger(descriptor.maxWeightToSatisfy()));
|
|
78
|
-
const vSize = Math.ceil(descriptor.maxWeightToSatisfy() / 4);
|
|
79
|
-
console.log(scriptType, "scriptLength", descriptor.atDerivationIndex(0).encode().length, "vSize", vSize);
|
|
80
|
-
assert.equal(vSize, getExpectedMaxWeightToSatisfy(scriptType));
|
|
81
|
-
assert.equal(getTotalInputSize(vSize), getExpectedVSize(scriptType));
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
describe("fixedScript to descriptor", function () {
|
|
86
|
-
scriptTypes.forEach((scriptType) => {
|
|
87
|
-
index.forEach((index) => {
|
|
88
|
-
scope.forEach((scope) => {
|
|
89
|
-
runTest(scriptType, index, scope);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function getFixture(path: string, defaultValue: unknown): Promise<unknown>;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs/promises";
|
|
2
|
-
export async function getFixture(path, defaultValue) {
|
|
3
|
-
try {
|
|
4
|
-
return JSON.parse(await fs.readFile(path, "utf8"));
|
|
5
|
-
}
|
|
6
|
-
catch (e) {
|
|
7
|
-
if (typeof e === "object" &&
|
|
8
|
-
e !== null &&
|
|
9
|
-
"code" in e &&
|
|
10
|
-
e.code === "ENOENT") {
|
|
11
|
-
await fs.writeFile(path, JSON.stringify(defaultValue, null, 2));
|
|
12
|
-
throw new Error(`Fixture not found at ${path}, created a new one`);
|
|
13
|
-
}
|
|
14
|
-
throw e;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|