Skip to content

feat(utxo-core): PayGo Attestation util functions #6155

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions modules/utxo-core/src/paygo/attestation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const NILL_UUID = '00000000-0000-0000-0000-000000000000';

/** This function reconstructs the proof <ENTROPY><ADDRESS><UUID>
* given the address and entropy.
*
* @param address
* @param entropy
* @returns
*/
export function createPayGoAttestationBuffer(address: string, entropy: Buffer): Buffer {
const addressBuffer = Buffer.from(address);
return Buffer.concat([entropy, addressBuffer, Buffer.from(NILL_UUID)]);
}
1 change: 1 addition & 0 deletions modules/utxo-core/src/paygo/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './ExtractAddressPayGoAttestation';
export * from './psbt';
29 changes: 29 additions & 0 deletions modules/utxo-core/src/paygo/psbt/Errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export class ErrorNoPayGoProof extends Error {
constructor(public outputIndex: number) {
super(`There is no paygo address proof encoded in the PSBT at output ${outputIndex}.`);
}
}

export class ErrorMultiplePayGoProof extends Error {
constructor() {
super('There are multiple paygo address proofs encoded in the PSBT. Something went wrong.');
}
}

export class ErrorPayGoAddressProofFailedVerification extends Error {
constructor() {
super('Cannot verify the paygo address signature with the provided pubkey.');
}
}

export class ErrorOutputIndexOutOfBounds extends Error {
constructor(public outputIndex: number) {
super(`Output index ${outputIndex} is out of bounds for PSBT outputs.`);
}
}

export class ErrorMultiplePayGoProofAtPsbtIndex extends Error {
constructor(public outputIndex: number) {
super(`There are multiple PayGo addresses in the PSBT output ${outputIndex}.`);
}
}
1 change: 1 addition & 0 deletions modules/utxo-core/src/paygo/psbt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './payGoAddressProof';
110 changes: 110 additions & 0 deletions modules/utxo-core/src/paygo/psbt/payGoAddressProof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as utxolib from '@bitgo/utxo-lib';
import { checkForOutput } from 'bip174/src/lib/utils';

import { verifyMessage } from '../../bip32utils';
import { createPayGoAttestationBuffer } from '../attestation';

import {
ErrorMultiplePayGoProof,
ErrorMultiplePayGoProofAtPsbtIndex,
ErrorNoPayGoProof,
ErrorOutputIndexOutOfBounds,
ErrorPayGoAddressProofFailedVerification,
} from './Errors';

/** This function adds the entropy and signature into the PSBT output unknown key vals.
* We store the entropy so that we reconstruct the message <ENTROPY><ADDRESS><UUID>
* to later verify.
*
* @param psbt - PSBT that we need to encode our paygo address into
* @param outputIndex - the index of the address in our output
* @param sig - the signature that we want to encode
*/
export function addPayGoAddressProof(
psbt: utxolib.bitgo.UtxoPsbt,
outputIndex: number,
sig: Buffer,
entropy: Buffer
): void {
utxolib.bitgo.addProprietaryKeyValuesFromUnknownKeyValues(psbt, 'output', outputIndex, {
key: {
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
keydata: entropy,
},
value: sig,
});
}

