Skip to content

Commit 502e561

Browse files
feat(utxo-lib): extract address from paygo attestation proof
Made a function that extracts the address buffer from an attestation format. We created a testutil function that generates this attestation format so that we can test within utxo-lib. This messages will later be signed by a key and we need to verify that signature as well as make sure that the address encoded in the attestation proof matches the address of the output that we attach this onto. TICKET: BTC-2150
1 parent 43be408 commit 502e561

File tree

3 files changed

+159
-7
lines changed

3 files changed

+159
-7
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as assert from 'assert';
2+
import { varuint } from 'bitcoinjs-lib/src/bufferutils';
3+
4+
// The signed address will always have the following structure:
5+
// 0x18Bitcoin Signed Message:\n<varint_length><ENTROPY><ADDRESS><UUID>
6+
7+
const PrefixLength = Buffer.from([0x18]).length + Buffer.from('Bitcoin Signed Message:\n').length;
8+
// UUID has the structure 00000000-0000-0000-0000-000000000000, and after
9+
// we Buffer.from and get it's length its 36.
10+
const UuidBufferLength = 36;
11+
// The entropy will always be 64 bytes
12+
const EntropyLen = 64;
13+
14+
/**
15+
* This function takes in the attestation proof of a PayGo address of the from
16+
* 0x18Bitcoin Signed Message:\n<varint_length><ENTROPY><ADDRESS><UUID> and returns
17+
* the address given its length. It is assumed that the ENTROPY is 64 bytes in the Buffer
18+
* so if not given an address proof length we can still extract the address from the proof.
19+
*
20+
* @param message
21+
* @param adressProofLength
22+
*/
23+
export default function extractAddressBufferFromPayGoAttestationProof(message: Buffer): Buffer {
24+
if (message.length <= PrefixLength + EntropyLen + UuidBufferLength) {
25+
throw new Error('PayGo attestation proof is too short to contain a valid address');
26+
}
27+
28+
// This generates the first part before the varint length so that we can
29+
// determine how many bytes this is and iterate through the Buffer.
30+
let offset = PrefixLength;
31+
32+
// we decode the varint of the message which is uint32
33+
// https://en.bitcoin.it/wiki/Protocol_documentation
34+
const varInt = varuint.decode(message, offset);
35+
assert(varInt);
36+
offset += 1;
37+
38+
const addressLength = varInt - EntropyLen - UuidBufferLength;
39+
offset += EntropyLen;
40+
41+
// we return what the Buffer subarray from the offset (beginning of address)
42+
// to the end of the address index in the buffer.
43+
return message.subarray(offset, offset + addressLength);
44+
}

modules/utxo-lib/src/testutil/psbt.ts

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import * as assert from 'assert';
2+
import { varuint } from 'bitcoinjs-lib/src/bufferutils';
3+
import * as crypto from 'crypto';
24

