Skip to content

Commit

Permalink
feat: add unit tests for bitcoin functions related to por calculations
Browse files Browse the repository at this point in the history
  • Loading branch information
Polybius93 committed Jul 11, 2024
1 parent f06efd1 commit 228b66b
Show file tree
Hide file tree
Showing 5 changed files with 339 additions and 12 deletions.
1 change: 0 additions & 1 deletion src/functions/bitcoin/bitcoin-request-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export async function fetchBitcoinTransaction(
bitcoinBlockchainAPI: string
): Promise<BitcoinTransaction> {
try {
console.log('txID: ', txID);
const bitcoinBlockchainAPITransactionEndpoint = `${bitcoinBlockchainAPI}/tx/${txID}`;

const response = await fetch(bitcoinBlockchainAPITransactionEndpoint);
Expand Down
192 changes: 192 additions & 0 deletions tests/mocks/bitcoin.test.constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,194 @@
import { BitcoinTransaction } from '../../src/models/bitcoin-models';

export const TEST_TAPROOT_UNHARDENED_DERIVED_PUBLIC_KEY_1 =
'dc544c17af0887dfc8ca9936755c9fdef0c79bbc8866cd69bf120c71509742d2';

export const TEST_TAPROOT_MULTISIG_PAYMENT_SCRIPT_1 =
'51206d7e5019c795d05fd3df81713069aa3a309e912a61555ab3ebd6e477f42c1f70';

export const TEST_BITCOIN_BLOCKCHAIN_BLOCK_HEIGHT_1 = 2867441;
export const TEST_BITCOIN_BLOCKCHAIN_BLOCK_HEIGHT_2 = 2867285;

export const TEST_UNSPENDABLE_KEY_COMMITED_TO_UUID_1 =
'tpubD6NzVbkrYhZ4Wmm9QfhLLpfzQRoJApR3Sf4AgkiyLMgxbhPtBLVmqA2ZG7zKTgYzzCCK7bAoS5UEXotNdAnhhQhUhB1Q1uqFF1BLVCkArmr';

export const TEST_UNHARDENED_DERIVED_UNSPENDABLE_KEY_COMMITED_TO_UUID_1 =
'02b733c776dd7776657c20a58f1f009567afc75db226965bce83d5d0afc29e46c9';

// This is a testnet funding transaction with valid inputs and outputs
export const TEST_TESTNET_FUNDING_TRANSACTION_1: BitcoinTransaction = {
txid: '4cf5c2954c84bf5225d98ef014aa97bbfa0f05d56b5749782fcd8af8b9d505a5',
version: 2,
locktime: 0,
vin: [
{
txid: 'cefbeafc3e50618a59646ba6e7b3bba8f15b3e2551570af98182f4234586d085',
vout: 2,
prevout: {
scriptpubkey: '5120192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_asm:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_type: 'v1_p2tr',
scriptpubkey_address: 'tb1prykktsems67p98tqdsf0qxp4d82zwvk4njknhusg4x5l6wcnsfyqar32mq',
value: 71607616,
},
scriptsig: '',
scriptsig_asm: '',
witness: [
'd4ad3523fdc9ec709e8bf2ecadd56c9266f9c57bccb5d165cd57dc815a88de34957764482a6fab3897ce7be2677168f69be93d799021b502899b556436c3f6bb',
],
is_coinbase: false,
sequence: 4294967280,
},
],
vout: [
{
scriptpubkey: '51206d7e5019c795d05fd3df81713069aa3a309e912a61555ab3ebd6e477f42c1f70',
scriptpubkey_asm:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 6d7e5019c795d05fd3df81713069aa3a309e912a61555ab3ebd6e477f42c1f70',
scriptpubkey_type: 'v1_p2tr',
scriptpubkey_address: 'tb1pd4l9qxw8jhg9l57ls9cnq6d28gcfayf2v9244vlt6mj80apvracqgdt090',
value: 10000000,
},
{
scriptpubkey: '0014f28ec1a3e3df0240b98582ca7754e6948e9bf930',
scriptpubkey_asm: 'OP_0 OP_PUSHBYTES_20 f28ec1a3e3df0240b98582ca7754e6948e9bf930',
scriptpubkey_type: 'v0_p2wpkh',
scriptpubkey_address: 'tb1q728vrglrmupypwv9st98w48xjj8fh7fs8mrdre',
value: 100000,
},
{
scriptpubkey: '5120192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_asm:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_type: 'v1_p2tr',
scriptpubkey_address: 'tb1prykktsems67p98tqdsf0qxp4d82zwvk4njknhusg4x5l6wcnsfyqar32mq',
value: 61490226,
},
],
size: 236,
weight: 740,
fee: 17390,
status: {
confirmed: true,
block_height: 2867279,
block_hash: '000000000000001ee12e0297ff36e8c8041aefb65af0c1033a1af4fdb8146f0d',
block_time: 1720620175,
},
};

// This transaction is missing the output with the multisig's script.
export const TEST_TESTNET_FUNDING_TRANSACTION_2: BitcoinTransaction = {
txid: '4cf5c2954c84bf5225d98ef014aa97bbfa0f05d56b5749782fcd8af8b9d505a5',
version: 2,
locktime: 0,
vin: [
{
txid: 'cefbeafc3e50618a59646ba6e7b3bba8f15b3e2551570af98182f4234586d085',
vout: 2,
prevout: {
scriptpubkey: '5120192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_asm:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_type: 'v1_p2tr',
scriptpubkey_address: 'tb1prykktsems67p98tqdsf0qxp4d82zwvk4njknhusg4x5l6wcnsfyqar32mq',
value: 71607616,
},
scriptsig: '',
scriptsig_asm: '',
witness: [
'd4ad3523fdc9ec709e8bf2ecadd56c9266f9c57bccb5d165cd57dc815a88de34957764482a6fab3897ce7be2677168f69be93d799021b502899b556436c3f6bb',
],
is_coinbase: false,
sequence: 4294967280,
},
],
vout: [
{
scriptpubkey: '0014f28ec1a3e3df0240b98582ca7754e6948e9bf930',
scriptpubkey_asm: 'OP_0 OP_PUSHBYTES_20 f28ec1a3e3df0240b98582ca7754e6948e9bf930',
scriptpubkey_type: 'v0_p2wpkh',
scriptpubkey_address: 'tb1q728vrglrmupypwv9st98w48xjj8fh7fs8mrdre',
value: 100000,
},
{
scriptpubkey: '5120192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_asm:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_type: 'v1_p2tr',
scriptpubkey_address: 'tb1prykktsems67p98tqdsf0qxp4d82zwvk4njknhusg4x5l6wcnsfyqar32mq',
value: 61490226,
},
],
size: 236,
weight: 740,
fee: 17390,
status: {
confirmed: true,
block_height: 2867279,
block_hash: '000000000000001ee12e0297ff36e8c8041aefb65af0c1033a1af4fdb8146f0d',
block_time: 1720620175,
},
};

// This transaction's multisig output value does not match the vault's valueLocked field.
export const TEST_TESTNET_FUNDING_TRANSACTION_3: BitcoinTransaction = {
txid: '4cf5c2954c84bf5225d98ef014aa97bbfa0f05d56b5749782fcd8af8b9d505a5',
version: 2,
locktime: 0,
vin: [
{
txid: 'cefbeafc3e50618a59646ba6e7b3bba8f15b3e2551570af98182f4234586d085',
vout: 2,
prevout: {
scriptpubkey: '5120192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_asm:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_type: 'v1_p2tr',
scriptpubkey_address: 'tb1prykktsems67p98tqdsf0qxp4d82zwvk4njknhusg4x5l6wcnsfyqar32mq',
value: 71607616,
},
scriptsig: '',
scriptsig_asm: '',
witness: [
'd4ad3523fdc9ec709e8bf2ecadd56c9266f9c57bccb5d165cd57dc815a88de34957764482a6fab3897ce7be2677168f69be93d799021b502899b556436c3f6bb',
],
is_coinbase: false,
sequence: 4294967280,
},
],
vout: [
{
scriptpubkey: '51206d7e5019c795d05fd3df81713069aa3a309e912a61555ab3ebd6e477f42c1f70',
scriptpubkey_asm:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 6d7e5019c795d05fd3df81713069aa3a309e912a61555ab3ebd6e477f42c1f70',
scriptpubkey_type: 'v1_p2tr',
scriptpubkey_address: 'tb1pd4l9qxw8jhg9l57ls9cnq6d28gcfayf2v9244vlt6mj80apvracqgdt090',
value: 5000000,
},
{
scriptpubkey: '0014f28ec1a3e3df0240b98582ca7754e6948e9bf930',
scriptpubkey_asm: 'OP_0 OP_PUSHBYTES_20 f28ec1a3e3df0240b98582ca7754e6948e9bf930',
scriptpubkey_type: 'v0_p2wpkh',
scriptpubkey_address: 'tb1q728vrglrmupypwv9st98w48xjj8fh7fs8mrdre',
value: 100000,
},
{
scriptpubkey: '5120192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_asm:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_type: 'v1_p2tr',
scriptpubkey_address: 'tb1prykktsems67p98tqdsf0qxp4d82zwvk4njknhusg4x5l6wcnsfyqar32mq',
value: 61490226,
},
],
size: 236,
weight: 740,
fee: 17390,
status: {
confirmed: true,
block_height: 2867279,
block_hash: '000000000000001ee12e0297ff36e8c8041aefb65af0c1033a1af4fdb8146f0d',
block_time: 1720620175,
},
};
2 changes: 2 additions & 0 deletions tests/mocks/ethereum.test.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const TEST_VAULT_UUID_1 =
'0x2b898d65df757575417a920aabe518586793bac4fa682f00ad2c33fad2471999';
77 changes: 77 additions & 0 deletions tests/unit/bitcoin-functions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
import { testnet } from 'bitcoinjs-lib/src/networks';

import {
createTaprootMultisigPayment,
deriveUnhardenedPublicKey,
getScriptMatchingOutputFromTransaction,
getUnspendableKeyCommittedToUUID,
} from '../../src/functions/bitcoin/bitcoin-functions';
import {
TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1,
TEST_TESTNET_ATTESTOR_UNHARDENED_DERIVED_PUBLIC_KEY_1,
} from '../mocks/attestor.test.constants';
import {
TEST_TAPROOT_MULTISIG_PAYMENT_SCRIPT_1,
TEST_TAPROOT_UNHARDENED_DERIVED_PUBLIC_KEY_1,
TEST_TESTNET_FUNDING_TRANSACTION_1,
TEST_TESTNET_FUNDING_TRANSACTION_2,
TEST_UNHARDENED_DERIVED_UNSPENDABLE_KEY_COMMITED_TO_UUID_1,
TEST_UNSPENDABLE_KEY_COMMITED_TO_UUID_1,
} from '../mocks/bitcoin.test.constants';
import { TEST_VAULT_UUID_1 } from '../mocks/ethereum.test.constants';

describe('Bitcoin Functions', () => {
describe('getUnspendableKeyCommittedToUUID', () => {
it('should return an unspendable key committed to the given uuid', () => {
const result = getUnspendableKeyCommittedToUUID(TEST_VAULT_UUID_1, testnet);

expect(result).toBe(TEST_UNSPENDABLE_KEY_COMMITED_TO_UUID_1);
});
});

describe('deriveUnhardenedPublicKey', () => {
it('should derive an unhardened public key from a given public key', () => {
const result = deriveUnhardenedPublicKey(
TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1,
testnet
);

expect(result.toString('hex')).toBe(TEST_TESTNET_ATTESTOR_UNHARDENED_DERIVED_PUBLIC_KEY_1);
});
});

describe('createTaprootMultisigPayment', () => {
it('should create a taproot multisig payment', () => {
const result = createTaprootMultisigPayment(
Buffer.from(TEST_UNHARDENED_DERIVED_UNSPENDABLE_KEY_COMMITED_TO_UUID_1, 'hex'),
Buffer.from(TEST_TESTNET_ATTESTOR_UNHARDENED_DERIVED_PUBLIC_KEY_1, 'hex'),
Buffer.from(TEST_TAPROOT_UNHARDENED_DERIVED_PUBLIC_KEY_1, 'hex'),
testnet
);

expect(bytesToHex(result.script)).toBe(TEST_TAPROOT_MULTISIG_PAYMENT_SCRIPT_1);
});
});

describe('getScriptMatchingOutputFromTransaction', () => {
it('should get the script matching output from a transaction', () => {
const result = getScriptMatchingOutputFromTransaction(
TEST_TESTNET_FUNDING_TRANSACTION_1,
hexToBytes(TEST_TAPROOT_MULTISIG_PAYMENT_SCRIPT_1)
);

expect(result).toBeDefined();
expect(result).toBe(TEST_TESTNET_FUNDING_TRANSACTION_1.vout[0]);
});

it('should return undefined for a transaction without any output linked to the multisig script', () => {
const result = getScriptMatchingOutputFromTransaction(
TEST_TESTNET_FUNDING_TRANSACTION_2,
hexToBytes(TEST_TAPROOT_MULTISIG_PAYMENT_SCRIPT_1)
);

expect(result).toBeUndefined();
});
});
});
79 changes: 68 additions & 11 deletions tests/unit/proof-of-reserve.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import { testnet } from 'bitcoinjs-lib/src/networks.js';

import * as bitcoinRequestFunctions from '../../src/functions/bitcoin/bitcoin-request-functions.js';
import { verifyVaultDeposit } from '../../src/functions/proof-of-reserve/proof-of-reserve-functions.js';
import { TEST_TESTNET_BITCOIN_BLOCKCHAIN_API } from '../mocks/api.test.constants.js';
import { TEST_TESTNET_ATTESTOR_UNHARDENED_DERIVED_PUBLIC_KEY_1 } from '../mocks/attestor.test.constants.js';
import {
TEST_BITCOIN_BLOCKCHAIN_BLOCK_HEIGHT_1,
TEST_BITCOIN_BLOCKCHAIN_BLOCK_HEIGHT_2,
TEST_TESTNET_FUNDING_TRANSACTION_1,
TEST_TESTNET_FUNDING_TRANSACTION_2,
TEST_TESTNET_FUNDING_TRANSACTION_3,
} from '../mocks/bitcoin.test.constants.js';
import { TEST_TESTNET_FUNDING_TRANSACTION, TEST_VAULT_2 } from '../mocks/constants';

jest.mock('../../src/functions/bitcoin/bitcoin-request-functions.js', () => {
const actual = jest.requireActual('../../src/functions/bitcoin/bitcoin-request-functions.js');
return {
...actual,
fetchBitcoinTransaction: () => TEST_TESTNET_FUNDING_TRANSACTION,
};
});
import { TEST_VAULT_2 } from '../mocks/constants';

describe('Proof of Reserve Calculation', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('verifyVaultDeposit', () => {
it("should return true when the vault's funding transaction is confirmed, contains an output with the multisig's script, and the output's value matches the vault's valueLocked field", async () => {
xit("should return true when the vault's funding transaction is confirmed, contains an output with the multisig's script, and the output's value matches the vault's valueLocked field", async () => {
jest
.spyOn(bitcoinRequestFunctions, 'fetchBitcoinTransaction')
.mockImplementationOnce(async () => TEST_TESTNET_FUNDING_TRANSACTION_1);

const result = await verifyVaultDeposit(
TEST_VAULT_2,
Buffer.from(TEST_TESTNET_ATTESTOR_UNHARDENED_DERIVED_PUBLIC_KEY_1, 'hex'),
Expand All @@ -31,7 +34,29 @@ describe('Proof of Reserve Calculation', () => {
expect(result).toBe(true);
});

it("should return false when the vault's funding transaction is not yet confirmed", async () => {
xit('should return false if the funding transaction is not found', async () => {
jest
.spyOn(bitcoinRequestFunctions, 'fetchBitcoinTransaction')
.mockImplementationOnce(async () => {
throw new Error('Transaction not found');
});

const result = await verifyVaultDeposit(
TEST_VAULT_2,
Buffer.from(TEST_TESTNET_ATTESTOR_UNHARDENED_DERIVED_PUBLIC_KEY_1, 'hex'),
TEST_BITCOIN_BLOCKCHAIN_BLOCK_HEIGHT_1,
TEST_TESTNET_BITCOIN_BLOCKCHAIN_API,
testnet
);

expect(result).toBe(false);
});

xit("should return false when the vault's funding transaction is not yet confirmed", async () => {
jest
.spyOn(bitcoinRequestFunctions, 'fetchBitcoinTransaction')
.mockImplementationOnce(async () => TEST_TESTNET_FUNDING_TRANSACTION_1);

const result = await verifyVaultDeposit(
TEST_VAULT_2,
Buffer.from(TEST_TESTNET_ATTESTOR_UNHARDENED_DERIVED_PUBLIC_KEY_1, 'hex'),
Expand All @@ -42,5 +67,37 @@ describe('Proof of Reserve Calculation', () => {

expect(result).toBe(false);
});

it("should return false if the vault's funding transaction lacks an output with the multisig's script", async () => {
jest
.spyOn(bitcoinRequestFunctions, 'fetchBitcoinTransaction')
.mockImplementationOnce(async () => TEST_TESTNET_FUNDING_TRANSACTION_2);

const result = await verifyVaultDeposit(
TEST_VAULT_2,
Buffer.from(TEST_TESTNET_ATTESTOR_UNHARDENED_DERIVED_PUBLIC_KEY_1, 'hex'),
TEST_BITCOIN_BLOCKCHAIN_BLOCK_HEIGHT_1,
TEST_TESTNET_BITCOIN_BLOCKCHAIN_API,
testnet
);

expect(result).toBe(false);
});

it("should return false if the output value related to the multisig script differs from the vault's valueLocked field in the funding transaction", async () => {
jest
.spyOn(bitcoinRequestFunctions, 'fetchBitcoinTransaction')
.mockImplementationOnce(async () => TEST_TESTNET_FUNDING_TRANSACTION_3);

const result = await verifyVaultDeposit(
TEST_VAULT_2,
Buffer.from(TEST_TESTNET_ATTESTOR_UNHARDENED_DERIVED_PUBLIC_KEY_1, 'hex'),
TEST_BITCOIN_BLOCKCHAIN_BLOCK_HEIGHT_1,
TEST_TESTNET_BITCOIN_BLOCKCHAIN_API,
testnet
);

expect(result).toBe(false);
});
});
});

0 comments on commit 228b66b

Please sign in to comment.