@covenance/dlc
Version:
Crypto and Bitcoin functions for Covenance DLC implementation
493 lines (426 loc) • 16.4 kB
text/typescript
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);
});
});