Skip to content

Commit be76a2f

Browse files
committed
feat: polyfill for bitcoinjs-lib PR-2137
See: bitcoinjs/bitcoinjs-lib#2137
1 parent a2d0ad5 commit be76a2f

File tree

1 file changed

+200
-0
lines changed

1 file changed

+200
-0
lines changed

src/applyPR2137.ts

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
//While this PR is not merged: https://github.com/bitcoinjs/bitcoinjs-lib/pull/2137
2+
//The Async functions have not been "fixed"
3+
import type { Psbt, Signer } from 'bitcoinjs-lib';
4+
import { checkForInput } from 'bip174/src/lib/utils';
5+
import type { SignerAsync } from 'ecpair';
6+
import type { PsbtInput } from 'bip174/src/lib/interfaces';
7+
import { tapTweakHash } from 'bitcoinjs-lib/src/payments/bip341';
8+
import { isTaprootInput } from 'bitcoinjs-lib/src/psbt/bip371';
9+
import { taggedHash } from 'bitcoinjs-lib/src/crypto';
10+
11+
interface HDSignerBase {
12+
/**
13+
* DER format compressed publicKey buffer
14+
*/
15+
publicKey: Buffer;
16+
/**
17+
* The first 4 bytes of the sha256-ripemd160 of the publicKey
18+
*/
19+
fingerprint: Buffer;
20+
}
21+
interface HDSigner extends HDSignerBase {
22+
/**
23+
* The path string must match /^m(\/\d+'?)+$/
24+
* ex. m/44'/0'/0'/1/23 levels with ' must be hard derivations
25+
*/
26+
derivePath(path: string): HDSigner;
27+
/**
28+
* Input hash (the "message digest") for the signature algorithm
29+
* Return a 64 byte signature (32 byte r and 32 byte s in that order)
30+
*/
31+
sign(hash: Buffer): Buffer;
32+
/**
33+
* Adjusts a keypair for Taproot payments by applying a tweak to derive the internal key.
34+
*
35+
* In Taproot, a keypair may need to be tweaked to produce an internal key that conforms to the Taproot script.
36+
* This tweak process involves modifying the original keypair based on a specific tweak value to ensure compatibility
37+
* with the Taproot address format and functionality.
38+
*/
39+
tweak(t: Buffer): Signer;
40+
}
41+
interface HDSignerAsync extends HDSignerBase {
42+
derivePath(path: string): HDSignerAsync;
43+
sign(hash: Buffer): Promise<Buffer>;
44+
tweak(t: Buffer): Signer;
45+
}
46+
47+
const toXOnly = (pubKey: Buffer) =>
48+
pubKey.length === 32 ? pubKey : pubKey.slice(1, 33);
49+
50+
function range(n: number): number[] {
51+
return [...Array(n).keys()];
52+
}
53+
54+
function tapBranchHash(a: Buffer, b: Buffer): Buffer {
55+
return taggedHash('TapBranch', Buffer.concat([a, b]));
56+
}
57+
58+
function calculateScriptTreeMerkleRoot(
59+
leafHashes: Buffer[]
60+
): Buffer | undefined {
61+
if (!leafHashes || leafHashes.length === 0) {
62+
return undefined;
63+
}
64+
65+
// sort the leaf nodes
66+
leafHashes.sort(Buffer.compare);
67+
68+
// create the initial hash node
69+
let currentLevel = leafHashes;
70+
71+
// build Merkle Tree
72+
while (currentLevel.length > 1) {
73+
const nextLevel = [];
74+
for (let i = 0; i < currentLevel.length; i += 2) {
75+
const left = currentLevel[i];
76+
const right = i + 1 < currentLevel.length ? currentLevel[i + 1] : left;
77+
nextLevel.push(
78+
i + 1 < currentLevel.length ? tapBranchHash(left!, right!) : left
79+
);
80+
}
81+
currentLevel = nextLevel as Buffer[];
82+
}
83+
84+
return currentLevel[0];
85+
}
86+
87+
function getTweakSignersFromHD(
88+
inputIndex: number,
89+
inputs: PsbtInput[],
90+
hdKeyPair: HDSigner | HDSignerAsync
91+
): Array<Signer | SignerAsync> {
92+
const input = checkForInput(inputs, inputIndex);
93+
if (!input.tapBip32Derivation || input.tapBip32Derivation.length === 0) {
94+
throw new Error('Need tapBip32Derivation to sign with HD');
95+
}
96+
const myDerivations = input.tapBip32Derivation
97+
.map(bipDv => {
98+
if (bipDv.masterFingerprint.equals(hdKeyPair.fingerprint)) {
99+
return bipDv;
100+
} else {
101+
return;
102+
}
103+
})
104+
.filter(v => !!v);
105+
if (myDerivations.length === 0) {
106+
throw new Error(
107+
'Need one tapBip32Derivation masterFingerprint to match the HDSigner fingerprint'
108+
);
109+
}
110+
111+
const signers: Array<Signer | SignerAsync> = myDerivations.map(bipDv => {
112+
const node = hdKeyPair.derivePath(bipDv!.path);
113+
if (!bipDv!.pubkey.equals(toXOnly(node.publicKey))) {
114+
throw new Error('pubkey did not match tapBip32Derivation');
115+
}
116+
const h = calculateScriptTreeMerkleRoot(bipDv!.leafHashes);
117+
const tweakValue = tapTweakHash(toXOnly(node.publicKey), h);
118+
119+
return node.tweak(tweakValue);
120+
});
121+
return signers;
122+
}
123+
function getSignersFromHD(
124+
inputIndex: number,
125+
inputs: PsbtInput[],
126+
hdKeyPair: HDSigner | HDSignerAsync
127+
): Array<Signer | SignerAsync> {
128+
const input = checkForInput(inputs, inputIndex);
129+
if (isTaprootInput(input)) {
130+
return getTweakSignersFromHD(inputIndex, inputs, hdKeyPair);
131+
}
132+
133+
if (!input.bip32Derivation || input.bip32Derivation.length === 0) {
134+
throw new Error('Need bip32Derivation to sign with HD');
135+
}
136+
const myDerivations = input.bip32Derivation
137+
.map(bipDv => {
138+
if (bipDv.masterFingerprint.equals(hdKeyPair.fingerprint)) {
139+
return bipDv;
140+
} else {
141+
return;
142+
}
143+
})
144+
.filter(v => !!v);
145+
if (myDerivations.length === 0) {
146+
throw new Error(
147+
'Need one bip32Derivation masterFingerprint to match the HDSigner fingerprint'
148+
);
149+
}
150+
const signers: Array<Signer | SignerAsync> = myDerivations.map(bipDv => {
151+
const node = hdKeyPair.derivePath(bipDv!.path);
152+
if (!bipDv!.pubkey.equals(node.publicKey)) {
153+
throw new Error('pubkey did not match bip32Derivation');
154+
}
155+
return node;
156+
});
157+
return signers;
158+
}
159+
160+
export const applyPR2137 = (psbt: Psbt) => {
161+
psbt.signInputHD = function signInputHD(
162+
inputIndex: number,
163+
hdKeyPair: HDSigner,
164+
sighashTypes?: number[]
165+
) {
166+
if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) {
167+
throw new Error('Need HDSigner to sign input');
168+
}
169+
const signers = getSignersFromHD(
170+
inputIndex,
171+
this.data.inputs,
172+
hdKeyPair
173+
) as Signer[];
174+
signers.forEach(signer => this.signInput(inputIndex, signer, sighashTypes));
175+
return this;
176+
};
177+
178+
psbt.signAllInputsHD = function signAllInputsHD(
179+
hdKeyPair: HDSigner,
180+
sighashTypes?: number[]
181+
) {
182+
if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) {
183+
throw new Error('Need HDSigner to sign input');
184+
}
185+
186+
const results: boolean[] = [];
187+
for (const i of range(psbt.data.inputs.length)) {
188+
try {
189+
psbt.signInputHD(i, hdKeyPair, sighashTypes);
190+
results.push(true);
191+
} catch (err) {
192+
results.push(false);
193+
}
194+
}
195+
if (results.every(v => v === false)) {
196+
throw new Error('No inputs were signed');
197+
}
198+
return psbt;
199+
};
200+
};

0 commit comments

Comments
 (0)