Skip to content

Commit 636f8a9

Browse files
chore: added test for paygo addr and moved generate paygo proof to other pr
TICKET: BTC-2047 chore: determined msg to sign and verify Issue: BTC-2047 TICKET: BTC-2047 chore(utxo-lib): modified verify function and tests Issue: BTC-2047 TICKET: BTC-2047 chore(utxo-core): moved util functions to uxto-core TICKET: BTC-2047 chore: got tests to work BTC-2047 TICKET: BTC-2047 feat(utxo-core): cleaned up comments TICKET: BTC-2047 chore(utxo-core): added bitcoinjs-message to deps and fixed import TICKET: BTC-2047 chore(utxo-core): used new helper functions for prop kv func and removed old TICKET: BTC-2047
1 parent cd96125 commit 636f8a9

File tree

10 files changed

+273
-223
lines changed

10 files changed

+273
-223
lines changed

modules/utxo-core/src/paygo/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './ExtractAddressPayGoAttestation';
2+
export * from './psbt';
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
import * as bitcoinMessage from 'bitcoinjs-message';
3+
import { checkForOutput } from 'bip174/src/lib/utils';
4+
5+
import { extractAddressBufferFromPayGoAttestationProof } from '../ExtractAddressPayGoAttestation';
6+
7+
/** The function consumes the signature as a parameter and adds the PayGo address to the
8+
* PSBT output at the output index where the signature is of the format:
9+
* 0x18Bitcoin Signed Message:\n<varint_length><ENTROPY><ADDRESS><UUID> signed by
10+
* the HSM beforehand.
11+
*
12+
* @param psbt - PSBT that we need to encode our paygo address into
13+
* @param outputIndex - the index of the address in our output
14+
* @param sig - the signature that we want to encode
15+
*/
16+
export function addPaygoAddressProof(
17+
psbt: utxolib.bitgo.UtxoPsbt,
18+
outputIndex: number,
19+
sig: Buffer,
20+
pub: Buffer
21+
): void {
22+
utxolib.bitgo.addProprietaryKeyValuesFromUnknownKeyValues(psbt, 'output', outputIndex, {
23+
key: {
24+
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
25+
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
26+
keydata: pub,
27+
},
28+
value: sig,
29+
});
30+
}
31+
32+
/** Verify the paygo address signature is valid using BitGoJs statics
33+
*
34+
* @param psbt - PSBT we want to verify that the paygo address is in
35+
* @param outputIndex - we have the output index that address is in
36+
* @param pub - The public key that we want to verify the proof with
37+
* @param message - The message we want to verify corresponding to sig
38+
* @returns
39+
*/
40+
export function verifyPaygoAddressProof(
41+
psbt: utxolib.bitgo.UtxoPsbt,
42+
outputIndex: number,
43+
message: Buffer,
44+
attestationPubKey: Buffer
45+
): void {
46+
const psbtOutputs = checkForOutput(psbt.data.outputs, outputIndex);
47+
const stored = utxolib.bitgo.getProprietaryKeyValuesFromUnknownKeyValues(psbtOutputs, {
48+
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
49+
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
50+
});
51+
if (!stored) {
52+
throw new Error(`No address proof.`);
53+
}
54+
55+
// assert stored length is 0 or 1
56+
if (stored.length === 0) {
57+
throw new Error(`There is no paygo address proof encoded in the PSBT at output ${outputIndex}.`);
58+
} else if (stored.length > 1) {
59+
throw new Error('There are multiple paygo address proofs encoded in the PSBT. Something went wrong.');
60+
}
61+
62+
const signature = stored[0].value;
63+
const pub = stored[0].key.keydata;
64+
65+
// Check that the keydata pubkey is the same as the one we are verifying against
66+
if (Buffer.compare(pub, attestationPubKey) !== 0) {
67+
throw new Error('The public key in the PSBT does not match the provided public key.');
68+
}
69+
70+
// It doesn't matter that this is bitcoin or not, we just need to convert the public key buffer into an address format
71+
// for the verification
72+
const messageToVerify = utxolib.address.toBase58Check(
73+
utxolib.crypto.hash160(pub),
74+
utxolib.networks.bitcoin.pubKeyHash,
75+
utxolib.networks.bitcoin
76+
);
77+
78+
if (!bitcoinMessage.verify(message, messageToVerify, signature, utxolib.networks.bitcoin.messagePrefix)) {
79+
throw new Error('Cannot verify the paygo address signature with the provided pubkey.');
80+
}
81+
// We should be verifying the address that was encoded into our message.
82+
const addressFromProof = extractAddressBufferFromPayGoAttestationProof(message).toString();
83+
84+
// Check that the address from the proof matches what is in the PSBT
85+
const txOutputs = psbt.txOutputs;
86+
if (outputIndex >= txOutputs.length) {
87+
throw new Error(`Output index ${outputIndex} is out of bounds for PSBT outputs.`);
88+
}
89+
const output = txOutputs[outputIndex];
90+
const addressFromOutput = utxolib.address.fromOutputScript(output.script, psbt.network);
91+
92+
if (addressFromProof !== addressFromOutput) {
93+
throw new Error(
94+
`The address from the output (${addressFromOutput}) does not match the address that is in the proof (${addressFromProof}).`
95+
);
96+
}
97+
}
98+
99+
/** Get the output index of the paygo output if there is one. It does this by
100+
* checking if the metadata is on one of the outputs of the PSBT. If there is
101+
* no paygo output, return undefined
102+
*
103+
* @param psbt
104+
* @returns number - the index of the output address
105+
*/
106+
export function getPaygoAddressProofOutputIndex(psbt: utxolib.bitgo.UtxoPsbt): number | undefined {
107+
const res = psbt.data.outputs.flatMap((output, outputIndex) => {
108+
const proprietaryKeyVals = utxolib.bitgo.getPsbtOutputProprietaryKeyVals(output, {
109+
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
110+
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
111+
});
112+
113+
if (proprietaryKeyVals.length > 1) {
114+
throw new Error(`There are multiple PayGo addresses in the PSBT output ${outputIndex}.`);
115+
}
116+
117+
return proprietaryKeyVals.length === 0 ? [] : [outputIndex];
118+
});
119+
120+
return res.length === 0 ? undefined : res[0];
121+
}
122+
123+
export function psbtOutputIncludesPaygoAddressProof(psbt: utxolib.bitgo.UtxoPsbt): boolean {
124+
return getPaygoAddressProofOutputIndex(psbt) !== undefined;
125+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './PayGoUtils';
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import assert from 'assert';
2+
3+
import * as utxolib from '@bitgo/utxo-lib';
4+
import * as bitcoinMessage from 'bitcoinjs-message';
5+
import { decodeProprietaryKey } from 'bip174/src/lib/proprietaryKeyVal';
6+
import { KeyValue } from 'bip174/src/lib/interfaces';
7+
import { checkForOutput } from 'bip174/src/lib/utils';
8+
9+
import {
10+
addPaygoAddressProof,
11+
getPaygoAddressProofOutputIndex,
12+
psbtOutputIncludesPaygoAddressProof,
13+
verifyPaygoAddressProof,
14+
} from '../../../src/paygo/psbt/PayGoUtils';
15+
import { generatePayGoAttestationProof } from '../../../src/testutil/generatePayGoAttestationProof.utils';
16+
17+
// To construct our PSBTs
18+
const network = utxolib.networks.bitcoin;
19+
const keys = [1, 2, 3].map((v) => utxolib.bip32.fromSeed(Buffer.alloc(16, `test/2/${v}`), network));
20+
const rootWalletKeys = new utxolib.bitgo.RootWalletKeys([keys[0], keys[1], keys[2]]);
21+
22+
// PSBT INPUTS AND OUTPUTS
23+
const psbtInputs = utxolib.testutil.inputScriptTypes.map((scriptType) => ({
24+
scriptType,
25+
value: BigInt(1000),
26+
}));
27+
const psbtOutputs = utxolib.testutil.outputScriptTypes.map((scriptType) => ({
28+
scriptType,
29+
value: BigInt(900),
30+
}));
31+
32+
// wallet pub and priv key for tbtc
33+
const dummyPub1 = rootWalletKeys.deriveForChainAndIndex(50, 200);
34+
const attestationPubKey = dummyPub1.user.publicKey;
35+
const attestationPrvKey = dummyPub1.user.privateKey!;
36+
37+
// UUID structure
38+
const nilUUID = '00000000-0000-0000-0000-000000000000';
39+
40+
// our xpub converted to base58 address
41+
const addressToVerify = utxolib.address.toBase58Check(
42+
utxolib.crypto.hash160(Buffer.from(dummyPub1.backup.publicKey)),
43+
utxolib.networks.bitcoin.pubKeyHash,
44+
utxolib.networks.bitcoin
45+
);
46+
47+
// this should be retuning a Buffer
48+
const addressProofBuffer = generatePayGoAttestationProof(nilUUID, Buffer.from(addressToVerify));
49+
// signature with the given msg addressProofBuffer
50+
const sig = bitcoinMessage.sign(addressProofBuffer, attestationPrvKey!, true, network.messagePrefix);
51+
52+
function getTestPsbt() {
53+
return utxolib.testutil.constructPsbt(psbtInputs, psbtOutputs, network, rootWalletKeys, 'unsigned');
54+
}
55+
56+
describe('addPaygoAddressProof and verifyPaygoAddressProof', () => {
57+
function getPaygoProprietaryKey(proprietaryKeyVals: KeyValue[]) {
58+
return proprietaryKeyVals
59+
.map(({ key, value }) => {
60+
return { key: decodeProprietaryKey(key), value };
61+
})
62+
.filter((keyValue) => {
63+
return (
64+
keyValue.key.identifier === utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER &&
65+
keyValue.key.subtype === utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF
66+
);
67+
});
68+
}
69+
70+
it("should fail a proof verification if the proof isn't valid", () => {
71+
const outputIndex = 0;
72+
const psbt = getTestPsbt();
73+
addPaygoAddressProof(psbt, outputIndex, sig, Buffer.from(attestationPubKey));
74+
const output = checkForOutput(psbt.data.outputs, outputIndex);
75+
const proofInPsbt = getPaygoProprietaryKey(output.unknownKeyVals!);
76+
assert(proofInPsbt.length === 1);
77+
assert.throws(
78+
() => verifyPaygoAddressProof(psbt, 0, Buffer.from('Random Signed Message'), attestationPubKey),
79+
(e: any) => e.message === 'Cannot verify the paygo address signature with the provided pubkey.'
80+
);
81+
});
82+
83+
it('should add and verify a valid paygo address proof on the PSBT', () => {
84+
const psbt = getTestPsbt();
85+
psbt.addOutput({ script: utxolib.address.toOutputScript(addressToVerify, network), value: BigInt(10000) });
86+
const outputIndex = psbt.data.outputs.length - 1;
87+
addPaygoAddressProof(psbt, outputIndex, sig, Buffer.from(attestationPubKey));
88+
verifyPaygoAddressProof(psbt, outputIndex, Buffer.from(addressProofBuffer), attestationPubKey);
89+
});
90+
91+
it('should throw an error if there are multiple PayGo proprietary keys in the PSBT', () => {
92+
const outputIndex = 0;
93+
const psbt = getTestPsbt();
94+
addPaygoAddressProof(psbt, outputIndex, sig, Buffer.from(attestationPubKey));
95+
addPaygoAddressProof(psbt, outputIndex, Buffer.from('signature2'), Buffer.from('fakepubkey2s'));
96+
const output = checkForOutput(psbt.data.outputs, outputIndex);
97+
const proofInPsbt = getPaygoProprietaryKey(output.unknownKeyVals!);
98+
assert(proofInPsbt.length !== 0);
99+
assert(proofInPsbt.length > 1);
100+
assert.throws(
101+
() => verifyPaygoAddressProof(psbt, outputIndex, addressProofBuffer, attestationPubKey),
102+
(e: any) => e.message === 'There are multiple paygo address proofs encoded in the PSBT. Something went wrong.'
103+
);
104+
});
105+
});
106+
107+
describe('verifyPaygoAddressProof', () => {
108+
it('should throw an error if there is no PayGo address in PSBT', () => {
109+
const psbt = getTestPsbt();
110+
assert.throws(
111+
() => verifyPaygoAddressProof(psbt, 0, addressProofBuffer, attestationPubKey),
112+
(e: any) => e.message === 'There is no paygo address proof encoded in the PSBT at output 0.'
113+
);
114+
});
115+
});
116+
117+
describe('getPaygoAddressProofIndex', () => {
118+
it('should get PayGo address proof index from PSBT if there is one', () => {
119+
const psbt = getTestPsbt();
120+
const outputIndex = 0;
121+
addPaygoAddressProof(psbt, outputIndex, sig, Buffer.from(attestationPubKey));
122+
assert(psbtOutputIncludesPaygoAddressProof(psbt));
123+
assert(getPaygoAddressProofOutputIndex(psbt) === 0);
124+
});
125+
126+
it('should return undefined if there is no PayGo address proof in PSBT', () => {
127+
const psbt = getTestPsbt();
128+
assert(getPaygoAddressProofOutputIndex(psbt) === undefined);
129+
assert(!psbtOutputIncludesPaygoAddressProof(psbt));
130+
});
131+
132+
it('should return an error and fail if we have multiple PayGo address in the PSBT in the same output index', () => {
133+
const psbt = getTestPsbt();
134+
const outputIndex = 0;
135+
addPaygoAddressProof(psbt, outputIndex, sig, Buffer.from(attestationPubKey));
136+
addPaygoAddressProof(psbt, outputIndex, sig, Buffer.from('xpub12345abcdef29a028510d3b2d4'));
137+
assert.throws(
138+
() => getPaygoAddressProofOutputIndex(psbt),
139+
(e: any) => e.message === 'There are multiple PayGo addresses in the PSBT output 0.'
140+
);
141+
});
142+
});

modules/utxo-lib/src/bitgo/PsbtUtil.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export enum ProprietaryKeySubtype {
2020
MUSIG2_PARTICIPANT_PUB_KEYS = 0x01,
2121
MUSIG2_PUB_NONCE = 0x02,
2222
MUSIG2_PARTIAL_SIG = 0x03,
23+
PAYGO_ADDRESS_ATTESTATION_PROOF = 0x04,
2324
}
2425

2526
/**

modules/utxo-lib/src/bitgo/UtxoPsbt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
Transaction as ITransaction,
88
TransactionFromBuffer,
99
} from 'bip174/src/lib/interfaces';
10-
import { checkForInput, checkForOutput } from 'bip174/src/lib/utils';
10+
import { checkForInput } from 'bip174/src/lib/utils';
1111
import { BufferWriter, varuint } from 'bitcoinjs-lib/src/bufferutils';
1212
import { SessionKey } from '@brandonblack/musig';
1313
import { BIP32Factory, BIP32Interface } from 'bip32';

modules/utxo-lib/src/bitgo/psbt/paygoAddressProof.ts

Lines changed: 0 additions & 97 deletions
This file was deleted.

0 commit comments

Comments
 (0)