35
import {
46
createOutputScriptP2shP2pk,
@@ -77,7 +79,10 @@ export function toUnspent(
7779
rootWalletKeys: RootWalletKeys
7880
): Unspent<bigint> {
7981
if (input.scriptType === 'p2shP2pk') {
80-
return mockReplayProtectionUnspent(network, input.value, { key: rootWalletKeys['user'], vout: index });
82+
return mockReplayProtectionUnspent(network, input.value, {
83+
key: rootWalletKeys['user'],
84+
vout: index,
85+
});
8186
} else {
8287
const chain = getInternalChainCode(input.scriptType === 'taprootKeyPathSpend' ? 'p2trMusig2' : input.scriptType);
8388
return mockWalletUnspent(network, input.value, {
@@ -95,7 +100,10 @@ export function toUnspent(
95100
* user and backup as signer and cosigner respectively for p2trMusig2.
96101
* user and bitgo as signer and cosigner respectively for other input script types.
97102
*/
98-
export function getSigners(inputType: InputScriptType): { signerName: KeyName; cosignerName?: KeyName } {
103+
export function getSigners(inputType: InputScriptType): {
104+
signerName: KeyName;
105+
cosignerName?: KeyName;
106+
} {
99107
return {
100108
signerName: 'user',
101109
cosignerName: inputType === 'p2shP2pk' ? undefined : inputType === 'p2trMusig2' ? 'backup' : 'bitgo',
@@ -138,7 +146,10 @@ export function signPsbtInput(
138146
if (sign === 'fullsigned' && cosignerName && input.scriptType !== 'p2shP2pk') {
139147
signPsbt(
140148
psbt,
141-
() => psbt.signInputHD(inputIndex, rootWalletKeys[cosignerName], { deterministic }),
149+
() =>
150+
psbt.signInputHD(inputIndex, rootWalletKeys[cosignerName], {
151+
deterministic,
152+
}),
142153
skipNonWitnessUtxo
143154
);
144155
}
@@ -161,7 +172,11 @@ export function signAllPsbtInputs(
161172
): void {
162173
const { signers, deterministic, skipNonWitnessUtxo } = params ?? {};
163174
inputs.forEach((input, inputIndex) => {
164-
signPsbtInput(psbt, input, inputIndex, rootWalletKeys, sign, { signers, deterministic, skipNonWitnessUtxo });
175+
signPsbtInput(psbt, input, inputIndex, rootWalletKeys, sign, {
176+
signers,
177+
deterministic,
178+
skipNonWitnessUtxo,
179+
});
165180
});
166181
}
167182

@@ -195,7 +210,9 @@ export function constructPsbt(
195210
} else {
196211
const { redeemScript } = createOutputScriptP2shP2pk(rootWalletKeys[signerName].publicKey);
197212
assert(redeemScript);
198-
addReplayProtectionUnspentToPsbt(psbt, u, redeemScript, { skipNonWitnessUtxo });
213+
addReplayProtectionUnspentToPsbt(psbt, u, redeemScript, {
214+
skipNonWitnessUtxo,
215+
});
199216
}
200217
});
201218

@@ -224,10 +241,17 @@ export function constructPsbt(
224241
psbt.setAllInputsMusig2NonceHD(rootWalletKeys['user']);
225242
psbt.setAllInputsMusig2NonceHD(rootWalletKeys['bitgo'], { deterministic });
226243

227-
signAllPsbtInputs(psbt, inputs, rootWalletKeys, 'halfsigned', { signers, skipNonWitnessUtxo });
244+
signAllPsbtInputs(psbt, inputs, rootWalletKeys, 'halfsigned', {
245+
signers,
246+
skipNonWitnessUtxo,
247+
});
228248

229249
if (sign === 'fullsigned') {
230-
signAllPsbtInputs(psbt, inputs, rootWalletKeys, sign, { signers, deterministic, skipNonWitnessUtxo });
250+
signAllPsbtInputs(psbt, inputs, rootWalletKeys, sign, {
251+
signers,
252+
deterministic,
253+
skipNonWitnessUtxo,
254+
});
231255
}
232256

233257
return psbt;
@@ -261,3 +285,44 @@ export function verifyFullySignedSignatures(
261285
}
262286
});
263287
}
288+
289+
/** We have a mirrored function similar to our hsm that generates our Bitcoin signed
290+
* message so that we can use for testing. This creates a random entropy as well using
291+
* the nilUUID structure to construct our uuid buffer and given our address we can
292+
* directly encode it into our message.
293+
*
294+
* @param attestationPrvKey
295+
* @param uuid
296+
* @param address
297+
* @returns
298+
*/
299+
export function generatePayGoAttestationProof(uuid: string, address: Buffer): Buffer {
300+
// <0x18Bitcoin Signed Message:\n
301+
const prefixByte = Buffer.from([0x18]);
302+
const prefixMessage = Buffer.from('Bitcoin Signed Message:\n');
303+
const prefixBuffer = Buffer.concat([prefixByte, prefixMessage]);
304+
305+
// <ENTROPY>
306+
const entropyLength = 64;
307+
const entropy = crypto.randomBytes(entropyLength);
308+
309+
// <UUID>
310+
const uuidBuffer = Buffer.from(uuid);
311+
const uuidBufferLength = uuidBuffer.length;
312+
313+
// <ADDRESS>
314+
const addressBufferLength = address.length;
315+
316+
// <VARINT_LENGTH>
317+
const msgLength = entropyLength + addressBufferLength + uuidBufferLength;
318+
const msgLengthBuffer = varuint.encode(msgLength);
319+
320+
// <0x18Bitcoin Signed Message:\n<LENGTH><ENTROPY><ADDRESS><UUID>
321+
const proofMessage = Buffer.concat([prefixBuffer, msgLengthBuffer, entropy, address, uuidBuffer]);
322+
323+
// we sign this with the priv key
324+
// don't know what sign function to call. Since this is just a mirrored function don't know if we need
325+
// to include this part.
326+
// const signedMsg = sign(attestationPrvKey, proofMessage);
327+
return proofMessage;
328+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import * as assert from 'assert';
2+
3+
import { generatePayGoAttestationProof } from '../../src/testutil/psbt';
4+
import extractAddressBufferFromPayGoAttestationProof from '../../src/bitgo/ExtractAddressPayGoAttestation';
5+
6+
const addressFromPubKeyBase58 = 'bitgoAddressToExtract';
7+
const bufferAddressPubKeyB58 = Buffer.from(addressFromPubKeyBase58);
8+
9+
describe('extractAddressBufferFromPayGoAttestationProof', () => {
10+
it('should extractAddressBufferFromPayGoAttestationProof properly', () => {
11+
const paygoAttestationProof = generatePayGoAttestationProof(
12+
'00000000-0000-0000-0000-000000000000',
13+
bufferAddressPubKeyB58
14+
);
15+
const addressFromProof = extractAddressBufferFromPayGoAttestationProof(paygoAttestationProof);
16+
assert.deepStrictEqual(Buffer.compare(addressFromProof, bufferAddressPubKeyB58), 0);
17+
});
18+
19+
it('should extract the paygo address paygo attestation proof given a non nilUUID', () => {
20+
const paygoAttestationProof = generatePayGoAttestationProof(
21+
'12345678-1234-4567-6890-231928472123',
22+
bufferAddressPubKeyB58
23+
);
24+
const addressFromProof = extractAddressBufferFromPayGoAttestationProof(paygoAttestationProof);
25+
assert(Buffer.compare(addressFromProof, bufferAddressPubKeyB58) === 0);
26+
});
27+
28+
it('should not extract the correct address given a uuid of wrong format', () => {
29+
const paygoAttestationProof = generatePayGoAttestationProof(
30+
'000000000000000-000000-0000000-000000-0000000000000000',
31+
bufferAddressPubKeyB58
32+
);
33+
const addressFromProof = extractAddressBufferFromPayGoAttestationProof(paygoAttestationProof);
34+
assert(Buffer.compare(addressFromProof, bufferAddressPubKeyB58) !== 0);
35+
});
36+
37+
it('should throw an error if the paygo attestation proof is too short', () => {
38+
assert.throws(
39+
() => extractAddressBufferFromPayGoAttestationProof(Buffer.from('shortproof-shrug')),
40+
'PayGo attestation proof is too short to contain a valid address.'
41+
);
42+
});
43+
});

0 commit comments

Comments
 (0)