@covenance/dlc
Version:
Crypto and Bitcoin functions for Covenance DLC implementation
339 lines • 14.7 kB
JavaScript
"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