/** Verify the paygo address signature is valid using verification pub key.
*
* @param psbt - PSBT we want to verify that the paygo address is in
* @param outputIndex - we have the output index that address is in
* @param uuid
* @returns
*/
export function verifyPayGoAddressProof(
psbt: utxolib.bitgo.UtxoPsbt,
outputIndex: number,
verificationPubkey: Buffer
): void {
const psbtOutputs = checkForOutput(psbt.data.outputs, outputIndex);
const stored = utxolib.bitgo.getProprietaryKeyValuesFromUnknownKeyValues(psbtOutputs, {
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
});

// assert stored length is 0 or 1
if (stored.length === 0) {
throw new ErrorNoPayGoProof(outputIndex);
} else if (stored.length > 1) {
throw new ErrorMultiplePayGoProof();
}

// We get the signature and entropy from our PSBT unknown key vals
const signature = stored[0].value;
const entropy = stored[0].key.keydata;

// Get the the PayGo address from the txOutputs
const txOutputs = psbt.txOutputs;
if (outputIndex >= txOutputs.length) {
throw new ErrorOutputIndexOutOfBounds(outputIndex);
}
const output = txOutputs[outputIndex];
const addressFromOutput = utxolib.address.fromOutputScript(output.script, psbt.network);

// We construct our message <ENTROPY><ADDRESS><UUID>
const message = createPayGoAttestationBuffer(addressFromOutput, entropy);

if (!verifyMessage(message.toString(), verificationPubkey, signature, utxolib.networks.bitcoin)) {
throw new ErrorPayGoAddressProofFailedVerification();
}
}

/** Get the output index of the paygo output if there is one. It does this by
* checking if the metadata is on one of the outputs of the PSBT. If there is
* no paygo output, return undefined
*
* @param psbt
* @returns number - the index of the output address
*/
export function getPayGoAddressProofOutputIndex(psbt: utxolib.bitgo.UtxoPsbt): number | undefined {
const res = psbt.data.outputs.flatMap((output, outputIndex) => {
const proprietaryKeyVals = utxolib.bitgo.getPsbtOutputProprietaryKeyVals(output, {
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
});

if (proprietaryKeyVals.length > 1) {
throw new ErrorMultiplePayGoProofAtPsbtIndex(outputIndex);
}

return proprietaryKeyVals.length === 0 ? [] : [outputIndex];
});

return res.length === 0 ? undefined : res[0];
}

export function psbtOutputIncludesPaygoAddressProof(psbt: utxolib.bitgo.UtxoPsbt): boolean {
return getPayGoAddressProofOutputIndex(psbt) !== undefined;
}
1 change: 1 addition & 0 deletions modules/utxo-core/src/testutil/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './fixtures.utils';
export * from './key.utils';
export * from './toPlainObject.utils';
export * from './generatePayGoAttestationProof.utils';
export * from './parseVaspProof';
25 changes: 25 additions & 0 deletions modules/utxo-core/src/testutil/parseVaspProof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as utxolib from '@bitgo/utxo-lib';

/** We receive a proof in the form:
* 0x18Bitcoin Signed Message:\n<varint_length><ENTROPY><ADDRESS><UUID>
* and when verifying our message in our PayGo utils we want to only verify
* the message portion of our proof. This helps to pare our proof in that format,
* and returns a Buffer.
*
* @param proof
* @returns
*/
export function parseVaspProof(proof: Buffer): Buffer {
const prefix = '\u0018Bitcoin Signed Message:\n';
if (proof.toString().startsWith(prefix)) {
proof = proof.slice(Buffer.from(prefix).length);
utxolib.bufferutils.varuint.decode(proof, 0);
// Determines how many bytes were consumed during our last varuint.decode(Buffer, offset)
// So if varuint.decode(0xfd) then varuint.decode.bytes = 3
// varuint.decode(0xfe) then varuint.decode.bytes = 5, etc.
const varintBytesLength = utxolib.bufferutils.varuint.decode.bytes;

proof.slice(varintBytesLength);
}
return proof;
}
151 changes: 151 additions & 0 deletions modules/utxo-core/test/paygo/psbt/payGoAddressProof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import assert from 'assert';
import crypto from 'crypto';

import * as utxolib from '@bitgo/utxo-lib';
import { decodeProprietaryKey } from 'bip174/src/lib/proprietaryKeyVal';
import { KeyValue } from 'bip174/src/lib/interfaces';
import { checkForOutput } from 'bip174/src/lib/utils';

