Skip to content

Commit 4f6021d

Browse files
chore: added testing and functions to get proprietary key vals from output
TICKET: BTC-2047
1 parent 54c13ab commit 4f6021d

File tree

3 files changed

+141
-84
lines changed

3 files changed

+141
-84
lines changed

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

Lines changed: 71 additions & 3 deletions
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,7 @@ import { getTaprootOutputKey } from '../taproot';
5757
import {
5858
getPsbtInputProprietaryKeyVals,
5959
getPsbtInputSignatureCount,
60+
getPsbtOutputProprietaryKeyVals,
6061
ProprietaryKeySearch,
6162
ProprietaryKeySubtype,
6263
ProprietaryKeyValue,
@@ -1109,7 +1110,7 @@ export class UtxoPsbt<Tx extends UtxoTransaction<bigint> = UtxoTransaction<bigin
11091110
}
11101111

11111112
/**
1112-
* To search any data from proprietary key value against keydata.
1113+
* To search any data from proprietary key value against keydata in the inputs.
11131114
* Default identifierEncoding is utf-8 for identifier.
11141115
*/
11151116
getProprietaryKeyVals(inputIndex: number, keySearch?: ProprietaryKeySearch): ProprietaryKeyValue[] {
@@ -1118,7 +1119,7 @@ export class UtxoPsbt<Tx extends UtxoTransaction<bigint> = UtxoTransaction<bigin
11181119
}
11191120

11201121
/**
1121-
* To delete any data from proprietary key value.
1122+
* To delete any data from proprietary key value in PSBT input.
11221123
* Default identifierEncoding is utf-8 for identifier.
11231124
*/
11241125
deleteProprietaryKeyVals(inputIndex: number, keysToDelete?: ProprietaryKeySearch): this {
@@ -1142,6 +1143,73 @@ export class UtxoPsbt<Tx extends UtxoTransaction<bigint> = UtxoTransaction<bigin
11421143
return this;
11431144
}
11441145

