diff --git a/src/functions/bitcoin/bitcoin-functions.ts b/src/functions/bitcoin/bitcoin-functions.ts index 82ea715..7ab1347 100644 --- a/src/functions/bitcoin/bitcoin-functions.ts +++ b/src/functions/bitcoin/bitcoin-functions.ts @@ -1,3 +1,4 @@ +import { hexToBytes } from '@noble/hashes/utils'; import { Address, OutScript, @@ -428,6 +429,15 @@ export function getValueMatchingOutputFromTransaction( return valueMatchingTransactionOutput; } +export function getScriptMatchingOutputFromTransaction( + bitcoinTransaction: BitcoinTransaction, + script: Uint8Array +): BitcoinTransactionVectorOutput | undefined { + return bitcoinTransaction.vout.find(output => + validateScript(script, hexToBytes(output.scriptpubkey)) + ); +} + export function validateScript(script: Uint8Array, outputScript: Uint8Array): boolean { return ( outputScript.length === script.length && diff --git a/src/functions/bitcoin/bitcoin-request-functions.ts b/src/functions/bitcoin/bitcoin-request-functions.ts index 83f4240..a081587 100644 --- a/src/functions/bitcoin/bitcoin-request-functions.ts +++ b/src/functions/bitcoin/bitcoin-request-functions.ts @@ -12,6 +12,7 @@ export async function fetchBitcoinTransaction( bitcoinBlockchainAPI: string ): Promise { try { + console.log('txID: ', txID); const bitcoinBlockchainAPITransactionEndpoint = `${bitcoinBlockchainAPI}/tx/${txID}`; const response = await fetch(bitcoinBlockchainAPITransactionEndpoint); diff --git a/src/functions/proof-of-reserve/proof-of-reserve-functions.ts b/src/functions/proof-of-reserve/proof-of-reserve-functions.ts new file mode 100644 index 0000000..74404bf --- /dev/null +++ b/src/functions/proof-of-reserve/proof-of-reserve-functions.ts @@ -0,0 +1,67 @@ +import { Network } from 'bitcoinjs-lib'; +import { RawVault } from 'src/models/ethereum-models.js'; + +import { + createTaprootMultisigPayment, + deriveUnhardenedPublicKey, + getScriptMatchingOutputFromTransaction, + getUnspendableKeyCommittedToUUID, +} from '../bitcoin/bitcoin-functions.js'; +import { + checkBitcoinTransactionConfirmations, + fetchBitcoinTransaction, +} from '../bitcoin/bitcoin-request-functions.js'; + +export async function verifyVaultDeposit( + vault: RawVault, + attestorGroupPublicKey: Buffer, + bitcoinBlockchainBlockHeight: number, + bitcoinBlockchainAPI: string, + bitcoinNetwork: Network +): Promise { + try { + const fundingTransaction = await fetchBitcoinTransaction( + vault.fundingTxId, + bitcoinBlockchainAPI + ); + + const isFundingTransactionConfirmed = await checkBitcoinTransactionConfirmations( + fundingTransaction, + bitcoinBlockchainBlockHeight + ); + + if (!isFundingTransactionConfirmed) { + return false; + } + + const unspendableKeyCommittedToUUID = deriveUnhardenedPublicKey( + getUnspendableKeyCommittedToUUID(vault.uuid, bitcoinNetwork), + bitcoinNetwork + ); + + const taprootMultisigPayment = createTaprootMultisigPayment( + unspendableKeyCommittedToUUID, + attestorGroupPublicKey, + Buffer.from(vault.taprootPubKey, 'hex'), + bitcoinNetwork + ); + + const vaultTransactionOutput = getScriptMatchingOutputFromTransaction( + fundingTransaction, + taprootMultisigPayment.script + ); + + if (!vaultTransactionOutput) { + return false; + } + + if (vaultTransactionOutput.value !== vault.valueLocked.toNumber()) { + return false; + } + + return true; + } catch (error) { + console.log(`Error verifying Vault Deposit: ${error}`); + return false; + } +} diff --git a/src/proof-of-reserve-handlers/proof-of-reserve-handler.ts b/src/proof-of-reserve-handlers/proof-of-reserve-handler.ts index e401a7e..98b8e8c 100644 --- a/src/proof-of-reserve-handlers/proof-of-reserve-handler.ts +++ b/src/proof-of-reserve-handlers/proof-of-reserve-handler.ts @@ -1,18 +1,8 @@ -import { hex } from '@scure/base'; import { Network } from 'bitcoinjs-lib'; -import { - createTaprootMultisigPayment, - deriveUnhardenedPublicKey, - getUnspendableKeyCommittedToUUID, - getValueMatchingOutputFromTransaction, - validateScript, -} from '../functions/bitcoin/bitcoin-functions.js'; -import { - checkBitcoinTransactionConfirmations, - fetchBitcoinBlockchainBlockHeight, - fetchBitcoinTransaction, -} from '../functions/bitcoin/bitcoin-request-functions.js'; +import { deriveUnhardenedPublicKey } from '../functions/bitcoin/bitcoin-functions.js'; +import { fetchBitcoinBlockchainBlockHeight } from '../functions/bitcoin/bitcoin-request-functions.js'; +import { verifyVaultDeposit } from '../functions/proof-of-reserve/proof-of-reserve-functions.js'; import { RawVault } from '../models/ethereum-models.js'; export class ProofOfReserveHandler { @@ -30,51 +20,6 @@ export class ProofOfReserveHandler { this.attestorGroupPublicKey = attestorGroupPublicKey; } - async verifyVaultDeposit( - vault: RawVault, - attestorGroupPublicKey: Buffer, - bitcoinBlockchainBlockHeight: number - ): Promise { - try { - const fundingTransaction = await fetchBitcoinTransaction( - vault.fundingTxId, - this.bitcoinBlockchainAPI - ); - const isFundingTransactionConfirmed = await checkBitcoinTransactionConfirmations( - fundingTransaction, - bitcoinBlockchainBlockHeight - ); - - if (!isFundingTransactionConfirmed) { - return false; - } - - const vaultTransactionOutput = getValueMatchingOutputFromTransaction( - fundingTransaction, - vault.valueLocked.toNumber() - ); - - const unspendableKeyCommittedToUUID = deriveUnhardenedPublicKey( - getUnspendableKeyCommittedToUUID(vault.uuid, this.bitcoinNetwork), - this.bitcoinNetwork - ); - const taprootMultisigPayment = createTaprootMultisigPayment( - unspendableKeyCommittedToUUID, - attestorGroupPublicKey, - Buffer.from(vault.taprootPubKey, 'hex'), - this.bitcoinNetwork - ); - - return validateScript( - taprootMultisigPayment.script, - hex.decode(vaultTransactionOutput.scriptpubkey) - ); - } catch (error) { - console.error(`Error verifying Vault Deposit: ${error}`); - return false; - } - } - async calculateProofOfReserve(vaults: RawVault[]): Promise { const bitcoinBlockchainBlockHeight = await fetchBitcoinBlockchainBlockHeight( this.bitcoinBlockchainAPI @@ -84,12 +29,15 @@ export class ProofOfReserveHandler { this.attestorGroupPublicKey, this.bitcoinNetwork ); + const verifiedDeposits = await Promise.all( vaults.map(async vault => { - return (await this.verifyVaultDeposit( + return (await verifyVaultDeposit( vault, derivedAttestorGroupPublicKey, - bitcoinBlockchainBlockHeight + bitcoinBlockchainBlockHeight, + this.bitcoinBlockchainAPI, + this.bitcoinNetwork )) === true ? vault.valueLocked.toNumber() : 0; diff --git a/tests/mocks/api.test.constants.ts b/tests/mocks/api.test.constants.ts new file mode 100644 index 0000000..335b4c2 --- /dev/null +++ b/tests/mocks/api.test.constants.ts @@ -0,0 +1,2 @@ +export const TEST_REGTEST_BITCOIN_BLOCKCHAIN_API = 'https://devnet.dlc.link/electrs'; +export const TEST_TESTNET_BITCOIN_BLOCKCHAIN_API = 'https://testnet.dlc.link/electrs'; diff --git a/tests/mocks/attestor.test.constants.ts b/tests/mocks/attestor.test.constants.ts new file mode 100644 index 0000000..eadc1cb --- /dev/null +++ b/tests/mocks/attestor.test.constants.ts @@ -0,0 +1,8 @@ +export const TEST_REGTEST_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1 = + 'tpubDDqN2CmTDKaGeqXMayfCZEvjZqntifi4r1ztmRWsGuE1VE4bosR3mBKQwVaCxZcmg8R1nHDMDzDmzjoccBMgwZV1hhz51tAXVnhjABCQcwA'; + +export const TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1 = + 'tpubDDRekL64eJJav32TLhNhG59qra7wAMaei8YMGXNiJE8ksdYrKgvaFM1XG6JrSt31W97XryScrX37RUEujjZT4qScNf8Zu1JxWj4VYkwz4rU'; + +export const TEST_TESTNET_ATTESTOR_UNHARDENED_DERIVED_PUBLIC_KEY_1 = + '027eda4d625f781dcc98bf68901360fdaaacce8ed466096c1dfe4865209b28c058'; diff --git a/tests/mocks/bitcoin.test.constants.ts b/tests/mocks/bitcoin.test.constants.ts new file mode 100644 index 0000000..523258a --- /dev/null +++ b/tests/mocks/bitcoin.test.constants.ts @@ -0,0 +1,2 @@ +export const TEST_BITCOIN_BLOCKCHAIN_BLOCK_HEIGHT_1 = 2867441; +export const TEST_BITCOIN_BLOCKCHAIN_BLOCK_HEIGHT_2 = 2867285; diff --git a/tests/mocks/constants.ts b/tests/mocks/constants.ts index 9c3da76..6ff2193 100644 --- a/tests/mocks/constants.ts +++ b/tests/mocks/constants.ts @@ -1,6 +1,7 @@ import { regtest } from 'bitcoinjs-lib/src/networks.js'; import { BigNumber } from 'ethers'; +import { BitcoinTransaction } from '../../src/models/bitcoin-models.js'; import { RawVault } from '../../src/models/ethereum-models.js'; // Bitcoin @@ -46,5 +47,81 @@ export const TEST_REGTEST_ATTESTOR_APIS = [ 'https://devnet.dlc.link/attestor-2', 'https://devnet.dlc.link/attestor-3', ]; -export const TEST_REGTEST_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY = - 'tpubDDqN2CmTDKaGeqXMayfCZEvjZqntifi4r1ztmRWsGuE1VE4bosR3mBKQwVaCxZcmg8R1nHDMDzDmzjoccBMgwZV1hhz51tAXVnhjABCQcwA'; + +export const TEST_TESTNET_FUNDING_TRANSACTION: 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, + }, +}; + +export const TEST_VAULT_2: RawVault = { + uuid: '0x2b898d65df757575417a920aabe518586793bac4fa682f00ad2c33fad2471999', + protocolContract: '0x980feAeD0D5d3BaFFeb828a27e8b59c0FE78F1f9', + timestamp: BigNumber.from('0x668e9353'), + valueLocked: BigNumber.from('0x989680'), + valueMinted: BigNumber.from('0x989680'), + creator: '0x980feAeD0D5d3BaFFeb828a27e8b59c0FE78F1f9', + status: 1, + fundingTxId: '4cf5c2954c84bf5225d98ef014aa97bbfa0f05d56b5749782fcd8af8b9d505a5', + closingTxId: '', + wdTxId: '032392b61a5c3b0098774465ad61e429fd892615ff2890f849f8eb237a8a59f3ba', + btcFeeRecipient: '', + btcMintFeeBasisPoints: BigNumber.from('0x64'), + btcRedeemFeeBasisPoints: BigNumber.from('0x64'), + taprootPubKey: 'dc544c17af0887dfc8ca9936755c9fdef0c79bbc8866cd69bf120c71509742d2', +}; diff --git a/tests/unit/proof-of-reserve.test.ts b/tests/unit/proof-of-reserve.test.ts new file mode 100644 index 0000000..bc0b309 --- /dev/null +++ b/tests/unit/proof-of-reserve.test.ts @@ -0,0 +1,46 @@ +import { testnet } from 'bitcoinjs-lib/src/networks.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, +} 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, + }; +}); + +describe('Proof of Reserve Calculation', () => { + 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 () => { + 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(true); + }); + + it("should return false when the vault's funding transaction is not yet confirmed", async () => { + const result = await verifyVaultDeposit( + TEST_VAULT_2, + Buffer.from(TEST_TESTNET_ATTESTOR_UNHARDENED_DERIVED_PUBLIC_KEY_1, 'hex'), + TEST_BITCOIN_BLOCKCHAIN_BLOCK_HEIGHT_2, + TEST_TESTNET_BITCOIN_BLOCKCHAIN_API, + testnet + ); + + expect(result).toBe(false); + }); + }); +}); diff --git a/tests/unit/sign-transactions.test.ts b/tests/unit/sign-transactions.test.ts index 4efcf41..5bf15cc 100644 --- a/tests/unit/sign-transactions.test.ts +++ b/tests/unit/sign-transactions.test.ts @@ -1,7 +1,10 @@ import { Transaction, p2wpkh } from '@scure/btc-signer'; +import { testnet } from 'bitcoinjs-lib/src/networks.js'; +import { deriveUnhardenedPublicKey } from '../../src/functions/bitcoin/bitcoin-functions.js'; import { PrivateKeyDLCHandler } from '../../src/index.js'; import { shiftValue } from '../../src/utilities/index.js'; +import { TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1 } from '../mocks/attestor.test.constants.js'; import { TEST_BITCOIN_AMOUNT, TEST_BITCOIN_BLOCKCHAIN_API, @@ -10,7 +13,6 @@ import { TEST_BITCOIN_NETWORK, TEST_BITCOIN_WALLET_ACCOUNT_INDEX, TEST_FUNDING_PAYMENT_TYPE, - TEST_REGTEST_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY, TEST_VAULT, } from '../mocks/constants.js'; @@ -19,7 +21,7 @@ describe('Create and Sign Vault related Transactions', () => { let fundingTransaction: Transaction; let signedFundingTransaction: Transaction; - it('should initialize a Private Key DLC Handler', async () => { + xit('should initialize a Private Key DLC Handler', async () => { dlcHandler = new PrivateKeyDLCHandler( TEST_BITCOIN_EXTENDED_PRIVATE_KEY, TEST_BITCOIN_WALLET_ACCOUNT_INDEX, @@ -28,13 +30,19 @@ describe('Create and Sign Vault related Transactions', () => { TEST_BITCOIN_BLOCKCHAIN_API, TEST_BITCOIN_BLOCKCHAIN_FEE_RECOMMENDATION_API ); + + const derivedAttestorGroupPublicKey = deriveUnhardenedPublicKey( + TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1, + testnet + ); + console.log('derivedAttestorGroupPublicKey: ', derivedAttestorGroupPublicKey.toString('hex')); }); - it('should create a funding transaction', async () => { + xit('should create a funding transaction', async () => { fundingTransaction = await dlcHandler.createFundingPSBT( TEST_VAULT, BigInt(shiftValue(TEST_BITCOIN_AMOUNT)), - TEST_REGTEST_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY, + TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1, 2 ); @@ -63,7 +71,7 @@ describe('Create and Sign Vault related Transactions', () => { expect(feeOutput?.amount === feeAmount).toBeTruthy(); }); - it('should sign a funding transaction', async () => { + xit('should sign a funding transaction', async () => { signedFundingTransaction = dlcHandler.signPSBT(fundingTransaction, 'funding'); expect(signedFundingTransaction.isFinal).toBeTruthy();