Skip to content
Merged
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
62 changes: 62 additions & 0 deletions modules/abstract-utxo/src/transaction/bip322.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { decodeOrElse } from '@bitgo/sdk-core';
import { bip322 } from '@bitgo/utxo-core';
import { bitgo, networks, Network } from '@bitgo/utxo-lib';
import * as t from 'io-ts';

const BIP322MessageInfo = t.type({
Expand Down Expand Up @@ -33,3 +35,63 @@ export function deserializeBIP322BroadcastableMessage(hex: string): BIP322Messag
throw new Error(`Failed to decode ${BIP322MessageBroadcastable.name}: ${error}`);
});
}

export function verifyTransactionFromBroadcastableMessage(
message: BIP322MessageBroadcastable,
coinName: string
): boolean {
let network: Network = networks.bitcoin;
if (coinName === 'tbtc4') {
network = networks.bitcoinTestnet4;
} else if (coinName !== 'btc') {
throw new Error('Only tbtc4 or btc coinNames are supported.');
}
if (bitgo.isPsbt(message.txHex)) {
const psbt = bitgo.createPsbtFromBuffer(Buffer.from(message.txHex, 'hex'), network);
try {
bip322.assertBip322PsbtProof(psbt, message.messageInfo);
return true;
} catch (error) {
return false;
}
} else {
const tx = bitgo.createTransactionFromBuffer(Buffer.from(message.txHex, 'hex'), network, { amountType: 'bigint' });
try {
bip322.assertBip322TxProof(tx, message.messageInfo);
return true;
} catch (error) {
return false;
}
}
}

