@covenance/dlc
Version:
Crypto and Bitcoin functions for Covenance DLC implementation
415 lines (368 loc) • 13.5 kB
text/typescript
import * as bitcore from '../btc';
import { Script, Address, Transaction, Networks, crypto } from '../btc';
import { DlcInitTx, UTXO, OracleEvent, OracleCET, LoanConfig, P2trOutputScript } from './types';
import { PubKey, Sighash, Signature } from '../crypto/types';
import { Tap } from '@cmdcode/tapscript';
import { sigToTaprootBuf } from './signature';
import { encodeXOnlyPubkey } from '../crypto/general';
const Opcode = (bitcore as any).Opcode;
// const multisigWitnessVBytes = 55; TODO
const p2wpkhWitnessVBytes = 27;
// Base input and output vbytes, excluding witness and scripts
const baseInputVBytes = 41;
const baseOutputVBytes = 9;
const p2wpkhOutputScriptVBytes = 22;
const p2trOutputScriptVBytes = 34;
const dustThreshold = 330;
/**
* Creates a 2-of-2 multisig witness script
* @param borrowerPubKey Borrower's public key
* @param lenderPubKey Lender's public key
* @returns The witness script for 2-of-2 multisig
*/
function createMultisigWitnessScript(
borrowerDlcPubKey: PubKey,
lenderDlcPubKey: PubKey
): Script {
return new Script('')
.add(encodeXOnlyPubkey(borrowerDlcPubKey))
.add(Opcode.OP_CHECKSIGADD)
.add(encodeXOnlyPubkey(lenderDlcPubKey))
.add(Opcode.OP_CHECKSIGADD)
.add(Opcode.OP_2)
.add(Opcode.OP_EQUAL);
}
/**
* Creates a P2TR output script from a witness script
* @param witnessScript The witness script to create P2TR for
* @returns The P2TR output script
*/
function createP2trOutputScript(witnessScript: Script): P2trOutputScript {
const witnessScriptBuffer = witnessScript.toBuffer();
const tapleaf = Tap.encodeScript(witnessScriptBuffer);
const bip341UnspendablePubKey = '0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0';
const [tPubKey, cBlock] = Tap.getPubKey(bip341UnspendablePubKey, { target: tapleaf });
return {
scriptPubKey: new Script(`OP_1 32 0x${tPubKey}}`),
tPubKey,
cBlock
};
}
/**
* Creates a DLC initialization transaction that locks funds in a 2-of-2 multisig output
* @param collateralUtxos Array of collateral UTXOs controlled by the borrower
* @param collateralAmount Amount of collateral to lock in the DLC
* @param borrowerPubKey Borrower's public key for the DLC
* @param lenderPubKey Lender's public key for the DLC
* @param changeAddress Address to receive any change from the transaction
* @param feeRate Fee rate in satoshis per vB
* @param network Network to use for addresses
* @returns DLC init transaction object containing the transaction, multisig script and address
*/
export function createDlcInitTx(
collateralUtxos: UTXO[],
collateralAmount: number,
borrowerDlcPubKey: PubKey,
lenderDlcPubKey: PubKey,
changeAddress: Address,
feeRate = 5,
network = Networks.testnet
): DlcInitTx {
// Create a new transaction
const tx = new Transaction();
// Add all collateral UTXOs as inputs
tx.from(collateralUtxos as Transaction.UnspentOutput[]);
// Create 2-of-2 multisig script
const witnessScript = createMultisigWitnessScript(borrowerDlcPubKey, lenderDlcPubKey);
const p2trOutputScript = createP2trOutputScript(witnessScript);
const p2trAddress = new Address(p2trOutputScript.scriptPubKey, network, 'taproot');
let vBytes = (tx as any).vsize;
vBytes += tx.inputs.length * p2wpkhWitnessVBytes;
vBytes += baseInputVBytes + p2trOutputScriptVBytes;
vBytes += baseInputVBytes + p2wpkhOutputScriptVBytes;
const totalInputAmount = collateralUtxos.reduce((acc, utxo) => acc + utxo.satoshis, 0);
const fee = Math.ceil(vBytes * feeRate);
const change = totalInputAmount - collateralAmount - fee;
// Add outputs
tx.to(p2trAddress, collateralAmount);
// Add change output if there is any change
if (change > dustThreshold) {
tx.to(changeAddress, change);
}
const dlcUtxo: UTXO = {
txId: tx.id,
outputIndex: 0,
satoshis: collateralAmount,
script: p2trOutputScript.scriptPubKey,
address: p2trAddress
};
return {
tx,
dlcUtxo,
witnessScript,
cBlock: p2trOutputScript.cBlock,
tPubKey: p2trOutputScript.tPubKey
};
}
/**
* Creates a Contract Execution Transaction (CET) that distributes funds between borrower and lender
* @param dlcUtxo The UTXO from the DLC initialization transaction
* @param borrowerReceiveAmount Amount of satoshis the borrower should receive
* @param lenderReceiveAmount Amount of satoshis the lender should receive
* @param borrowerAddress Borrower's address to receive funds
* @param lenderAddress Lender's address to receive funds
* @param network Network to use for addresses
* @returns The CET transaction
*/
export function createCet(
dlcUtxo: UTXO,
borrowerReceiveAmount: number,
lenderReceiveAmount: number,
borrowerAddress: Address,
lenderAddress: Address
): Transaction {
// Create a new transaction
const tx = new Transaction();
// Add the DLC UTXO as input
tx.from([dlcUtxo as Transaction.UnspentOutput]);
// Add outputs for borrower and lender
if (borrowerReceiveAmount > 0) {
tx.to(borrowerAddress, borrowerReceiveAmount);
}
if (lenderReceiveAmount > 0) {
tx.to(lenderAddress, lenderReceiveAmount);
}
return tx;
}
/**
* Funds the fees for a Contract Execution Transaction (CET) by creating a separate funding transaction
* that can be unlocked by the CET. Since the CET parties sign it using SIGHASH_ANYONECANPAY,
* we can add an additional input without invalidating the signatures.
* @param cet The Contract Execution Transaction to fund fees for
* @param feeRate Fee rate in satoshis per vB
* @param fundsUtxos Array of UTXOs to use for funding the fees
* @param fundingAddress Address of the final funding output, which will be the CET input
* @param changeAddress Address to receive any change from the funding transaction
* @returns Object containing the updated CET and the funding transaction
*/
export function fundCetFees(
cet: Transaction,
fundsUtxos: UTXO[],
fundingAddress: Address,
changeAddress: Address,
feeRate = 5
): { cet: Transaction; fundingTx: Transaction } {
// Create a new funding transaction
const fundingTx = new Transaction();
// Add funding UTXOs as inputs
fundingTx.from(fundsUtxos as Transaction.UnspentOutput[]);
// Calculate the fee required for the CET
let cetVBytes = (cet as any).vsize;
cetVBytes += baseInputVBytes + p2wpkhWitnessVBytes;
const cetFee = Math.ceil(cetVBytes * feeRate);
// Calculate the funding transaction fee
let fundingTxVBytes = (fundingTx as any).vsize;
fundingTxVBytes += fundsUtxos.length * p2wpkhWitnessVBytes;
fundingTxVBytes += 2 * (baseOutputVBytes + p2wpkhOutputScriptVBytes);
const fundingTxFee = Math.ceil(fundingTxVBytes * feeRate);
// Calculate total input amount and change
const totalInputAmount = fundsUtxos.reduce((acc, utxo) => acc + utxo.satoshis, 0);
const change = totalInputAmount - cetFee - fundingTxFee;
// Add the CET fee output. We use P2WPKH.
const cetFeeOutput = new Transaction.Output({
script: (Script as any).buildWitnessV0Out(fundingAddress),
satoshis: cetFee
});
fundingTx.outputs.push(cetFeeOutput);
// Add change output if there is any change
if (change > dustThreshold) {
fundingTx.to(changeAddress, change);
}
const cetUtxo = new Transaction.UnspentOutput({
txId: fundingTx.id,
outputIndex: 0,
script: fundingTx.outputs[0].script,
satoshis: cetFee
});
cet.from([cetUtxo]);
return {
cet,
fundingTx
};
}
/**
* Returns the sighash for a transaction input.
* @param tx The transaction object
* @param sighash The sighash flag
* @param inputIndex The index of the input to hash
* @param prevScriptPubKey The previous scriptPubKey of the input
* @param satoshis The amount of satoshis in the input
* @returns The sighash
*/
export function getTxSigHash(
tx: Transaction,
sighash: number,
inputIndex: number,
prevScriptPubKey: Script,
satoshis: number
): Sighash {
const satoshisBN = new (bitcore as any).crypto.BN.fromNumber(satoshis);
const satoshisBuffer = (bitcore as any).encoding.BufferWriter().writeUInt64LEBN(satoshisBN).toBuffer();
const sighashBuffer: Buffer = (Transaction as any).SighashWitness.sighash(
tx, sighash, inputIndex, prevScriptPubKey.toBuffer(), satoshisBuffer,
);
return new Uint8Array(sighashBuffer);
}
/**
* Creates Contract Execution Transactions (CETs) for liquidation events
* @param events Array of liquidation events
* @param config Configuration object containing loan parameters
* @param dlcUtxo The UTXO from the DLC initialization transaction
* @param borrowerAddress Borrower's address to receive funds
* @param lenderAddress Lender's address to receive funds
* @returns Array of OracleCET objects
*/
export function createLiquidationCets(
events: OracleEvent[],
config: LoanConfig,
dlcUtxo: UTXO,
borrower: Address,
lender: Address
): OracleCET[] {
const secsPerYear = 31536000;
const oracleCets = new Array<OracleCET>(events.length);
let tSinceStart = 0;
for (let i = 0; i < events.length; i++) {
const ev = events[i];
tSinceStart = ev.timestamp - events[0].timestamp;
const tYears = tSinceStart / secsPerYear;
const D_t = config.borrowedAmount * (1 + config.annualInterestRate * tYears);
const p_liq = D_t / (config.collateralAmount * config.liquidationThreshold);
// Find the price in the grid that is the closest to p_liq but still below it.
// ev.outcomePrices is sorted in ascending order
let idx = -1;
for (let i = 0; i < ev.outcomePrices.length; i++) {
if (ev.outcomePrices[i] <= p_liq) {
idx = i;
} else {
break;
}
}
if (idx >= ev.outcomePrices.length || idx < 0) throw new Error(`No grid price ≤ p_liq for ${ev.id}`);
const price = ev.outcomePrices[idx];
const pubKey = ev.outcomeSignaturePoints[idx];
const L = Math.min(
config.collateralAmount,
(D_t + config.borrowedAmount * config.penaltyPercentage) / price
);
const B = config.collateralAmount - L;
oracleCets[i] = {
cetTx: createCet(
dlcUtxo,
Math.floor(B * 1e8),
Math.floor(L * 1e8),
borrower,
lender
),
eventId: ev.id,
outcomeSignaturePoint: pubKey,
outcomeLiquidationPrice: price,
lenderAmount: L,
borrowerAmount: B
};
}
return oracleCets;
}
/**
* Creates Contract Execution Transactions (CETs) for the loan maturity event
* @param event Liquidation event
* @param grid Price grid
* @param config Configuration object containing loan parameters
* @param dlcUtxo The UTXO from the DLC initialization transaction
* @param borrowerAddress Borrower's address to receive funds
* @param lenderAddress Lender's address to receive funds
* @returns Array of OracleCET objects
*/
export function createMaturityCets(
event: OracleEvent,
startTime: number,
config: LoanConfig,
dlcUtxo: UTXO,
borrower: Address,
lender: Address
): OracleCET[] {
const secsPerYear = 31536000;
const oracleCets = new Array<OracleCET>(event.outcomePrices.length);
// TODO: Derive loan duration from the difference between the first and last event.
for (let i = 0; i < event.outcomePrices.length; i++) {
const D_end = config.borrowedAmount *
(1 + config.annualInterestRate * ((startTime - event.timestamp) / secsPerYear));
const p_end = event.outcomePrices[i];
const pubKey = event.outcomeSignaturePoints[i];
const B = Math.min(
config.collateralAmount,
D_end / p_end
);
const L = config.collateralAmount - B;
oracleCets[i] = {
cetTx: createCet(
dlcUtxo,
Math.floor(B * 1e8),
Math.floor(L * 1e8),
borrower,
lender
),
eventId: event.id,
outcomeSignaturePoint: pubKey,
outcomeLiquidationPrice: p_end,
lenderAmount: L,
borrowerAmount: B
};
}
return oracleCets;
}
/**
* Creates a Contract Execution Transaction (CET) for repayment that returns the full collateral to the borrower
* @param dlcUtxo The UTXO from the DLC initialization transaction
* @param collateralAmount Amount of satoshis to return to the borrower
* @param borrowerAddress Borrower's address to receive funds
* @returns The CET transaction
*/
export function createRepaymentCet(
dlcUtxo: UTXO,
collateralAmount: number,
borrowerAddress: Address
): Transaction {
const tx = new Transaction();
// Add the DLC UTXO as input
tx.from([dlcUtxo as Transaction.UnspentOutput]);
// Add output for borrower with full collateral
tx.to(borrowerAddress, collateralAmount);
return tx;
}
/**
* Applies signatures to a CET
* @param cet The CET to apply signatures to
* @param witnessScript The witness script of the CET
* @param sigBorrower The borrower's signature
* @param sigLender The lender's signature
* @param cBlock The cBlock of the CET
*/
export function applySignaturesCet(
cet: Transaction,
witnessScript: Script,
sigBorrower: Signature,
sigLender: Signature,
cBlock: string,
sighash = crypto.Signature.SIGHASH_ALL | crypto.Signature.SIGHASH_ANYONECANPAY,
): Transaction {
const witnesses = [
Buffer.from(sigToTaprootBuf(sigLender, sighash)),
Buffer.from(sigToTaprootBuf(sigBorrower, sighash)),
Buffer.alloc(0),
witnessScript.toBuffer(),
Buffer.from(cBlock, 'hex')
];
(cet.inputs[0] as any).witnesses = witnesses;
return cet;
}