Skip to content

Commit cd96125

Browse files
chore: wip paygo util functions
TICKET: BTC-2047 chore: wip adding tests for paygo address proof util functions TICKET: BTC-2047 chore: added testing and functions to get proprietary key vals from output TICKET: BTC-2047 chore: wip added functions and tests TICKET: BTC-2047
1 parent ae3cd26 commit cd96125

File tree

4 files changed

+292
-1
lines changed

4 files changed

+292
-1
lines changed

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

Lines changed: 72 additions & 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 } from 'bip174/src/lib/utils';
10+
import { checkForInput, checkForOutput } 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';
@@ -57,6 +57,8 @@ import { getTaprootOutputKey } from '../taproot';
5757
import {
5858
getPsbtInputProprietaryKeyVals,
5959
getPsbtInputSignatureCount,
60+
getPsbtOutputProprietaryKeyVals,
61+
ProprietaryKeySearch,
6062
ProprietaryKeySubtype,
6163
PSBT_PROPRIETARY_IDENTIFIER,
6264
} from './PsbtUtil';
@@ -1152,6 +1154,75 @@ export class UtxoPsbt<Tx extends UtxoTransaction<bigint> = UtxoTransaction<bigin
11521154
return this;
11531155
}
11541156

1157+
/**
1158+
* Adds a proprietary key value pair to PSBT output
1159+
* Default identifier is utf-8 for identifier
1160+
*/
1161+
addProprietaryKeyValToOutput(outputIndex: number, keyValueData: ProprietaryKeyValue): this {
1162+
const output = checkForOutput(this.data.outputs, outputIndex);
1163+
assert(output.unknownKeyVals);
1164+
return this.addUnknownKeyValToOutput(outputIndex, {
1165+
key: encodeProprietaryKey(keyValueData.key),
1166+
value: keyValueData.value
1167+
})
1168+
}
1169+
1170+
/**
1171+
* To search any data from proprietary key value against keydata in the PSBT outputs.
1172+
* Default identifierEncoding is utf-8 for identifier.
1173+
*/
1174+
getOutputProprietaryKeyVals(outputIndex: number, keySearch?: ProprietaryKeySearch): ProprietaryKeyValue[] {
1175+
const output = checkForOutput(this.data.outputs, outputIndex);
1176+
return getPsbtOutputProprietaryKeyVals(output, keySearch);
1177+
}
1178+
1179+
/**
1180+
* Adds or updates (if exists) proprietary key value pair to PSBT output.
1181+
* Default identifierEncoding is utf-8 for identifier.
1182+
*/
1183+
addOrUpdateProprietaryKeyValsToOutput(outputIndex: number, keyValueData: ProprietaryKeyValue): this {
1184+
const output = checkForOutput(this.data.outputs, outputIndex);
1185+
const key = encodeProprietaryKey(keyValueData.key);
1186+
const { value } = keyValueData;
1187+
if (output.unknownKeyVals?.length) {
1188+
const ukvIndex = output.unknownKeyVals.findIndex((ukv) => ukv.key.equals(key));
1189+
if (ukvIndex > -1) {
1190+
output.unknownKeyVals[ukvIndex] = { key, value };
1191+
return this;
1192+
}
1193+
}
1194+
this.addUnknownKeyValToOutput(outputIndex, {
1195+
key,
1196+
value,
1197+
});
1198+
return this;
1199+
}
1200+
1201+
/**
1202+
* To delete any data from proprietary key value in PSBT output.
1203+
* Default identifierEncoding is utf-8 for identifier.
1204+
*/
1205+
deleteProprietaryKeyValsInOutput(outputIndex: number, keysToDelete?: ProprietaryKeySearch): this {
1206+
const output = checkForOutput(this.data.outputs, outputIndex);
1207+
if (!output.unknownKeyVals?.length) {
1208+
return this;
1209+
}
1210+
if (keysToDelete && keysToDelete.subtype === undefined && Buffer.isBuffer(keysToDelete.keydata)) {
1211+
throw new Error('invalid proprietary key search filter combination. subtype is required');
1212+
}
1213+
output.unknownKeyVals = output.unknownKeyVals.filter((keyValue, i) => {
1214+
const key = decodeProprietaryKey(keyValue.key);
1215+
return !(
1216+
keysToDelete === undefined ||
1217+
(keysToDelete.identifier === key.identifier &&
1218+
(keysToDelete.subtype === undefined ||
1219+
(keysToDelete.subtype === key.subtype &&
1220+
(!Buffer.isBuffer(keysToDelete.keydata) || keysToDelete.keydata.equals(key.keydata)))))
1221+
);
1222+
});
1223+
return this;
1224+
}
1225+
11551226
private createMusig2NonceForInput(
11561227
inputIndex: number,
11571228
keyPair: BIP32Interface,
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import * as assert from 'assert';
2+
import * as bitcoinMessage from 'bitcoinjs-message';
3+
import { crypto } from 'bitcoinjs-lib';
4+
5+
import { address } from '../..';
6+
import { networks } from '../../networks';
7+
import { toBase58Check } from '../../address';
8+
import { getPsbtOutputProprietaryKeyVals, ProprietaryKeySubtype, PSBT_PROPRIETARY_IDENTIFIER } from '../PsbtUtil';
9+
import { UtxoPsbt } from '../UtxoPsbt';
10+
11+
/** The function consumes the signature as a parameter and adds the PayGo address to the
12+
* PSBT output at the output index where the signature is of the format:
13+
* 0x18Bitcoin Signed Message:\n<varint_length><ENTROPY><ADDRESS><UUID> signed by
14+
* the HSM beforehand.
15+
*
16+
* @param psbt - PSBT that we need to encode our paygo address into
17+
* @param outputIndex - the index of the address in our output
18+
* @param sig - the signature that we want to encode
19+
*/
20+
export function addPaygoAddressProof(psbt: UtxoPsbt, outputIndex: number, sig: Buffer, pub: Buffer): void {
21+
psbt.addProprietaryKeyValToOutput(outputIndex, {
22+
key: {
23+
identifier: PSBT_PROPRIETARY_IDENTIFIER,
24+
subtype: ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
25+
keydata: Buffer.from(pub)
26+
},
27+
value: sig,
28+
});
29+
}
30+
31+
/** Verify the paygo address signature is valid using BitGoJs statics
32+
*
33+
* @param psbt - PSBT we want to verify that the paygo address is in
34+
* @param outputIndex - we have the output index that address is in
35+
* @param pub - The public key that we want to verify the proof with
36+
* @returns
37+
*/
38+
export function verifyPaygoAddressProof(psbt: UtxoPsbt, outputIndex: number, pub: Buffer): void {
39+
const stored = psbt.getOutputProprietaryKeyVals(outputIndex, {
40+
identifier: PSBT_PROPRIETARY_IDENTIFIER,
41+
subtype: ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
42+
});
43+
if (!stored) {
44+
throw new Error('No address proof');
45+
}
46+
47+
// assert stored length is 0 or 1
48+
if (stored.length === 0) {
49+
throw new Error(`There is no paygo address proof encoded in the PSBT at output ${outputIndex}.`);
50+
} else if (stored.length > 1) {
51+
throw new Error('There are multiple paygo address proofs encoded in the PSBT. Something went wrong.');
52+
}
53+
54+
const signature = stored[0].value;
55+
// It doesn't matter that this is bitcoin or not, we just need to convert the public key buffer into an address format
56+
// for the verification
57+
const messageToVerify = toBase58Check(crypto.hash160(pub), networks.bitcoin.pubKeyHash, networks.bitcoin);
58+
59+
// TODO: need to figure out what the message is in this context
60+
// Are we verifying the address of the PayGo? we can call getAddressFromScript given output index.
61+
if (!bitcoinMessage.verify(message, messageToVerify, signature)) {
62+
throw new Error('Cannot verify the paygo address signature with the provided pubkey.');
63+
}
64+
65+
const out = psbt.txOutputs[outputIndex];
66+
assert(out);
67+
const addressFromOutput = address.fromOutputScript(out.script, psbt.network);
68+
const addressFromProof = extractAddressFromPayGoAttestationProof(message, addressFromOutput.length);
69+
70+
if (addressFromProof !== addressFromOutput) {
71+
throw new Error(`The address from the output (${addressFromOutput}) does not match the address that is in the proof (${addressFromProof}).`)
72+
}
73+
}
74+
75+
/** Get the output index of the paygo output if there is one. It does this by
76+
* checking if the metadata is on one of the outputs of the PSBT. If there is
77+
* no paygo output, return undefined
78+
*
79+
* @param psbt
80+
* @returns number - the index of the output address
81+
*/
82+
export function getPaygoAddressProofOutputIndex(psbt: UtxoPsbt): number | undefined {
83+
const res = psbt.data.outputs.flatMap((output, outputIndex) => {
84+
const proprietaryKeyVals = getPsbtOutputProprietaryKeyVals(output, {
85+
identifier: PSBT_PROPRIETARY_IDENTIFIER,
86+
subtype: ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
87+
});
88+
89+
return proprietaryKeyVals.length === 0 ? [] : [outputIndex];
90+
});
91+
92+
return res.length === 0 ? undefined : res[0];
93+
}
94+
95+
export function psbtOutputIncludesPaygoAddressProof(psbt: UtxoPsbt): boolean {
96+
return getPaygoAddressProofOutputIndex(psbt) !== undefined;
97+
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,26 @@ export function verifyFullySignedSignatures(
261261
}
262262
});
263263
}
264+
265+
/** We generate the PayGo attestation proof based on the private key, UUID, and address of our PayGo.
266+
* We create a random entropy of 64 bytes encoded to base58 and used to create the attestation proof
267+
* in the format 0x18Bitcoin Signed Message:\n<varint_length><ENTROPY><ADDRESS><UUID>
268+
*
269+
* @param attestationPrvKey
270+
* @param uuid
271+
* @param address
272+
*/
273+
export function generatePayGoAttestationProof(attestationPrvKey: Buffer, uuid: string, address: Buffer): Buffer {
274+
// This is our prefix to our bitcoin signed message
275+
const prefixByte = Buffer.from([0x18]);
276+
const signedMessagePrefix = Buffer.from('Bitcoin Signed Message:\n', 'utf8');
277+
// We always create a 32 byte buffer array but we can implement a random
278+
// function to generate between two ranges of our length of entropy
279+
const entropy = Buffer.allocUnsafe(32);
280+
crypto.getRandomValues(entropy);
281+
const uuidToBuffer = Buffer.from(uuid, 'hex');
282+
const signedMessageBufferRaw = Buffer.concat([prefixByte, signedMessagePrefix, entropy, address, uuidToBuffer]);
283+
const varInt = Buffer.from([signedMessageBufferRaw.length]);
284+
const fullResMessageBuffer = Buffer.concat([prefixByte, signedMessagePrefix, varInt, entropy, address, uuidToBuffer])l
285+
286+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import * as assert from 'assert'
2+
import { decodeProprietaryKey } from 'bip174/src/lib/proprietaryKeyVal';
3+
import { KeyValue } from 'bip174/src/lib/interfaces';
4+
import { checkForOutput } from 'bip174/src/lib/utils';
5+
6+
import { bip32, networks, testutil } from '../../../src'
7+
import { addPaygoAddressProof, verifyPaygoAddressProof, getPaygoAddressProofOutputIndex, psbtOutputIncludesPaygoAddressProof } from "../../../src/bitgo/psbt/paygoAddressProof";
8+
import { generatePayGoAttestationProof, inputScriptTypes, outputScriptTypes } from '../../../src/testutil';
9+
import { ProprietaryKeySubtype, PSBT_PROPRIETARY_IDENTIFIER, RootWalletKeys } from '../../../src/bitgo';
10+
11+
12+
const network = networks.bitcoin;
13+
const keys = [1,2,3].map((v) => bip32.fromSeed(Buffer.alloc(16, `test/2/${v}`), network))
14+
const rootWalletKeys = new RootWalletKeys([keys[0], keys[1], keys[2]])
15+
// const dummyKey1 = rootWalletKeys.deriveForChainAndIndex(50, 200);
16+
const dummyKey2 = rootWalletKeys.deriveForChainAndIndex(60, 201);
17+
18+
const psbtInputs = inputScriptTypes.map((scriptType) => ({scriptType, value: BigInt(1000)}))
19+
const psbtOutputs = outputScriptTypes.map((scriptType) => ({ scriptType, value: BigInt(900)}))
20+
// const dummy1PubKey = dummyKey1.user.publicKey;
21+
// This generatePayGoAttestationProof function should be returning the bitcoin signed message
22+
const sig2 = dummyKey2.user.privateKey!;
23+
24+
// wallet pub and priv key for tbtc
25+
const attestationPubKey = "xpub661MyMwAqRbcFU2Qx7pvGmmiQpVj8NcR7dSVpgqNChMkQyobpVWWERcrTb47WicmXwkhAY2VrC3hb29s18FDQWJf5pLm3saN6uLXAXpw1GV";
26+
const attestationPrvKey = "red";
27+
const nilUUID = '00000000-0000-0000-0000-000000000000';
28+
const addressProofBuffer = generatePayGoAttestationProof(Buffer.from(attestationPrvKey), nilUUID, Buffer.from(address))
29+
30+
31+
function getTestPsbt() {
32+
return testutil.constructPsbt(
33+
psbtInputs, psbtOutputs, network, rootWalletKeys, 'unsigned'
34+
)
35+
}
36+
37+
describe('addPaygoAddressProof and verifyPaygoAddressProof', () => {
38+
function getPaygoProprietaryKey(proprietaryKeyVals: KeyValue[]) {
39+
return proprietaryKeyVals.map(({key, value}) => {
40+
return { key: decodeProprietaryKey(key), value };
41+
}).filter((keyValue) => {
42+
return keyValue.key.identifier === PSBT_PROPRIETARY_IDENTIFIER && keyValue.key.subtype === ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF
43+
});
44+
}
45+
46+
it("should fail a proof verification if the proof isn't valid", () => {
47+
const outputIndex = 0;
48+
const psbt = getTestPsbt();
49+
addPaygoAddressProof(psbt, outputIndex, Buffer.from(addressProofBuffer), Buffer.from(attestationPubKey));
50+
const output = checkForOutput(psbt.data.outputs, outputIndex);
51+
const proofInPsbt = getPaygoProprietaryKey(output.unknownKeyVals!);
52+
assert(proofInPsbt.length === 1)
53+
assert.throws(() => verifyPaygoAddressProof(psbt, 0, dummyKey2.user.publicKey), (e: any) => e.message === 'Cannot verify the paygo address signature with the provided pubkey.');
54+
});
55+
56+
it("should add and verify a valid paygo address proof on the PSBT", () => {
57+
const outputIndex = 0;
58+
const psbt = getTestPsbt();
59+
addPaygoAddressProof(psbt, outputIndex, Buffer.from(addressProofBuffer), Buffer.from(attestationPubKey));
60+
// should verify function return a boolean? that way we can assert
61+
// if this is verified, throws an error otherwise or false + error msg as an object
62+
verifyPaygoAddressProof(psbt, outputIndex, Buffer.from(attestationPubKey));
63+
});
64+
65+
it("should throw an error if there are multiple PayGo proprietary keys in the PSBT", () => {
66+
const outputIndex = 0;
67+
const psbt = getTestPsbt();
68+
addPaygoAddressProof(psbt, outputIndex, Buffer.from(addressProofBuffer), Buffer.from(attestationPubKey));
69+
addPaygoAddressProof(psbt, outputIndex, Buffer.from(sig2), Buffer.from(attestationPubKey));
70+
const output = checkForOutput(psbt.data.outputs, outputIndex);
71+
const proofInPsbt = getPaygoProprietaryKey(output.unknownKeyVals!);
72+
assert(proofInPsbt.length !== 0)
73+
assert(proofInPsbt.length <= 1)
74+
assert.throws(() => verifyPaygoAddressProof(psbt, outputIndex, Buffer.from(attestationPubKey)), (e: any) => e.message === 'There are multiple paygo address proofs encoded in the PSBT. Something went wrong.');
75+
});
76+
});
77+
78+
79+
describe('verifyPaygoAddressProof', () => {
80+
it('should throw an error if there is no PayGo address in PSBT', () => {
81+
const psbt = getTestPsbt();
82+
assert.throws(() => verifyPaygoAddressProof(psbt, 0, Buffer.from(attestationPubKey)), (e: any) => e.message === 'here is no paygo address proof encoded in the PSBT.');
83+
});
84+
});
85+
86+
describe('getPaygoAddressProofIndex', () => {
87+
it('should get PayGo address proof index from PSBT if there is one', () => {
88+
const psbt = getTestPsbt();
89+
const outputIndex = 0;
90+
addPaygoAddressProof(psbt, outputIndex, Buffer.from(addressProofBuffer), Buffer.from(attestationPubKey));
91+
assert(psbtOutputIncludesPaygoAddressProof(psbt));
92+
assert(getPaygoAddressProofOutputIndex(psbt) === 0)
93+
});
94+
95+
it("should return undefined if there is no PayGo address proof in PSBT", () => {
96+
const psbt = getTestPsbt();
97+
assert(getPaygoAddressProofOutputIndex(psbt) === undefined)
98+
assert(!psbtOutputIncludesPaygoAddressProof(psbt))
99+
});
100+
});

0 commit comments

Comments
 (0)