UNPKG

@covenance/dlc

Version:

Crypto and Bitcoin functions for Covenance DLC implementation

486 lines (431 loc) 15.8 kB
import * as bitcore from '../btc'; import { Script, Address, Transaction, Networks, crypto } from '../btc'; import { DlcInitTx, UTXO, OracleEvent, OracleCET, LoanConfig, P2trOutputScript, LiquidationOutcome } from './types'; import { PubKey, Sighash, Signature } from '../crypto/types'; import { Tap } from '@cmdcode/tapscript'; import { sigToTaprootBuf } from './signature'; import { encodeXOnlyPubkey } from '../crypto/general'; import { csvTimeDelay } from '../utils'; 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 * @param timelock Optional relative timelock in blocks (using OP_CHECKSEQUENCEVERIFY) * @returns The witness script for 2-of-2 multisig */ function createMultisigWitnessScript( borrowerDlcPubKey: PubKey, lenderDlcPubKey: PubKey, timelock?: number ): Script { const script = new Script('') .add(encodeXOnlyPubkey(borrowerDlcPubKey)) .add(Opcode.OP_CHECKSIGADD) .add(encodeXOnlyPubkey(lenderDlcPubKey)) .add(Opcode.OP_CHECKSIGADD) .add(Opcode.OP_2) .add(Opcode.OP_EQUAL); if (timelock) { script .add(Opcode.OP_DROP) .add(csvTimeDelay(timelock)) .add(Opcode.OP_CHECKSEQUENCEVERIFY) .add(Opcode.OP_DROP) .add(Opcode.OP_1); } return script; } /** * 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 * @param timelock Optional relative timelock in seconds (using OP_CHECKSEQUENCEVERIFY) * @param dataPayload Optional OP_RETURN payload in an additional (unspendable) output * @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, timelock?: number, dataPayload?: string | Buffer ): 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, timelock); 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 DLC output tx.to(p2trAddress, collateralAmount); // Add data output if there is any data payload if (dataPayload) { const dataOutput = new Transaction.Output({ script: Script.buildDataOut(dataPayload), satoshis: 0 }); tx.outputs.push(dataOutput); } // 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); } /** * Selects the oracle outcome (price + signature point) that lies at or below the * given liquidation price threshold for a single oracle event. * @param event The oracle event containing the full price grid. * @param liquidationPrice Calculated liquidation-threshold price for this event. * @returns The selected price and its Schnorr signature point. */ export function selectLiquidationOutcome(event: OracleEvent, liquidationPrice: number): { price: number; pubKey: PubKey } { let idx = -1; for (let i = 0; i < event.outcomePrices.length; i++) { if (event.outcomePrices[i] <= liquidationPrice) { idx = i; } else { break; } } if (idx >= event.outcomePrices.length || idx < 0) { throw new Error(`No grid price ≤ liquidation price for ${event.id}`); } return { price: event.outcomePrices[idx], pubKey: event.outcomeSignaturePoints[idx] }; } /** * Picks event outcomes for liquidation. * @param events Array of oracle price events in chronological order (ascending timestamp) * @param config Loan configuration parameters * @returns Array of event outcomes for liquidations */ export function selectLiquidationOutcomes( events: OracleEvent[], config: LoanConfig ): LiquidationOutcome[] { const secsPerYear = 31536000; const outcomes: LiquidationOutcome[] = new Array(events.length); for (let i = 0; i < events.length; i++) { const ev = events[i]; const 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); const { price, pubKey } = selectLiquidationOutcome(ev, p_liq); outcomes[i] = { timestamp: ev.timestamp, price, signaturePoint: pubKey, eventId: ev.id }; } return outcomes; } /** * Creates Contract Execution Transactions (CETs) for 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 * @param outcomes Array of event outcomes for liquidations * @returns Array of OracleCET objects */ export function createLiquidationCets( config: LoanConfig, dlcUtxo: UTXO, borrower: Address, lender: Address, outcomes: LiquidationOutcome[] ): OracleCET[] { const n = outcomes.length; if (n === 0) return []; const secsPerYear = 31536000; const oracleCets: OracleCET[] = new Array(n); const t0 = outcomes[0].timestamp; for (let i = 0; i < n; i++) { const { timestamp, price, signaturePoint, eventId } = outcomes[i]; const tYears = (timestamp - t0) / secsPerYear; const D_t = config.borrowedAmount * (1 + config.annualInterestRate * tYears); 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, outcomeSignaturePoint: signaturePoint, 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; }