UNPKG

@covenance/dlc

Version:

Crypto and Bitcoin functions for Covenance DLC implementation

493 lines (426 loc) 16.4 kB
import { expect } from 'chai'; import { Transaction, Address, PrivateKey, Script, Networks, PublicKey } from '../../src/btc'; import { Point, utils } from '../../src/crypto/secp256k1'; import { createDlcInitTx, createCet, fundCetFees, applySignaturesCet } from '../../src/cet/transactions'; import { getTxSigHash } from '../../src/cet/transactions'; import { signCetWithAdaptorSig } from '../../src/cet/signature'; import { commitToEvent, attestEventOutcome } from '../../src'; import { adaptSig } from '../../src/crypto/counterparty'; import { PrivKey, EventOutcomeHash, PubKey } from '../../src/crypto/types'; import { bytesToHex } from '../../src/utils'; describe('DLC Transactions', () => { describe('createDlcInitTx', () => { let borrowerKey: PrivateKey; let borrowerPubKey: PublicKey; let borrowerAddress: Address; let borrowerDlcPrivateKey: PrivKey; let borrowerDlcPubKey: PubKey; let lenderDlcPrivateKey: PrivKey; let lenderDlcPubKey: PubKey; before(() => { // Create test keys borrowerKey = new PrivateKey(); borrowerPubKey = borrowerKey.toPublicKey(); borrowerAddress = new Address(borrowerPubKey, Networks.testnet, 'witnesspubkeyhash'); borrowerDlcPrivateKey = utils.randomPrivateKey(); borrowerDlcPubKey = Point.fromPrivateKey(borrowerDlcPrivateKey); lenderDlcPrivateKey = utils.randomPrivateKey(); lenderDlcPubKey = Point.fromPrivateKey(lenderDlcPrivateKey); }); it('should create a valid DLC init transaction with 2-of-2 multisig output', () => { const collateralAmount = 100000; // 0.001 BTC const inputAmount = collateralAmount * 2; // Create test UTXO const collateralUtxos = [{ txId: 'a'.repeat(64), // Mock txId outputIndex: 0, satoshis: inputAmount, script: (Script as any).buildWitnessV0Out(borrowerAddress) }]; // Create DLC init transaction const dlcInitTx = createDlcInitTx( collateralUtxos, collateralAmount, borrowerDlcPubKey, lenderDlcPubKey, borrowerAddress ); // Verify transaction structure expect(dlcInitTx.tx).to.be.instanceOf(Transaction); expect(dlcInitTx.tx.inputs.length).to.equal(1); expect(dlcInitTx.tx.outputs.length).to.equal(2); // Verify output amounts expect(dlcInitTx.tx.outputs[0].satoshis).to.equal(collateralAmount); expect(dlcInitTx.tx.outputs[1].satoshis).to.be.lessThan(collateralAmount); expect( dlcInitTx.tx.outputs[0].satoshis + dlcInitTx.tx.outputs[1].satoshis ).to.be.lessThan(inputAmount); }); it('should handle multiple collateral UTXOs correctly', () => { const firstInputAmount = 50000; const secondInputAmount = 75000; const thirdInputAmount = 100000; const collateralAmount = firstInputAmount + secondInputAmount; // Create multiple test UTXOs const collateralUtxos = [ { txId: 'a'.repeat(64), outputIndex: 0, satoshis: firstInputAmount, script: (Script as any).buildWitnessV0Out(borrowerAddress) }, { txId: 'b'.repeat(64), outputIndex: 1, satoshis: secondInputAmount, script: (Script as any).buildWitnessV0Out(borrowerAddress) }, { txId: 'c'.repeat(64), outputIndex: 0, satoshis: thirdInputAmount, script: (Script as any).buildWitnessV0Out(borrowerAddress) } ]; // Create DLC init transaction const dlcInitTx = createDlcInitTx( collateralUtxos, collateralAmount, borrowerDlcPubKey, lenderDlcPubKey, borrowerAddress ); // Verify inputs and outputs counts expect(dlcInitTx.tx.inputs.length).to.equal(3); expect(dlcInitTx.tx.outputs.length).to.equal(2); // Verify output amounts expect(dlcInitTx.tx.outputs[0].satoshis).to.equal(collateralAmount); expect(dlcInitTx.tx.outputs[1].satoshis).to.be.lessThan(collateralAmount); expect( dlcInitTx.tx.outputs[0].satoshis + dlcInitTx.tx.outputs[1].satoshis ).to.be.lessThan( firstInputAmount + secondInputAmount + thirdInputAmount ); }); }); describe('createCet', () => { let borrowerKey: PrivateKey; let lenderKey: PrivateKey; let borrowerAddress: Address; let lenderAddress: Address; before(() => { // Create test keys and addresses borrowerKey = new PrivateKey(); lenderKey = new PrivateKey(); borrowerAddress = new Address(borrowerKey.toPublicKey(), Networks.testnet, 'taproot'); lenderAddress = new Address(lenderKey.toPublicKey(), Networks.testnet, 'taproot'); }); it('should create a valid CET with correct amounts and outputs', () => { // Create mock UTXO const dlcUtxo = { txId: 'a'.repeat(64), outputIndex: 0, satoshis: 100000, // 0.001 BTC script: new Script('') }; // Create CET const borrowerAmount = 60000; const lenderAmount = 40000; const cet = createCet( dlcUtxo, borrowerAmount, lenderAmount, borrowerAddress, lenderAddress ); // Verify transaction structure expect(cet).to.be.instanceOf(Transaction); expect(cet.inputs.length).to.equal(1); expect(cet.outputs.length).to.equal(2); // Verify input expect(cet.inputs[0].prevTxId.toString('hex')).to.equal(dlcUtxo.txId); expect(cet.inputs[0].outputIndex).to.equal(dlcUtxo.outputIndex); // Verify output amounts expect(cet.outputs[0].satoshis).to.equal(borrowerAmount); expect(cet.outputs[1].satoshis).to.equal(lenderAmount); // Verify output scripts are P2TR expect(cet.outputs[0].script.toHex()).to.include('5120'); expect(cet.outputs[1].script.toHex()).to.include('5120'); }); it('should handle zero amounts correctly', () => { // Create mock DLC UTXO const dlcUtxo = { txId: 'a'.repeat(64), outputIndex: 0, satoshis: 100000, script: new Script('') }; // Create CET with zero amounts const cet = createCet( dlcUtxo, 0, 100000, borrowerAddress, lenderAddress ); // Verify output amounts expect(cet.outputs[0].satoshis).to.equal(100000); expect(cet.outputs.length).to.equal(1); }); }); describe('fundCetFees', () => { let borrowerKey: PrivateKey; let lenderKey: PrivateKey; let borrowerAddress: Address; let lenderAddress: Address; let fundingAddress: Address; let changeAddress: Address; let dlcUtxo: any; let cet: Transaction; before(() => { // Create test keys and addresses borrowerKey = new PrivateKey(); lenderKey = new PrivateKey(); borrowerAddress = new Address(borrowerKey.toPublicKey(), Networks.testnet, 'witnesspubkeyhash'); lenderAddress = new Address(lenderKey.toPublicKey(), Networks.testnet, 'witnesspubkeyhash'); fundingAddress = new Address(new PrivateKey().toPublicKey(), Networks.testnet, 'witnesspubkeyhash'); changeAddress = new Address(new PrivateKey().toPublicKey(), Networks.testnet, 'witnesspubkeyhash'); // Create mock DLC UTXO dlcUtxo = { txId: 'a'.repeat(64), outputIndex: 0, satoshis: 100000, script: new Script('') }; // Create a CET to fund cet = createCet( dlcUtxo, 60000, 40000, borrowerAddress, lenderAddress ); }); it('should create a valid funding transaction with correct fee calculations', () => { const feeRate = 5; // 5 sat/vB const fundsUtxos = [{ txId: 'b'.repeat(64), outputIndex: 0, satoshis: 100000, script: (Script as any).buildWitnessV0Out(fundingAddress) }]; const result = fundCetFees( cet, fundsUtxos, fundingAddress, changeAddress, feeRate ); // Verify funding transaction structure expect(result.fundingTx).to.be.instanceOf(Transaction); expect(result.fundingTx.inputs.length).to.equal(1); expect(result.fundingTx.outputs.length).to.be.at.least(1); // Verify CET was updated with new input expect(result.cet.inputs.length).to.equal(2); // Original input + funding input expect(result.cet.inputs[1].prevTxId.toString('hex')).to.equal(result.fundingTx.id); expect(result.cet.inputs[1].outputIndex).to.equal(0); // Verify funding transaction outputs const cetFeeOutput = result.fundingTx.outputs[0]; expect(cetFeeOutput.satoshis).to.be.greaterThan(0); expect(cetFeeOutput.script.toHex()).to.include('0014'); // Verify change output exists if there's enough change const totalInputAmount = fundsUtxos.reduce((acc, utxo) => acc + utxo.satoshis, 0); const totalOutputAmount = result.fundingTx.outputs.reduce((acc, output) => acc + output.satoshis, 0); const fee = totalInputAmount - totalOutputAmount; expect(fee).to.be.greaterThan(0); }); it('should handle multiple funding UTXOs correctly', () => { const feeRate = 5; const fundsUtxos = [ { txId: 'b'.repeat(64), outputIndex: 0, satoshis: 50000, script: (Script as any).buildWitnessV0Out(fundingAddress) }, { txId: 'c'.repeat(64), outputIndex: 1, satoshis: 75000, script: (Script as any).buildWitnessV0Out(fundingAddress) } ]; const result = fundCetFees( cet, fundsUtxos, fundingAddress, changeAddress, feeRate ); // Verify funding transaction has correct number of inputs expect(result.fundingTx.inputs.length).to.equal(2); // Verify total input amount is correct const totalInputAmount = fundsUtxos.reduce((acc, utxo) => acc + utxo.satoshis, 0); const totalOutputAmount = result.fundingTx.outputs.reduce((acc, output) => acc + output.satoshis, 0); const fee = totalInputAmount - totalOutputAmount; expect(fee).to.be.greaterThan(0); }); it('should handle dust threshold correctly', () => { const feeRate = 5; const fundsUtxos = [{ txId: 'b'.repeat(64), outputIndex: 0, satoshis: 2000, // Small amount that will cover fees but result in dust change script: (Script as any).buildWitnessV0Out(fundingAddress) }]; const result = fundCetFees( cet, fundsUtxos, fundingAddress, changeAddress, feeRate ); // Verify no change output if change would be dust const totalInputAmount = fundsUtxos.reduce((acc, utxo) => acc + utxo.satoshis, 0); const totalOutputAmount = result.fundingTx.outputs.reduce((acc, output) => acc + output.satoshis, 0); const fee = totalInputAmount - totalOutputAmount; expect(fee).to.be.greaterThan(0); expect(result.fundingTx.outputs.length).to.equal(1); // Only CET fee output, no change }); }); }); describe('getTxSigHash', () => { it('should generate correct sighash for a transaction input', () => { const priv = new PrivateKey(); const addr = new Address(priv.toPublicKey(), Networks.testnet, 'taproot'); // Create a simple transaction const tx = new Transaction(); const inputAmount = 100000; // 0.001 BTC const outputAmount = 90000; // 0.0009 BTC // Create a mock input const prevTxId = '0000000000000000000000000000000000000000000000000000000000000000'; const prevOutputIndex = 0; const unspentOutput = new Transaction.UnspentOutput({ txid: prevTxId, vout: prevOutputIndex, script: (Script as any).buildWitnessV1Out(addr), satoshis: inputAmount }); tx.from([unspentOutput]); // Add an output tx.to(addr, outputAmount); // Generate sighash const sighash = getTxSigHash( tx, 0x01, // SIGHASH_ALL 0, unspentOutput.script, inputAmount ); // Verify the sighash is a Uint8Array expect(sighash).to.be.instanceOf(Uint8Array); expect(sighash.length).to.be.greaterThan(0); // Verify the sighash is deterministic (tx.inputs[0] as any).witnesses.push(Buffer.from('01')); const sighash2 = getTxSigHash( tx, 0x01, // SIGHASH_ALL 0, unspentOutput.script, inputAmount ); expect(sighash).to.deep.equal(sighash2); }); }); describe('applySignaturesCet', () => { let borrowerPrivKey: PrivKey; let lenderPrivKey: PrivKey; let borrowerPubKey: Point; let borrowerAddress: Address; let lenderAddress: Address; let dlcUtxo: any; let cet: Transaction; let oraclePrivKey: PrivKey; let oraclePubKey: Point; let eventOutcomeHashes: EventOutcomeHash[]; before(async () => { // Generate test keys using @noble/secp256k1 oraclePrivKey = utils.randomPrivateKey(); oraclePubKey = Point.fromPrivateKey(oraclePrivKey); borrowerPrivKey = utils.randomPrivateKey(); borrowerPubKey = Point.fromPrivateKey(borrowerPrivKey); lenderPrivKey = utils.randomPrivateKey(); // Convert @noble/secp256k1 keys to bitcore-lib format for addresses const borrowerBitcoreKey = new PrivateKey(bytesToHex(borrowerPrivKey)); const lenderBitcoreKey = new PrivateKey(bytesToHex(lenderPrivKey)); borrowerAddress = new Address(borrowerBitcoreKey.toPublicKey(), Networks.testnet, 'taproot'); lenderAddress = new Address(lenderBitcoreKey.toPublicKey(), Networks.testnet, 'taproot'); eventOutcomeHashes = [ new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6]) ]; dlcUtxo = { txId: 'a'.repeat(64), outputIndex: 0, satoshis: 100000, script: new Script('') }; cet = createCet(dlcUtxo, 60000, 40000, borrowerAddress, lenderAddress); }); it('should successfully apply signatures to a CET', async () => { const { signaturePoints, nonce } = await commitToEvent(eventOutcomeHashes, oraclePubKey); const outcomeIndex = 0; const borrowerAdaptorSig = await signCetWithAdaptorSig( borrowerPrivKey, signaturePoints[outcomeIndex], cet, 0, Buffer.from('') ); const lenderAdaptorSig = await signCetWithAdaptorSig( lenderPrivKey, signaturePoints[outcomeIndex], cet, 0, Buffer.from('') ); const oracleSig = await attestEventOutcome( oraclePrivKey, nonce, eventOutcomeHashes[outcomeIndex] ); const borrowerSig = adaptSig(borrowerAdaptorSig, oracleSig.s); const lenderSig = adaptSig(lenderAdaptorSig, oracleSig.s); // Create a simple witness script for testing const witnessScript = new Script('76a914' + bytesToHex(borrowerPubKey.toRawBytes()) + '88ac'); // Create a test cBlock (normally this would come from tapscript) const testCBlock = '0123456789abcdef'.repeat(4); // 32 bytes hex const signedCet = applySignaturesCet( cet, witnessScript, borrowerSig, lenderSig, testCBlock ); // Verify the signatures were applied correctly expect(signedCet.inputs[0]).to.have.property('witnesses'); const witnesses = (signedCet.inputs[0] as any).witnesses; expect(witnesses).to.be.an('array').with.lengthOf(5); // Verify each witness component expect(witnesses[0]).to.be.instanceOf(Buffer); expect(witnesses[0].length).to.equal(65); // 64 bytes + 1 byte sighash type expect(witnesses[1]).to.be.instanceOf(Buffer); expect(witnesses[1].length).to.equal(65); expect(witnesses[2]).to.be.instanceOf(Buffer); expect(witnesses[2].length).to.equal(0); expect(witnesses[3]).to.be.instanceOf(Buffer); expect(witnesses[3].toString('hex')).to.equal(witnessScript.toHex()); expect(witnesses[4]).to.be.instanceOf(Buffer); expect(witnesses[4].toString('hex')).to.equal(testCBlock); // Verify the transaction structure remains valid expect(signedCet.inputs.length).to.equal(1); expect(signedCet.outputs.length).to.equal(2); expect(signedCet.outputs[0].satoshis).to.equal(60000); expect(signedCet.outputs[1].satoshis).to.equal(40000); }); });