Skip to content

Commit 367b82d

Browse files
Merge pull request #6155 from BitGo/BTC-2047.adding-paygoinfo
feat(utxo-core): PayGo Attestation util functions
2 parents f08b154 + da0fe48 commit 367b82d

File tree

11 files changed

+353
-2
lines changed

11 files changed

+353
-2
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import assert from 'assert';
2+
3+
import * as utxolib from '@bitgo/utxo-lib';
4+
export const NIL_UUID = '00000000-0000-0000-0000-000000000000';
5+
6+
/** This function reconstructs the proof <ENTROPY><ADDRESS><UUID>
7+
* given the address and entropy.
8+
*
9+
* @param address
10+
* @param entropy
11+
* @returns
12+
*/
13+
export function createPayGoAttestationBuffer(address: string, entropy: Buffer, network: utxolib.Network): Buffer {
14+
assert(address.length > 0);
15+
const isValidAddress = utxolib.address.toOutputScript(address, network);
16+
assert(isValidAddress, `Address ${address} is not a valid address.`);
17+
const addressBuffer = Buffer.from(address);
18+
return Buffer.concat([entropy, addressBuffer, Buffer.from(NIL_UUID)]);
19+
}

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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export class PayGoError extends Error {
2+
constructor(message: string) {
3+
super(message);
4+
this.name = this.constructor.name;
5+
}
6+
}
7+
8+
export class ErrorNoPayGoProof extends PayGoError {
9+
constructor(public outputIndex: number) {
10+
super(`There is no paygo address proof encoded in the PSBT at output ${outputIndex}.`);
11+
}
12+
}
13+
14+
export class ErrorMultiplePayGoProof extends PayGoError {
15+
constructor() {
16+
super('There are multiple paygo address proofs encoded in the PSBT. Something went wrong.');
17+
}
18+
}
19+
20+
export class ErrorPayGoAddressProofFailedVerification extends PayGoError {
21+
constructor() {
22+
super('Cannot verify the paygo address signature with the provided pubkey.');
23+
}
24+
}
25+
26+
export class ErrorOutputIndexOutOfBounds extends PayGoError {
27+
constructor(public outputIndex: number) {
28+
super(`Output index ${outputIndex} is out of bounds for PSBT outputs.`);
29+
}
30+
}
31+
32+
export class ErrorMultiplePayGoProofAtPsbtIndex extends PayGoError {
33+
constructor(public outputIndex: number) {
34+
super(`There are multiple PayGo addresses in the PSBT output ${outputIndex}.`);
35+
}
36+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './payGoAddressProof';
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
import { checkForOutput } from 'bip174/src/lib/utils';
3+
4+
import { verifyMessage } from '../../bip32utils';
5+
import { createPayGoAttestationBuffer } from '../attestation';
6+
7+
import {
8+
ErrorMultiplePayGoProof,
9+
ErrorMultiplePayGoProofAtPsbtIndex,
10+
ErrorNoPayGoProof,
11+
ErrorOutputIndexOutOfBounds,
12+
ErrorPayGoAddressProofFailedVerification,
13+
} from './Errors';
14+
15+
/** This function adds the entropy and signature into the PSBT output unknown key vals.
16+
* We store the entropy so that we reconstruct the message <ENTROPY><ADDRESS><UUID>
17+
* to later verify.
18+
*
19+
* @param psbt - PSBT that we need to encode our paygo address into
20+
* @param outputIndex - the index of the address in our output
21+
* @param sig - the signature that we want to encode
22+
*/
23+
export function addPayGoAddressProof(
24+
psbt: utxolib.bitgo.UtxoPsbt,
25+
outputIndex: number,
26+
sig: Buffer,
27+
entropy: Buffer
28+
): void {
29+
utxolib.bitgo.addProprietaryKeyValuesFromUnknownKeyValues(psbt, 'output', outputIndex, {
30+
key: {
31+
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
32+
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
33+
keydata: entropy,
34+
},
35+
value: sig,
36+
});
37+
}
38+
39+
/** Verify the paygo address signature is valid using verification pub key.
40+
*
41+
* @param psbt - PSBT we want to verify that the paygo address is in
42+
* @param outputIndex - we have the output index that address is in
43+
* @param uuid
44+
* @returns
45+
*/
46+
export function verifyPayGoAddressProof(
47+
psbt: utxolib.bitgo.UtxoPsbt,
48+
outputIndex: number,
49+
verificationPubkey: Buffer
50+
): void {
51+
const psbtOutputs = checkForOutput(psbt.data.outputs, outputIndex);
52+
const stored = utxolib.bitgo.getProprietaryKeyValuesFromUnknownKeyValues(psbtOutputs, {
53+
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
54+
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
55+
});
56+
57+
// assert stored length is 0 or 1
58+
if (stored.length === 0) {
59+
throw new ErrorNoPayGoProof(outputIndex);
60+
} else if (stored.length > 1) {
61+
throw new ErrorMultiplePayGoProof();
62+
}
63+
64+
// We get the signature and entropy from our PSBT unknown key vals
65+
const signature = stored[0].value;
66+
const entropy = stored[0].key.keydata;
67+
68+
// Get the the PayGo address from the txOutputs
69+
const txOutputs = psbt.txOutputs;
70+
if (outputIndex >= txOutputs.length) {
71+
throw new ErrorOutputIndexOutOfBounds(outputIndex);
72+
}
73+
const output = txOutputs[outputIndex];
74+
const addressFromOutput = utxolib.address.fromOutputScript(output.script, psbt.network);
75+
76+
// We construct our message <ENTROPY><ADDRESS><UUID>
77+
const message = createPayGoAttestationBuffer(addressFromOutput, entropy, psbt.network);
78+
79+
if (!verifyMessage(message.toString(), verificationPubkey, signature, utxolib.networks.bitcoin)) {
80+
throw new ErrorPayGoAddressProofFailedVerification();
81+
}
82+
}
83+
84+
/** Get the output index of the paygo output if there is one. It does this by
85+
* checking if the metadata is on one of the outputs of the PSBT. If there is
86+
* no paygo output, return undefined
87+
*
88+
* @param psbt
89+
* @returns number - the index of the output address
90+
*/
91+
export function getPayGoAddressProofOutputIndex(psbt: utxolib.bitgo.UtxoPsbt): number | undefined {
92+
const res = psbt.data.outputs.flatMap((output, outputIndex) => {
93+
const proprietaryKeyVals = utxolib.bitgo.getPsbtOutputProprietaryKeyVals(output, {
94+
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
95+
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
96+
});
97+
98+
if (proprietaryKeyVals.length > 1) {
99+
throw new ErrorMultiplePayGoProofAtPsbtIndex(outputIndex);
100+
}
101+
102+
return proprietaryKeyVals.length === 0 ? [] : [outputIndex];
103+
});
104+
105+
return res.length === 0 ? undefined : res[0];
106+
}
107+
108+
export function psbtOutputIncludesPaygoAddressProof(psbt: utxolib.bitgo.UtxoPsbt): boolean {
109+
return getPayGoAddressProofOutputIndex(psbt) !== undefined;
110+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './fixtures.utils';
22
export * from './key.utils';
33
export * from './toPlainObject.utils';
44
export * from './generatePayGoAttestationProof.utils';
5+
export * from './trimMessagePrefix';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
3+
/** We receive a proof in the form:
4+
* 0x18Bitcoin Signed Message:\n<varint_length><ENTROPY><ADDRESS><UUID>
5+
* and when verifying our message we want to exclude the 0x18Bitcoin Signed Message:\n<varint_length>
6+
* of the proof so that we are left with the entropy address and uuid as our message.
7+
* This is what we are going to be verifying.
8+
*
9+
* @param proof
10+
* @returns
11+
*/
12+
export function trimMessagePrefix(proof: Buffer): Buffer {
13+
const prefix = '\u0018Bitcoin Signed Message:\n';
14+
if (proof.toString().startsWith(prefix)) {
15+
proof = proof.slice(Buffer.from(prefix).length);
16+
utxolib.bufferutils.varuint.decode(proof, 0);
17+
// Determines how many bytes were consumed during our last varuint.decode(Buffer, offset)
18+
// So if varuint.decode(0xfd) then varuint.decode.bytes = 3
19+
// varuint.decode(0xfe) then varuint.decode.bytes = 5, etc.
20+
const varintBytesLength = utxolib.bufferutils.varuint.decode.bytes;
21+
22+
proof.slice(varintBytesLength);
23+
}
24+
return proof;
25+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import assert from 'assert';
2+
3+
import { createPayGoAttestationBuffer } from '../../src/paygo/attestation';
4+
import { generatePayGoAttestationProof } from '../../src/testutil';
5+
6+
import { addressProofEntropy, addressProofMsgBuffer, addressToVerify, network } from './psbt/payGoAddressProof';
7+
8+
describe('createPayGoAttestationBuffer', () => {
9+
it('should create a PayGo Attestation proof matching with original proof', () => {
10+
const payGoAttestationProof = createPayGoAttestationBuffer(addressToVerify, addressProofEntropy, network);
11+
assert.strictEqual(payGoAttestationProof.toString(), addressProofMsgBuffer.toString());
12+
assert(Buffer.compare(payGoAttestationProof, addressProofMsgBuffer) === 0);
13+
});
14+
15+
it('should create a PayGo Attestation proof that does not match with different uuid', () => {
16+
const addressProofBufferDiffUuid = generatePayGoAttestationProof(
17+
'00000000-0000-0000-0000-000000000001',
18+
Buffer.from(addressToVerify)
19+
);
20+
const payGoAttestationProof = createPayGoAttestationBuffer(addressToVerify, addressProofEntropy, network);
21+
assert.notStrictEqual(payGoAttestationProof.toString(), addressProofBufferDiffUuid.toString());
22+
assert(Buffer.compare(payGoAttestationProof, addressProofBufferDiffUuid) !== 0);
23+
});
24+
});
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import assert from 'assert';
2+
import crypto from 'crypto';
3+
4+
import * as utxolib from '@bitgo/utxo-lib';
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/payGoAddressProof';
15+
import { generatePayGoAttestationProof } from '../../../src/testutil/generatePayGoAttestationProof.utils';
16+
import { trimMessagePrefix } from '../../../src/testutil/trimMessagePrefix';
17+
import { signMessage } from '../../../src/bip32utils';
18+
import { NIL_UUID } from '../../../src/paygo/attestation';
19+
20+
// To construct our PSBTs
21+
export const network = utxolib.networks.bitcoin;
22+
const keys = [1, 2, 3].map((v) => utxolib.bip32.fromSeed(Buffer.alloc(16, `test/2/${v}`), network));
23+
const rootWalletKeys = new utxolib.bitgo.RootWalletKeys([keys[0], keys[1], keys[2]]);
24+
25+
// PSBT INPUTS AND OUTPUTS
26+
const psbtInputs = utxolib.testutil.inputScriptTypes.map((scriptType) => ({
27+
scriptType,
28+
value: BigInt(1000),
29+
}));
30+
const psbtOutputs = utxolib.testutil.outputScriptTypes.map((scriptType) => ({
31+
scriptType,
32+
value: BigInt(900),
33+
}));
34+
35+
// wallet pub and priv key for tbtc
36+
const dummyPub1 = rootWalletKeys.deriveForChainAndIndex(50, 200);
37+
export const attestationPubKey = dummyPub1.user.publicKey;
38+
export const attestationPrvKey = dummyPub1.user.privateKey!;
39+
40+
// our xpub converted to base58 address
41+
export 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+
export const addressProofBuffer = generatePayGoAttestationProof(NIL_UUID, Buffer.from(addressToVerify));
49+
export const addressProofMsgBuffer = trimMessagePrefix(addressProofBuffer);
50+
// We know that that the entropy is a set 64 bytes.
51+
export const addressProofEntropy = addressProofMsgBuffer.subarray(0, 65);
52+
53+
// signature with the given msg addressProofBuffer
54+
export const sig = signMessage(addressProofMsgBuffer.toString(), attestationPrvKey!, network);
55+
56+
function getTestPsbt() {
57+
return utxolib.testutil.constructPsbt(psbtInputs, psbtOutputs, network, rootWalletKeys, 'unsigned');
58+
}
59+
60+
describe('addPaygoAddressProof and verifyPaygoAddressProof', () => {
61+
function getPayGoProprietaryKey(proprietaryKeyVals: KeyValue[]) {
62+
return proprietaryKeyVals
63+
.map(({ key, value }) => {
64+
return { key: decodeProprietaryKey(key), value };
65+
})
66+
.filter((keyValue) => {
67+
return (
68+
keyValue.key.identifier === utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER &&
69+
keyValue.key.subtype === utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF
70+
);
71+
});
72+
}
73+
74+
it('should add and verify a valid paygo address proof on the PSBT', () => {
75+
const psbt = getTestPsbt();
76+
psbt.addOutput({ script: utxolib.address.toOutputScript(addressToVerify, network), value: BigInt(10000) });
77+
const outputIndex = psbt.data.outputs.length - 1;
78+
addPayGoAddressProof(psbt, outputIndex, sig, addressProofEntropy);
79+
verifyPayGoAddressProof(psbt, outputIndex, attestationPubKey);
80+
});
81+
82+
it('should throw an error if there are multiple PayGo proprietary keys in the PSBT', () => {
83+
const outputIndex = 0;
84+
const psbt = getTestPsbt();
85+
addPayGoAddressProof(psbt, outputIndex, sig, addressProofEntropy);
86+
addPayGoAddressProof(psbt, outputIndex, Buffer.from('signature2'), crypto.randomBytes(64));
87+
const output = checkForOutput(psbt.data.outputs, outputIndex);
88+
const proofInPsbt = getPayGoProprietaryKey(output.unknownKeyVals!);
89+
assert(proofInPsbt.length !== 0);
90+
assert(proofInPsbt.length > 1);
91+
assert.throws(
92+
() => verifyPayGoAddressProof(psbt, outputIndex, attestationPubKey),
93+
(e: any) => e.message === 'There are multiple paygo address proofs encoded in the PSBT. Something went wrong.'
94+
);
95+
});
96+
});
97+
98+
describe('verifyPaygoAddressProof', () => {
99+
it('should throw an error if there is no PayGo address in PSBT', () => {
100+
const psbt = getTestPsbt();
101+
assert.throws(
102+
() => verifyPayGoAddressProof(psbt, 0, attestationPubKey),
103+
(e: any) => e.message === 'There is no paygo address proof encoded in the PSBT at output 0.'
104+
);
105+
});
106+
});
107+
108+
describe('getPaygoAddressProofIndex', () => {
109+
it('should get PayGo address proof index from PSBT if there is one', () => {
110+
const psbt = getTestPsbt();
111+
const outputIndex = 0;
112+
addPayGoAddressProof(psbt, outputIndex, sig, Buffer.from(attestationPubKey));
113+
assert(psbtOutputIncludesPaygoAddressProof(psbt));
114+
assert(getPayGoAddressProofOutputIndex(psbt) === 0);
115+
});
116+
117+
it('should return undefined if there is no PayGo address proof in PSBT', () => {
118+
const psbt = getTestPsbt();
119+
assert(getPayGoAddressProofOutputIndex(psbt) === undefined);
120+
assert(!psbtOutputIncludesPaygoAddressProof(psbt));
121+
});
122+
123+
it('should return an error and fail if we have multiple PayGo address in the PSBT in the same output index', () => {
124+
const psbt = getTestPsbt();
125+
const outputIndex = 0;
126+
addPayGoAddressProof(psbt, outputIndex, sig, addressProofEntropy);
127+
addPayGoAddressProof(psbt, outputIndex, sig, crypto.randomBytes(64));
128+
assert.throws(
129+
() => getPayGoAddressProofOutputIndex(psbt),
130+
(e: any) => e.message === 'There are multiple PayGo addresses in the PSBT output 0.'
131+
);
132+
});
133+
});

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/zcash/address.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function toBase58Check(hash: Buffer, version: number): string {
2323
}
2424

2525
export function fromOutputScript(outputScript: Buffer, network: Network): string {
26-
assert(isZcash(network));
26+
assert.ok(isZcash(network));
2727
let o;
2828
let prefix;
2929
try {
@@ -41,7 +41,7 @@ export function fromOutputScript(outputScript: Buffer, network: Network): string
4141
}
4242

4343
export function toOutputScript(address: string, network: Network): Buffer {
44-
assert(isZcash(network));
44+
assert.ok(isZcash(network));
4545
const { version, hash } = fromBase58Check(address);
4646
if (version === network.pubKeyHash) {
4747
return payments.p2pkh({ hash }).output as Buffer;

0 commit comments

Comments
 (0)