Skip to content

Commit

Permalink
feat: modify por calculation, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Polybius93 committed Jul 11, 2024
1 parent ea53ec0 commit f06efd1
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 67 deletions.
10 changes: 10 additions & 0 deletions src/functions/bitcoin/bitcoin-functions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { hexToBytes } from '@noble/hashes/utils';
import {
Address,
OutScript,
Expand Down Expand Up @@ -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 &&
Expand Down
1 change: 1 addition & 0 deletions src/functions/bitcoin/bitcoin-request-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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
67 changes: 67 additions & 0 deletions src/functions/proof-of-reserve/proof-of-reserve-functions.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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;
}
}
68 changes: 8 additions & 60 deletions src/proof-of-reserve-handlers/proof-of-reserve-handler.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -30,51 +20,6 @@ export class ProofOfReserveHandler {
this.attestorGroupPublicKey = attestorGroupPublicKey;
}

async verifyVaultDeposit(
vault: RawVault,
attestorGroupPublicKey: Buffer,
bitcoinBlockchainBlockHeight: number
): Promise<boolean> {
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<number> {
const bitcoinBlockchainBlockHeight = await fetchBitcoinBlockchainBlockHeight(
this.bitcoinBlockchainAPI
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions tests/mocks/api.test.constants.ts
Original file line number Diff line number Diff line change
@@ -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';
8 changes: 8 additions & 0 deletions tests/mocks/attestor.test.constants.ts
Original file line number Diff line number Diff line change
@@ -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';
2 changes: 2 additions & 0 deletions tests/mocks/bitcoin.test.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const TEST_BITCOIN_BLOCKCHAIN_BLOCK_HEIGHT_1 = 2867441;
export const TEST_BITCOIN_BLOCKCHAIN_BLOCK_HEIGHT_2 = 2867285;
81 changes: 79 additions & 2 deletions tests/mocks/constants.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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',
};
46 changes: 46 additions & 0 deletions tests/unit/proof-of-reserve.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Loading

0 comments on commit f06efd1

Please sign in to comment.