export function generateBIP322MessageListAndVerifyFromMessageBroadcastable(
messageBroadcastables: BIP322MessageBroadcastable[],
coinName: string
): { address: string; message: string }[] {
// Map from the address to the message. If there are duplicates of the address, make sure that the
// message is the same. If there are duplicate addresses and the messages are not the same, throw an error.
const addressMap = new Map<string, string>();

messageBroadcastables.forEach((message, index) => {
if (verifyTransactionFromBroadcastableMessage(message, coinName)) {
message.messageInfo.forEach((info) => {
const { address, message: msg } = info;
if (addressMap.has(address)) {
if (addressMap.get(address) !== msg) {
throw new Error(`Duplicate address ${address} has different messages`);
}
} else {
addressMap.set(address, msg);
}
});
} else {
throw new Error(`Message Broadcastable ${index} did not have a successful validation`);
}
});

return Array.from(addressMap.entries()).map(([address, message]) => ({
address,
message,
}));
}
1 change: 1 addition & 0 deletions modules/utxo-core/src/bip322/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './toSpend';
export * from './toSign';
export * from './utils';
export * from './verify';
7 changes: 5 additions & 2 deletions modules/utxo-core/src/bip322/toSign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ export const MAX_NUM_BIP322_INPUTS = 200;
* Create the base PSBT for the to_sign transaction for BIP322 signing.
* There will be ever 1 output.
*/
export function createBaseToSignPsbt(rootWalletKeys?: bitgo.RootWalletKeys): bitgo.UtxoPsbt {
export function createBaseToSignPsbt(
rootWalletKeys?: bitgo.RootWalletKeys,
network = networks.bitcoin
): bitgo.UtxoPsbt {
// Create PSBT object for constructing the transaction
const psbt = bitgo.createPsbtForNetwork({ network: networks.bitcoin });
const psbt = bitgo.createPsbtForNetwork({ network });
// Set default value for nVersion and nLockTime
psbt.setVersion(0); // nVersion = 0
psbt.setLocktime(0); // nLockTime = 0
Expand Down
140 changes: 140 additions & 0 deletions modules/utxo-core/src/bip322/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import * as assert from 'assert';

import * as utxolib from '@bitgo/utxo-lib';

import { buildToSpendTransaction } from './toSpend';

export type MessageInfo = {
address: string;
message: string;
// Hex encoded pubkeys
pubkeys: string[];
scriptType: utxolib.bitgo.outputScripts.ScriptType2Of3;
};

export function assertBaseTx(tx: utxolib.bitgo.UtxoTransaction<bigint>): void {
assert.deepStrictEqual(tx.version, 0, 'Transaction version must be 0.');
assert.deepStrictEqual(tx.locktime, 0, 'Transaction locktime must be 0.');
assert.deepStrictEqual(tx.outs.length, 1, 'Transaction must have exactly 1 output.');
assert.deepStrictEqual(tx.outs[0].value, BigInt(0), 'Transaction output value must be 0.');
assert.deepStrictEqual(tx.outs[0].script.toString('hex'), '6a', 'Transaction output script must be OP_RETURN.');
}

export function assertTxInput(
tx: utxolib.bitgo.UtxoTransaction<bigint>,
inputIndex: number,
prevOuts: utxolib.TxOutput<bigint>[],
info: MessageInfo,
checkSignature: boolean
): void {
assert.ok(
inputIndex < tx.ins.length,
`inputIndex ${inputIndex} is out of range for tx with ${tx.ins.length} inputs.`
);
const input = tx.ins[inputIndex];
assert.deepStrictEqual(input.index, 0, `transaction input ${inputIndex} must have index=0.`);
assert.deepStrictEqual(input.sequence, 0, `transaction input ${inputIndex} sequence must be 0.`);

// Make sure that the message is correctly encoded into the input of the transaction and
// verify that the message info corresponds
const scriptPubKey = utxolib.bitgo.outputScripts.createOutputScript2of3(
info.pubkeys.map((pubkey) => Buffer.from(pubkey, 'hex')),
info.scriptType,
tx.network
).scriptPubKey;
assert.deepStrictEqual(
info.address,
utxolib.address.fromOutputScript(scriptPubKey, tx.network).toString(),
`Address does not match derived scriptPubKey for input ${inputIndex}.`
);

const txid = utxolib.bitgo.getOutputIdForInput(input).txid;
const toSpendTx = buildToSpendTransaction(scriptPubKey, info.message);
assert.deepStrictEqual(
txid,
toSpendTx.getId(),
`Input ${inputIndex} derived to_spend transaction is not encoded in the input.`
);

if (checkSignature) {
const signatureScript = utxolib.bitgo.parseSignatureScript2Of3(input);
const scriptType =
signatureScript.scriptType === 'taprootKeyPathSpend'
? 'p2trMusig2'
: signatureScript.scriptType === 'taprootScriptPathSpend'
? 'p2tr'
: signatureScript.scriptType;
assert.deepStrictEqual(scriptType, info.scriptType, 'Script type does not match.');
utxolib.bitgo.verifySignatureWithPublicKeys(
tx,
inputIndex,
prevOuts,
info.pubkeys.map((pubkey) => Buffer.from(pubkey, 'hex'))
);
}
}

export function assertBip322TxProof(tx: utxolib.bitgo.UtxoTransaction<bigint>, messageInfo: MessageInfo[]): void {
assertBaseTx(tx);
assert.deepStrictEqual(
tx.ins.length,
messageInfo.length,
'Transaction must have the same number of inputs as messageInfo entries.'
);
const prevOuts = messageInfo.map((info) => {
return {
value: 0n,
script: utxolib.bitgo.outputScripts.createOutputScript2of3(
info.pubkeys.map((pubkey) => Buffer.from(pubkey, 'hex')),
info.scriptType,
tx.network
).scriptPubKey,
};
});
tx.ins.forEach((input, inputIndex) => assertTxInput(tx, inputIndex, prevOuts, messageInfo[inputIndex], true));
}

export function assertBip322PsbtProof(psbt: utxolib.bitgo.UtxoPsbt, messageInfo: MessageInfo[]): void {
const unsignedTx = psbt.getUnsignedTx();

assertBaseTx(unsignedTx);
assert.deepStrictEqual(
psbt.data.inputs.length,
messageInfo.length,
'PSBT must have the same number of inputs as messageInfo entries.'
);

const prevOuts = psbt.data.inputs.map((input, inputIndex) => {
assert.ok(input.witnessUtxo, `PSBT input ${inputIndex} is missing witnessUtxo`);
return input.witnessUtxo;
});

psbt.data.inputs.forEach((input, inputIndex) => {
// Check that the metadata in the PSBT matches the messageInfo, then check the input data
const info = messageInfo[inputIndex];

// Check that the to_spend transaction is encoded in the nonWitnessUtxo
assert.ok(input.nonWitnessUtxo, `PSBT input ${inputIndex} is missing nonWitnessUtxo`);
const toSpendTx = buildToSpendTransaction(prevOuts[inputIndex].script, info.message);
assert.deepStrictEqual(input.nonWitnessUtxo.toString('hex'), toSpendTx.toHex());

if (input.bip32Derivation) {
input.bip32Derivation.forEach((b) => {
const pubkey = b.pubkey.toString('hex');
assert.ok(
info.pubkeys.includes(pubkey),
`PSBT input ${inputIndex} has a pubkey in (tap)bip32Derivation that is not in messageInfo`
);
});
} else if (!input.tapBip32Derivation) {
throw new Error(`PSBT input ${inputIndex} is missing (tap)bip32Derivation when it should have it.`);
}

// Verify the signature on the input
assert.ok(psbt.validateSignaturesOfInputCommon(inputIndex), `PSBT input ${inputIndex} has an invalid signature.`);

// Do not check the signature when using the PSBT, the signature is not there. We are going
// to signatures in the PSBT.
assertTxInput(unsignedTx, inputIndex, prevOuts, info, false);
});
}
Loading