1146+
/**
1147+
* Adds a proprietary key value pair to PSBT output
1148+
* Default identifier is utf-8 for identifier
1149+
*/
1150+
addProprietaryKeyValToOutput(outputIndex: number, keyValueData: ProprietaryKeyValue): this {
1151+
return this.addUnknownKeyValToOutput(outputIndex, {
1152+
key: encodeProprietaryKey(keyValueData.key),
1153+
value: keyValueData.value
1154+
})
1155+
}
1156+
1157+
/**
1158+
* To search any data from proprietary key value against keydata in the PSBT outputs.
1159+
* Default identifierEncoding is utf-8 for identifier.
1160+
*/
1161+
getOutputProprietaryKeyVals(outputIndex: number, keySearch?: ProprietaryKeySearch): ProprietaryKeyValue[] {
1162+
const output = checkForOutput(this.data.outputs, outputIndex);
1163+
return getPsbtOutputProprietaryKeyVals(output, keySearch);
1164+
}
1165+
1166+
/**
1167+
* Adds or updates (if exists) proprietary key value pair to PSBT output.
1168+
* Default identifierEncoding is utf-8 for identifier.
1169+
*/
1170+
addOrUpdateProprietaryKeyValsToOutput(outputIndex: number, keyValueData: ProprietaryKeyValue): this {
1171+
const output = checkForOutput(this.data.outputs, outputIndex);
1172+
const key = encodeProprietaryKey(keyValueData.key);
1173+
const { value } = keyValueData;
1174+
if (output.unknownKeyVals?.length) {
1175+
const ukvIndex = output.unknownKeyVals.findIndex((ukv) => ukv.key.equals(key));
1176+
if (ukvIndex > -1) {
1177+
output.unknownKeyVals[ukvIndex] = { key, value };
1178+
return this;
1179+
}
1180+
}
1181+
this.addUnknownKeyValToOutput(outputIndex, {
1182+
key,
1183+
value,
1184+
});
1185+
return this;
1186+
}
1187+
1188+
/**
1189+
* To delete any data from proprietary key value in PSBT output.
1190+
* Default identifierEncoding is utf-8 for identifier.
1191+
*/
1192+
deleteProprietaryKeyValsInOutput(outputIndex: number, keysToDelete?: ProprietaryKeySearch): this {
1193+
const output = checkForOutput(this.data.outputs, outputIndex);
1194+
if (!output.unknownKeyVals?.length) {
1195+
return this;
1196+
}
1197+
if (keysToDelete && keysToDelete.subtype === undefined && Buffer.isBuffer(keysToDelete.keydata)) {
1198+
throw new Error('invalid proprietary key search filter combination. subtype is required');
1199+
}
1200+
output.unknownKeyVals = output.unknownKeyVals.filter((keyValue, i) => {
1201+
const key = decodeProprietaryKey(keyValue.key);
1202+
return !(
1203+
keysToDelete === undefined ||
1204+
(keysToDelete.identifier === key.identifier &&
1205+
(keysToDelete.subtype === undefined ||
1206+
(keysToDelete.subtype === key.subtype &&
1207+
(!Buffer.isBuffer(keysToDelete.keydata) || keysToDelete.keydata.equals(key.keydata)))))
1208+
);
1209+
});
1210+
return this;
1211+
}
1212+
11451213
private createMusig2NonceForInput(
11461214
inputIndex: number,
11471215
keyPair: BIP32Interface,

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

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
11
import * as bitcoinMessage from 'bitcoinjs-message';
22
import { crypto } from 'bitcoinjs-lib';
3-
import { ProprietaryKey } from 'bip174/src/lib/proprietaryKeyVal';
43

54
import { networks } from '../../networks';
65
import { toBase58Check } from '../../address';
76
import { getPsbtOutputProprietaryKeyVals, ProprietaryKeySubtype, PSBT_PROPRIETARY_IDENTIFIER } from '../PsbtUtil';
87
import { UtxoPsbt } from '../UtxoPsbt';
98

10-
export const PayGoAddressProofKey: ProprietaryKey = {
11-
identifier: PSBT_PROPRIETARY_IDENTIFIER,
12-
subtype: ProprietaryKeySubtype.PAYGO_ADDRESS_PROOF,
13-
// Is there a better way to not provide a pubkey?
14-
keydata: Buffer.from([]),
15-
};
16-
179
/**
1810
*
1911
* @param psbt - PSBT that we need to encode our paygo address into
20-
* @param inputIndex - the index of the address in our output
12+
* @param outputIndex - the index of the address in our output
2113
* @param sig - the signature that we want to encode
2214
*/
23-
export function addPaygoAddressProof(psbt: UtxoPsbt, inputIndex: number, sig: Buffer): void {
24-
psbt.addProprietaryKeyValToInput(inputIndex, {
25-
key: PayGoAddressProofKey,
15+
export function addPaygoAddressProof(psbt: UtxoPsbt, outputIndex: number, sig: Buffer, pub: Buffer): void {
16+
psbt.addProprietaryKeyValToOutput(outputIndex, {
17+
key: {
18+
identifier: PSBT_PROPRIETARY_IDENTIFIER,
19+
subtype: ProprietaryKeySubtype.PAYGO_ADDRESS_PROOF,
20+
keydata: Buffer.from(pub)
21+
},
2622
value: sig,
2723
});
2824
}
@@ -35,7 +31,7 @@ export function addPaygoAddressProof(psbt: UtxoPsbt, inputIndex: number, sig: Bu
3531
* @returns
3632
*/
3733
export function verifyPaygoAddressProof(psbt: UtxoPsbt, outputIndex: number, pub: Buffer): void {
38-
const stored = psbt.getProprietaryKeyVals(outputIndex, {
34+
const stored = psbt.getOutputProprietaryKeyVals(outputIndex, {
3935
identifier: PSBT_PROPRIETARY_IDENTIFIER,
4036
subtype: ProprietaryKeySubtype.PAYGO_ADDRESS_PROOF,
4137
});
Lines changed: 61 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,94 @@
11
import * as assert from 'assert'
2-
import { testutil, networks } from 'modules/utxo-lib/src';
3-
import { addPaygoAddressProof, verifyPaygoAddressProof, getPaygoAddressProofIndex } from "modules/utxo-lib/src/bitgo/psbt/paygoAddressProof";
4-
import { getDefaultWalletKeys, inputScriptTypes, outputScriptTypes } from 'modules/utxo-lib/src/testutil';
5-
6-
import { SignatureTargetType } from './Psbt';
7-
import { PSBT_PROPRIETARY_IDENTIFIER, UtxoPsbt } from 'modules/utxo-lib/src/bitgo';
82
import { decodeProprietaryKey } from 'bip174/src/lib/proprietaryKeyVal';
93
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, getPaygoAddressProofIndex, psbtIncludesPaygoAddressProof } from "../../../src/bitgo/psbt/paygoAddressProof";
8+
import { inputScriptTypes, outputScriptTypes } from '../../../src/testutil';
9+
import { ProprietaryKeySubtype, PSBT_PROPRIETARY_IDENTIFIER, RootWalletKeys } from '../../../src/bitgo';
10+
1011

1112
const network = networks.bitcoin;
12-
const rootWalletKeys = getDefaultWalletKeys();
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+
const dumm1yXPubs = dummyKey1.publicKeys;
18+
const dummy1PubKey = dummyKey1.user.publicKey;
1319

1420
const psbtInputs = inputScriptTypes.map((scriptType) => ({scriptType, value: BigInt(1000)}))
1521
const psbtOutputs = outputScriptTypes.map((scriptType) => ({ scriptType, value: BigInt(900)}))
16-
const sig = 'paygoaddresssig'
22+
const sig = dummyKey1.user.privateKey!;
23+
const sig2 = dummyKey2.user.privateKey!;
1724

18-
function getTestPsbt(inputs: testutil.Input[], outputs: testutil.Output[], signed: SignatureTargetType) {
25+
function getTestPsbt() {
1926
return testutil.constructPsbt(
20-
inputs, outputs, network, rootWalletKeys, signed
27+
psbtInputs, psbtOutputs, network, rootWalletKeys, 'unsigned'
2128
)
2229
}
2330

24-
function addPaygoAddressProofToPsbt(index: number, hasInput: boolean, hasOutput: boolean): UtxoPsbt {
25-
// will update this function accordingly, just have this for now to help to simple testing
26-
const psbt = getTestPsbt(hasInput ? psbtInputs : [], hasOutput ? psbtOutputs : [], hasInput && hasOutput ? 'unsigned' : 'fullsigned');
27-
addPaygoAddressProof(psbt, index, Buffer.from(sig));
28-
return psbt;
29-
}
30-
31-
describe('addPaygoAddressProof function', () => {
31+
describe('addPaygoAddressProof and verifyPaygoAddressProof', () => {
3232
function getPaygoProprietaryKey(proprietaryKeyVals: KeyValue[]) {
3333
return proprietaryKeyVals.map(({key, value}) => {
3434
return { key: decodeProprietaryKey(key), value };
3535
}).filter((keyValue) => {
36-
return keyValue.key.identifier === PSBT_PROPRIETARY_IDENTIFIER && keyValue.value === Buffer.from(sig)
36+
return keyValue.key.identifier === PSBT_PROPRIETARY_IDENTIFIER && keyValue.key.subtype === ProprietaryKeySubtype.PAYGO_ADDRESS_PROOF
3737
});
3838
}
39-
it('should add PayGo Address proof to empty PSBT', () => {
40-
const inputIndex = 0;
41-
const psbt = getTestPsbt([], [], 'unsigned');
42-
// better sig test replacement, i just have random string as test
43-
addPaygoAddressProof(psbt, inputIndex, Buffer.from(sig));
44-
const proprietaryKeyVals = psbt.data.globalMap.unknownKeyVals;
45-
assert(proprietaryKeyVals)
46-
// I assume that the proprietaryKeyVal should be properly added
47-
// to the unknownKeyVals globalmap
48-
const proofInPsbt = getPaygoProprietaryKey(proprietaryKeyVals)
49-
assert(proofInPsbt.length === 1);
50-
});
5139

52-
it('should add Paygo Adress Proof to non-empty PSBT', () => {
53-
const inputIndex = 0;
54-
const psbt = getTestPsbt(psbtInputs, psbtOutputs, 'fullsigned');
55-
addPaygoAddressProof(psbt, inputIndex, Buffer.from(sig));
56-
const proprietaryKeyVals = psbt.data.globalMap.unknownKeyVals;
57-
assert(proprietaryKeyVals);
58-
const proofInPsbt = getPaygoProprietaryKey(proprietaryKeyVals);
59-
assert(proofInPsbt.length === 1);
40+
it("should fail a proof verification if the proof isn't valid", () => {
41+
const outputIndex = 0;
42+
const psbt = getTestPsbt();
43+
addPaygoAddressProof(psbt, outputIndex, Buffer.from(sig), dummy1PubKey);
44+
const output = checkForOutput(psbt.data.outputs, outputIndex);
45+
const proofInPsbt = getPaygoProprietaryKey(output.unknownKeyVals!);
46+
assert(proofInPsbt.length === 1)
47+
assert.throws(() => verifyPaygoAddressProof(psbt, 0, dummyKey2.user.publicKey), (e: any) => e.message === 'Cannot verify the paygo address signature with the provided pubkey.');
6048
});
6149

62-
// do we have an error check with the original addProprietaryKeyValToInput
63-
// to see if there are duplicates of the same? if not I will add the test case here.
64-
})
65-
66-
67-
describe('verifyPaygoAddressProof', () => {
68-
const pubkey = 'pubkeytestforpaygopsbt'
69-
70-
it('should verify a valid PayGo address proof', () => {
71-
const indexInput = 0;
72-
const psbt = addPaygoAddressProofToPsbt(indexInput, false, false);
73-
verifyPaygoAddressProof(psbt, 0, Buffer.from(pubkey))
74-
// nothing happens if verification is successful
50+
it("should add and verify a valid paygo address proof on the PSBT", () => {
51+
const outputIndex = 0;
52+
const psbt = getTestPsbt();
53+
addPaygoAddressProof(psbt, outputIndex, Buffer.from(sig), dummy1PubKey);
54+
// should verify function return a boolean? that way we can assert
55+
// if this is verified, throws an error otherwise or false + error msg as an object
56+
verifyPaygoAddressProof(psbt, outputIndex, dummy1PubKey);
7557
});
7658

77-
it('should throw an error if there is no PayGo address in PSBT', () => {
78-
const psbt = getTestPsbt(psbtInputs, psbtOutputs, 'fullsigned');
79-
assert.throws(() => verifyPaygoAddressProof(psbt, 0, Buffer.from(pubkey)), (e: any) => e.message === 'here is no paygo address proof encoded in the PSBT.')
59+
it("should throw an error if there are multiple PayGo proprietary keys in the PSBT", () => {
60+
const outputIndex = 0;
61+
const psbt = getTestPsbt();
62+
addPaygoAddressProof(psbt, outputIndex, Buffer.from(sig), dummy1PubKey);
63+
addPaygoAddressProof(psbt, outputIndex, Buffer.from(sig2), dummy1PubKey);
64+
const output = checkForOutput(psbt.data.outputs, outputIndex);
65+
const proofInPsbt = getPaygoProprietaryKey(output.unknownKeyVals!);
66+
assert(proofInPsbt.length !== 0)
67+
assert(proofInPsbt.length <= 1)
68+
assert.throws(() => verifyPaygoAddressProof(psbt, outputIndex, dummy1PubKey), (e: any) => e.message === 'There are multiple paygo address proofs encoded in the PSBT. Something went wrong.');
8069
});
70+
});
8171

82-
it('should throw an error if there is multiple PayGo address in PSBT', () => {
83-
const psbt = getTestPsbt(psbtInputs, psbtOutputs, 'fullsigned');
84-
const sig2 = 'paygoaddresssig2'
85-
addPaygoAddressProof(psbt, 0, Buffer.from(sig))
86-
addPaygoAddressProof(psbt, 1, Buffer.from(sig2))
87-
assert.throws(() => verifyPaygoAddressProof(psbt, 0, Buffer.from(pubkey)), (e: any) => e.message === 'There are multiple paygo address proofs encoded in the PSBT. Something went wrong.')
88-
});
8972

90-
// Once we think of what message should be signing, we can generate a test case
91-
// to test this error message properly
92-
it('should throw an error if the verification fails', () => {
93-
const psbt = addPaygoAddressProofToPsbt(0, false, false);
94-
assert.throws(() => verifyPaygoAddressProof(psbt, 0, Buffer.from(pubkey + 's')), (e: any) => e.message === 'Cannot verify the paygo address signature with the provided pubkey.')
73+
describe('verifyPaygoAddressProof', () => {
74+
it('should throw an error if there is no PayGo address in PSBT', () => {
75+
const psbt = getTestPsbt();
76+
assert.throws(() => verifyPaygoAddressProof(psbt, 0, dummy1PubKey), (e: any) => e.message === 'here is no paygo address proof encoded in the PSBT.');
9577
});
9678
});
9779

9880
describe('getPaygoAddressProofIndex', () => {
99-
it('should get PayGo address proof index from PSBT', () => {
81+
it('should get PayGo address proof index from PSBT if there is one', () => {
82+
const psbt = getTestPsbt();
83+
const outputIndex = 0;
84+
addPaygoAddressProof(psbt, outputIndex, Buffer.from(sig), dummy1PubKey);
85+
assert(psbtIncludesPaygoAddressProof(psbt));
86+
assert(getPaygoAddressProofIndex(psbt) === 1)
87+
});
88+
89+
it("should return undefined if there is no PayGo address proof in PSBT", () => {
90+
const psbt = getTestPsbt();
91+
assert(getPaygoAddressProofIndex(psbt) === undefined)
92+
assert(!psbtIncludesPaygoAddressProof(psbt))
10093
});
10194
});

0 commit comments

Comments
 (0)