UNPKG

@covenance/dlc

Version:

Crypto and Bitcoin functions for Covenance DLC implementation

339 lines 14.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.createDlcInitTx = createDlcInitTx; exports.createCet = createCet; exports.fundCetFees = fundCetFees; exports.getTxSigHash = getTxSigHash; exports.createLiquidationCets = createLiquidationCets; exports.createMaturityCets = createMaturityCets; exports.createRepaymentCet = createRepaymentCet; exports.applySignaturesCet = applySignaturesCet; const bitcore = __importStar(require("../btc")); const btc_1 = require("../btc"); const tapscript_1 = require("@cmdcode/tapscript"); const signature_1 = require("./signature"); const general_1 = require("../crypto/general"); const Opcode = bitcore.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, lenderDlcPubKey) { return new btc_1.Script('') .add((0, general_1.encodeXOnlyPubkey)(borrowerDlcPubKey)) .add(Opcode.OP_CHECKSIGADD) .add((0, general_1.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) { const witnessScriptBuffer = witnessScript.toBuffer(); const tapleaf = tapscript_1.Tap.encodeScript(witnessScriptBuffer); const bip341UnspendablePubKey = '0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'; const [tPubKey, cBlock] = tapscript_1.Tap.getPubKey(bip341UnspendablePubKey, { target: tapleaf }); return { scriptPubKey: new btc_1.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 */ function createDlcInitTx(collateralUtxos, collateralAmount, borrowerDlcPubKey, lenderDlcPubKey, changeAddress, feeRate = 5, network = btc_1.Networks.testnet) { // Create a new transaction const tx = new btc_1.Transaction(); // Add all collateral UTXOs as inputs tx.from(collateralUtxos); // Create 2-of-2 multisig script const witnessScript = createMultisigWitnessScript(borrowerDlcPubKey, lenderDlcPubKey); const p2trOutputScript = createP2trOutputScript(witnessScript); const p2trAddress = new btc_1.Address(p2trOutputScript.scriptPubKey, network, 'taproot'); let vBytes = tx.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 = { 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 */ function createCet(dlcUtxo, borrowerReceiveAmount, lenderReceiveAmount, borrowerAddress, lenderAddress) { // Create a new transaction const tx = new btc_1.Transaction(); // Add the DLC UTXO as input tx.from([dlcUtxo]); // 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 */ function fundCetFees(cet, fundsUtxos, fundingAddress, changeAddress, feeRate = 5) { // Create a new funding transaction const fundingTx = new btc_1.Transaction(); // Add funding UTXOs as inputs fundingTx.from(fundsUtxos); // Calculate the fee required for the CET let cetVBytes = cet.vsize; cetVBytes += baseInputVBytes + p2wpkhWitnessVBytes; const cetFee = Math.ceil(cetVBytes * feeRate); // Calculate the funding transaction fee let fundingTxVBytes = fundingTx.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 btc_1.Transaction.Output({ script: btc_1.Script.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 btc_1.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 */ function getTxSigHash(tx, sighash, inputIndex, prevScriptPubKey, satoshis) { const satoshisBN = new bitcore.crypto.BN.fromNumber(satoshis); const satoshisBuffer = bitcore.encoding.BufferWriter().writeUInt64LEBN(satoshisBN).toBuffer(); const sighashBuffer = btc_1.Transaction.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 */ function createLiquidationCets(events, config, dlcUtxo, borrower, lender) { const secsPerYear = 31536000; const oracleCets = new Array(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 */ function createMaturityCets(event, startTime, config, dlcUtxo, borrower, lender) { const secsPerYear = 31536000; const oracleCets = new Array(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 */ function createRepaymentCet(dlcUtxo, collateralAmount, borrowerAddress) { const tx = new btc_1.Transaction(); // Add the DLC UTXO as input tx.from([dlcUtxo]); // 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 */ function applySignaturesCet(cet, witnessScript, sigBorrower, sigLender, cBlock, sighash = btc_1.crypto.Signature.SIGHASH_ALL | btc_1.crypto.Signature.SIGHASH_ANYONECANPAY) { const witnesses = [ Buffer.from((0, signature_1.sigToTaprootBuf)(sigLender, sighash)), Buffer.from((0, signature_1.sigToTaprootBuf)(sigBorrower, sighash)), Buffer.alloc(0), witnessScript.toBuffer(), Buffer.from(cBlock, 'hex') ]; cet.inputs[0].witnesses = witnesses; return cet; } //# sourceMappingURL=transactions.js.map