import {
addPayGoAddressProof,
getPayGoAddressProofOutputIndex,
psbtOutputIncludesPaygoAddressProof,
verifyPayGoAddressProof,
} from '../../../src/paygo/psbt/payGoAddressProof';
import { generatePayGoAttestationProof } from '../../../src/testutil/generatePayGoAttestationProof.utils';
import { parseVaspProof } from '../../../src/testutil/parseVaspProof';
import { signMessage } from '../../../src/bip32utils';
import { createPayGoAttestationBuffer, NILL_UUID } from '../../../src/paygo/attestation';

// To construct our PSBTs
const network = utxolib.networks.bitcoin;
const keys = [1, 2, 3].map((v) => utxolib.bip32.fromSeed(Buffer.alloc(16, `test/2/${v}`), network));
const rootWalletKeys = new utxolib.bitgo.RootWalletKeys([keys[0], keys[1], keys[2]]);

// PSBT INPUTS AND OUTPUTS
const psbtInputs = utxolib.testutil.inputScriptTypes.map((scriptType) => ({
scriptType,
value: BigInt(1000),
}));
const psbtOutputs = utxolib.testutil.outputScriptTypes.map((scriptType) => ({
scriptType,
value: BigInt(900),
}));

// wallet pub and priv key for tbtc
const dummyPub1 = rootWalletKeys.deriveForChainAndIndex(50, 200);
const attestationPubKey = dummyPub1.user.publicKey;
const attestationPrvKey = dummyPub1.user.privateKey!;

// our xpub converted to base58 address
const addressToVerify = utxolib.address.toBase58Check(
utxolib.crypto.hash160(Buffer.from(dummyPub1.backup.publicKey)),
utxolib.networks.bitcoin.pubKeyHash,
utxolib.networks.bitcoin
);

// this should be retuning a Buffer
const addressProofBuffer = generatePayGoAttestationProof(NILL_UUID, Buffer.from(addressToVerify));
const addressProofMsgBuffer = parseVaspProof(addressProofBuffer);
// We know that that the entropy is a set 64 bytes.
const addressProofEntropy = addressProofMsgBuffer.subarray(0, 65);

// signature with the given msg addressProofBuffer
const sig = signMessage(addressProofMsgBuffer.toString(), attestationPrvKey!, network);

function getTestPsbt() {
return utxolib.testutil.constructPsbt(psbtInputs, psbtOutputs, network, rootWalletKeys, 'unsigned');
}

describe('addPaygoAddressProof and verifyPaygoAddressProof', () => {
function getPaygoProprietaryKey(proprietaryKeyVals: KeyValue[]) {
return proprietaryKeyVals
.map(({ key, value }) => {
return { key: decodeProprietaryKey(key), value };
})
.filter((keyValue) => {
return (
keyValue.key.identifier === utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER &&
keyValue.key.subtype === utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF
);
});
}

it('should add and verify a valid paygo address proof on the PSBT', () => {
const psbt = getTestPsbt();
psbt.addOutput({ script: utxolib.address.toOutputScript(addressToVerify, network), value: BigInt(10000) });
const outputIndex = psbt.data.outputs.length - 1;
addPayGoAddressProof(psbt, outputIndex, sig, addressProofEntropy);
verifyPayGoAddressProof(psbt, outputIndex, attestationPubKey);
});

it('should throw an error if there are multiple PayGo proprietary keys in the PSBT', () => {
const outputIndex = 0;
const psbt = getTestPsbt();
addPayGoAddressProof(psbt, outputIndex, sig, addressProofEntropy);
addPayGoAddressProof(psbt, outputIndex, Buffer.from('signature2'), crypto.randomBytes(64));
const output = checkForOutput(psbt.data.outputs, outputIndex);
const proofInPsbt = getPaygoProprietaryKey(output.unknownKeyVals!);
assert(proofInPsbt.length !== 0);
assert(proofInPsbt.length > 1);
assert.throws(
() => verifyPayGoAddressProof(psbt, outputIndex, attestationPubKey),
(e: any) => e.message === 'There are multiple paygo address proofs encoded in the PSBT. Something went wrong.'
);
});
});

describe('verifyPaygoAddressProof', () => {
it('should throw an error if there is no PayGo address in PSBT', () => {
const psbt = getTestPsbt();
assert.throws(
() => verifyPayGoAddressProof(psbt, 0, attestationPubKey),
(e: any) => e.message === 'There is no paygo address proof encoded in the PSBT at output 0.'
);
});
});

describe('getPaygoAddressProofIndex', () => {
it('should get PayGo address proof index from PSBT if there is one', () => {
const psbt = getTestPsbt();
const outputIndex = 0;
addPayGoAddressProof(psbt, outputIndex, sig, Buffer.from(attestationPubKey));
assert(psbtOutputIncludesPaygoAddressProof(psbt));
assert(getPayGoAddressProofOutputIndex(psbt) === 0);
});

it('should return undefined if there is no PayGo address proof in PSBT', () => {
const psbt = getTestPsbt();
assert(getPayGoAddressProofOutputIndex(psbt) === undefined);
assert(!psbtOutputIncludesPaygoAddressProof(psbt));
});

it('should return an error and fail if we have multiple PayGo address in the PSBT in the same output index', () => {
const psbt = getTestPsbt();
const outputIndex = 0;
addPayGoAddressProof(psbt, outputIndex, sig, addressProofEntropy);
addPayGoAddressProof(psbt, outputIndex, sig, crypto.randomBytes(64));
assert.throws(
() => getPayGoAddressProofOutputIndex(psbt),
(e: any) => e.message === 'There are multiple PayGo addresses in the PSBT output 0.'
);
});
});

describe('createPayGoAttestationBuffer', () => {
it('should create a PayGo Attestation proof matching with original proof', () => {
const payGoAttestationProof = createPayGoAttestationBuffer(addressToVerify, addressProofEntropy);
assert.strictEqual(payGoAttestationProof.toString(), addressProofMsgBuffer.toString());
assert(Buffer.compare(payGoAttestationProof, addressProofMsgBuffer) === 0);
});

it('should create a PayGo Attestation proof that does not match with different uuid', () => {
const addressProofBufferDiffUuid = generatePayGoAttestationProof(
'00000000-0000-0000-0000-000000000001',
Buffer.from(addressToVerify)
);
const payGoAttestationProof = createPayGoAttestationBuffer(addressToVerify, addressProofEntropy);
assert.notStrictEqual(payGoAttestationProof.toString(), addressProofBufferDiffUuid.toString());
assert(Buffer.compare(payGoAttestationProof, addressProofBufferDiffUuid) !== 0);
});
});
1 change: 1 addition & 0 deletions modules/utxo-lib/src/bitgo/PsbtUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum ProprietaryKeySubtype {
MUSIG2_PARTICIPANT_PUB_KEYS = 0x01,
MUSIG2_PUB_NONCE = 0x02,
MUSIG2_PARTIAL_SIG = 0x03,
PAYGO_ADDRESS_ATTESTATION_PROOF = 0x04,
}

/**
Expand Down
4 changes: 2 additions & 2 deletions modules/utxo-lib/src/bitgo/zcash/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function toBase58Check(hash: Buffer, version: number): string {
}

export function fromOutputScript(outputScript: Buffer, network: Network): string {
assert(isZcash(network));
assert.ok(isZcash(network));
let o;
let prefix;
try {
Expand All @@ -41,7 +41,7 @@ export function fromOutputScript(outputScript: Buffer, network: Network): string
}

export function toOutputScript(address: string, network: Network): Buffer {
assert(isZcash(network));
assert.ok(isZcash(network));
const { version, hash } = fromBase58Check(address);
if (version === network.pubKeyHash) {
return payments.p2pkh({ hash }).output as Buffer;
Expand Down