@covenance/dlc
Version:
Crypto and Bitcoin functions for Covenance DLC implementation
491 lines (412 loc) • 17 kB
text/typescript
import { expect } from 'chai';
import { Address, PrivateKey, Script, Networks, PublicKey, Transaction } from '../../src/btc';
import { createLiquidationCets, createDlcInitTx, createMaturityCets, createRepaymentCet, applySignaturesCet, fundCetFees } from '../../src/cet/transactions';
import { OracleEvent, LoanConfig, OracleCET } from '../../src/cet/types';
import { Point, utils } from '../../src/crypto/secp256k1';
import { signCetWithAdaptorSig, verifyCetAdaptorSig } from '../../src/cet/signature';
import { AdaptorSignature, EventOutcomeHash, PrivKey } from '../../src/crypto/types';
import { adaptSig, attestEventOutcome, bytesToHex, commitToEvent, sha256, sighashForAdaptorSig, tapleafHash, verifySigStrict } from '../../src';
import { generateEvenYPrivateKey } from '../../src/crypto/counterparty';
const signCetAdaptorSignatures = async (
privateKey: PrivKey,
outcomeSignaturePoints: Point[],
cetTxs: Transaction[],
leafHash: Buffer,
inputIndex = 0
): Promise<AdaptorSignature[]> => {
const adaptorSigs = new Array<AdaptorSignature>(cetTxs.length);
for (let i = 0; i < cetTxs.length; i++) {
adaptorSigs[i] = await signCetWithAdaptorSig(
privateKey,
outcomeSignaturePoints[i],
cetTxs[i],
inputIndex,
leafHash
);
}
return adaptorSigs;
};
const validateAdaptorSignatures = async (
adaptorSigs: AdaptorSignature[],
pubKey: Point,
outcomeSignaturePoints: Point[],
cetTxs: Transaction[],
leafHash: Buffer,
inputIndex = 0
): Promise<void> => {
for (let i = 0; i < adaptorSigs.length; i++) {
const isAdaptorSigValid = await verifyCetAdaptorSig(
adaptorSigs[i],
pubKey,
outcomeSignaturePoints[i],
cetTxs[i],
inputIndex,
leafHash
);
expect(isAdaptorSigValid).to.be.true;
}
};
const createLiquidationEvents = async (
count: number,
oraclePubKey: Point,
startTime: number = (Date.now() / 1000) | 0,
): Promise<OracleEvent[]> => {
const eventTimeDelta = 60 * 60 * 12;
const events = new Array<OracleEvent>(count);
for (let i = 0; i < count; i++) {
const nOutcomes = 100 * 10;
const eventOutcomeHashes = new Array<EventOutcomeHash>(nOutcomes);
const outcomePrices = new Array<number>(nOutcomes);
for (let j = 0; j < nOutcomes; j++) {
const eventOutcomeHash = await sha256(new Uint8Array([i, j])); // TODO: Turn into random hash
eventOutcomeHashes[j] = eventOutcomeHash;
outcomePrices[j] = 50000 + j * 100;
}
const { signaturePoints, nonce } = await commitToEvent(eventOutcomeHashes, oraclePubKey);
events[i] = {
id: `event${i}`,
timestamp: startTime + i * eventTimeDelta,
outcomeSignaturePoints: signaturePoints,
outcomeHashes: eventOutcomeHashes,
outcomePrices,
nonce,
};
}
return events;
};
describe('End-to-End Tx Flow Test', function() {
this.timeout(160000);
before(function() {
if (process.env.TEST_HEAVY !== 'true') {
this.skip();
}
});
let borrowerKey: PrivateKey;
let lenderKey: PrivateKey;
let borrowerPubKey: PublicKey;
let borrowerAddress: Address;
let lenderAddress: Address;
let lenderRepaymentSecret: PrivKey;
let lenderRepaymentCommitment: Point;
let oraclePrivKey: PrivKey;
let oraclePubKey: Point;
let dlcUtxo: any;
let liquidationEvents: OracleEvent[];
let maturityEvent: OracleEvent;
before(async function() {
borrowerKey = new PrivateKey();
lenderKey = new PrivateKey();
borrowerPubKey = borrowerKey.toPublicKey();
borrowerAddress = new Address(borrowerPubKey, Networks.testnet, 'witnesspubkeyhash');
lenderAddress = new Address(lenderKey.toPublicKey(), Networks.testnet, 'witnesspubkeyhash');
lenderRepaymentSecret = utils.randomPrivateKey();
lenderRepaymentCommitment = Point.fromPrivateKey(lenderRepaymentSecret);
oraclePrivKey = utils.randomPrivateKey();
oraclePubKey = Point.fromPrivateKey(oraclePrivKey);
const nEvents = 10;
liquidationEvents = await createLiquidationEvents(nEvents, oraclePubKey);
// TODO: Move this to separate function createMaturityEvent
const maturityEventOutcomeHashes = await Promise.all(
Array.from({ length: 100 * 10 }, async (_, i) =>
await sha256(new Uint8Array([i])) // TODO: Turn into random hash
)
);
maturityEvent = {
id: 'maturity-event',
outcomeSignaturePoints: Array.from({ length: 100 * 10 }, () =>
Point.fromPrivateKey(utils.randomPrivateKey())
),
outcomeHashes: maturityEventOutcomeHashes,
outcomePrices: Array.from({ length: 100 * 10 }, (_, i) => 50000 + i * 100),
timestamp: Date.now(),
};
});
it('should run successfull end-to-end flow', async () => {
// The process starts with the borrower picking their loan terms.
const borrowedAmountUsd = 15000; // $15,000 USD
const liquidationThreshold = 0.8;
const annualInterestRate = 0.1;
const initalBtcPrice = 100000; // 100,000 USD/BTC
// Gives starting LR = 0.5 < 0.8, decent buffer
const collateralAmount = (borrowedAmountUsd / initalBtcPrice) * 2;
const config: LoanConfig = {
collateralAmount,
annualInterestRate,
liquidationThreshold,
borrowedAmount: borrowedAmountUsd,
penaltyPercentage: 0.1
};
const inputAmount = collateralAmount * 2;
const collateralUtxos = [{
txId: 'a'.repeat(64),
outputIndex: 0,
satoshis: inputAmount * 100000000,
script: (Script as any).buildWitnessV0Out(borrowerAddress)
}];
const borrowerDlcPrivateKey = generateEvenYPrivateKey();
const borrowerDlcPubKey = Point.fromPrivateKey(borrowerDlcPrivateKey);
// The borrower can now send the data created above to our service.
// At this point, our service will check that the loan terms are valid.
// If they are not valid, the borrower will be notified and the flow will end.
// If they are valid, our service will ask the lender to generate a DLC keypair for this contract.
// We'll also send over the loan terms for the lender to review and confirm.
const lenderDlcPrivateKey = generateEvenYPrivateKey();
const lenderDlcPubKey = Point.fromPrivateKey(lenderDlcPrivateKey);
// Our service then forwards the lenders DLC pubkey to the borrower.
// Now both parties have all the information they need to construct the DLC init tx and all of the CETs.
// Party creates the DLC init tx.
let feeRate = 5;
const dlcInitTx = createDlcInitTx(
collateralUtxos,
collateralAmount * 100000000,
borrowerDlcPubKey,
lenderDlcPubKey,
borrowerAddress,
feeRate
);
dlcUtxo = dlcInitTx.dlcUtxo;
// Party creates the liquidation CETs.
const liquidationCets = await createLiquidationCets(
liquidationEvents,
config,
dlcUtxo,
borrowerAddress,
lenderAddress
);
// Party creates the maturity CETs.
const maturityCets = createMaturityCets(
maturityEvent,
liquidationEvents[0].timestamp,
config,
dlcUtxo,
borrowerAddress,
lenderAddress
);
// Party creates the repayment CET.
const repaymentCet = createRepaymentCet(
dlcUtxo,
collateralAmount * 100000000,
borrowerAddress
);
// After constructing all of the CETs, the borrower and lender can sign them with adaptor sigs.
const leafHash = tapleafHash(dlcInitTx.witnessScript);
// Sign borrower's CETs
const borrowerLiquidationCetAdaptorSigs = await signCetAdaptorSignatures(
borrowerDlcPrivateKey,
liquidationCets.map(cet => cet.outcomeSignaturePoint),
liquidationCets.map(cet => cet.cetTx),
leafHash
);
const borrowerMaturityCetAdaptorSigs = await signCetAdaptorSignatures(
borrowerDlcPrivateKey,
maturityCets.map(cet => cet.outcomeSignaturePoint),
maturityCets.map(cet => cet.cetTx),
leafHash
);
const borrowerRepaymentCetAdaptorSig = await signCetWithAdaptorSig(
borrowerDlcPrivateKey,
lenderRepaymentCommitment,
repaymentCet,
0,
leafHash
);
// Sign lender's CETs
const lenderLiquidationCetAdaptorSigs = await signCetAdaptorSignatures(
lenderDlcPrivateKey,
liquidationCets.map(cet => cet.outcomeSignaturePoint),
liquidationCets.map(cet => cet.cetTx),
leafHash
);
const lenderMaturityCetAdaptorSigs = await signCetAdaptorSignatures(
lenderDlcPrivateKey,
maturityCets.map(cet => cet.outcomeSignaturePoint),
maturityCets.map(cet => cet.cetTx),
leafHash
);
const lenderRepaymentCetAdaptorSig = await signCetWithAdaptorSig(
lenderDlcPrivateKey,
lenderRepaymentCommitment,
repaymentCet,
0,
leafHash
);
// The borrower and lender can now share eachothers sigs and validate them.
// Validate borrower's adaptor signatures
await validateAdaptorSignatures(
borrowerLiquidationCetAdaptorSigs,
borrowerDlcPubKey,
liquidationCets.map(cet => cet.outcomeSignaturePoint),
liquidationCets.map(cet => cet.cetTx),
leafHash
);
await validateAdaptorSignatures(
borrowerMaturityCetAdaptorSigs,
borrowerDlcPubKey,
maturityCets.map(cet => cet.outcomeSignaturePoint),
maturityCets.map(cet => cet.cetTx),
leafHash
);
const isBorrowerRepaymentCetAdaptorSigValid = await verifyCetAdaptorSig(
borrowerRepaymentCetAdaptorSig,
borrowerDlcPubKey,
lenderRepaymentCommitment,
repaymentCet,
0,
leafHash
);
expect(isBorrowerRepaymentCetAdaptorSigValid).to.be.true;
// Validate lender's adaptor signatures
await validateAdaptorSignatures(
lenderLiquidationCetAdaptorSigs,
lenderDlcPubKey,
liquidationCets.map(cet => cet.outcomeSignaturePoint),
liquidationCets.map(cet => cet.cetTx),
leafHash
);
await validateAdaptorSignatures(
lenderMaturityCetAdaptorSigs,
lenderDlcPubKey,
maturityCets.map(cet => cet.outcomeSignaturePoint),
maturityCets.map(cet => cet.cetTx),
leafHash
);
const isLenderRepaymentCetAdaptorSigValid = await verifyCetAdaptorSig(
lenderRepaymentCetAdaptorSig,
lenderDlcPubKey,
lenderRepaymentCommitment,
repaymentCet,
0,
leafHash
);
expect(isLenderRepaymentCetAdaptorSigValid).to.be.true;
// Now that everything is validated the lender can proceed with deploying the EVM repayment contract.
// Once confirmed, the borrower can sign and broadcast the DLC init tx.
// Signing of the DLC init txs inputs will be done by the users wallet.
// For example with Unisat wallet, we'll be using the signPsbt function (I think).
dlcInitTx.tx.sign(borrowerKey);
expect((dlcInitTx.tx as any).isFullySigned()).to.be.true;
// The borrower can now broadcast the DLC init tx.
// ...
///////////////////////
// CASE 1: REPAYMENT //
///////////////////////
// Once the lenderRepaymentSecret is revealed in the EVM contract, the borrower (or any of the parties)
// can finalize the adaptor signatures of the repayment CET.
const cetSighash = sighashForAdaptorSig(repaymentCet, 0, leafHash);
const finalizedBorrowerRepaymentCetAdaptorSig = await adaptSig(
borrowerRepaymentCetAdaptorSig,
BigInt('0x' + bytesToHex(lenderRepaymentSecret))
);
const finalizedLenderRepaymentCetAdaptorSig = await adaptSig(
lenderRepaymentCetAdaptorSig,
BigInt('0x' + bytesToHex(lenderRepaymentSecret))
);
const isBorrowerSigValid = await verifySigStrict(finalizedBorrowerRepaymentCetAdaptorSig, borrowerDlcPubKey, cetSighash);
const isLenderSigValid = await verifySigStrict(finalizedLenderRepaymentCetAdaptorSig, lenderDlcPubKey, cetSighash);
expect(isBorrowerSigValid).to.be.true;
expect(isLenderSigValid).to.be.true;
// Apply the finalized adaptor sigs to the CET tx.
applySignaturesCet(
repaymentCet,
dlcInitTx.witnessScript,
finalizedBorrowerRepaymentCetAdaptorSig,
finalizedLenderRepaymentCetAdaptorSig,
dlcInitTx.cBlock
);
// Create funding tx for the CET, update the CET with an input to unlock it and sign that input.
// In an actual app the funding UTXOs and signing would be facilitated by the users wallet.
const fundingUtxos = [{
txId: 'b'.repeat(64),
outputIndex: 0,
satoshis: 1000000,
script: (Script as any).buildWitnessV0Out(borrowerAddress)
}];
feeRate = 6;
const { cet } = fundCetFees(repaymentCet, fundingUtxos, borrowerAddress, borrowerAddress, feeRate);
cet.sign(borrowerKey);
// Now the funding and the CET tx are ready to be broadcast.
// Optionaly locally interpret script execution for the CET tx to see if it's valid.
const interpreter = new (Script as any).Interpreter();
const flags = (Script as any).Interpreter.SCRIPT_VERIFY_WITNESS | (Script as any).Interpreter.SCRIPT_VERIFY_TAPROOT;
const witnesses = (cet.inputs[0] as any).witnesses;
const res = interpreter.verify(
new Script(''),
dlcInitTx.tx.outputs[0].script,
cet,
0,
flags,
witnesses,
dlcInitTx.tx.outputs[0].satoshis
);
expect(res).to.be.true;
//////////////////////////
// CASE 2: ORACLE EVENT //
//////////////////////////
// If repayment doesn't happend before, then eventually the oracle will attest to an event outcome.
// This is either one of the liquidation events outcomes or the maturity event outcome.
const liquidationEvent = liquidationEvents[3];
const oracleSig = await attestEventOutcome(
oraclePrivKey,
liquidationEvent.nonce as bigint,
liquidationEvent.outcomeHashes[125]
);
// TODO: In the actual app here we would need to check if we created a liquidation cet for the exact outcome hash that the oracle attested to.
// The lender (or any of the parties) can now verify the oracle sig and find and finalize the CET.
// The related CET may be retrieved via the event ID reference.
let liquidationCet: OracleCET;
for (let i = 0; i < liquidationCets.length; i++) {
if (liquidationCets[i].eventId === liquidationEvent.id) {
liquidationCet = liquidationCets[i];
const cetSighash = sighashForAdaptorSig(liquidationCet.cetTx, 0, leafHash);
const borrowerLiquidationCetAdaptorSig = borrowerLiquidationCetAdaptorSigs[i];
const lenderLiquidationCetAdaptorSig = lenderLiquidationCetAdaptorSigs[i];
const finalizedBorrowerLiquidationCetAdaptorSig = await adaptSig(
borrowerLiquidationCetAdaptorSig,
oracleSig.s
);
const finalizedLenderLiquidationCetAdaptorSig = await adaptSig(
lenderLiquidationCetAdaptorSig,
oracleSig.s
);
const isBorrowerSigValid = await verifySigStrict(finalizedBorrowerLiquidationCetAdaptorSig, borrowerDlcPubKey, cetSighash);
const isLenderSigValid = await verifySigStrict(finalizedLenderLiquidationCetAdaptorSig, lenderDlcPubKey, cetSighash);
expect(isBorrowerSigValid).to.be.true;
expect(isLenderSigValid).to.be.true;
// Apply the finalized adaptor sigs to the CET tx.
applySignaturesCet(
liquidationCet.cetTx,
dlcInitTx.witnessScript,
finalizedBorrowerLiquidationCetAdaptorSig,
finalizedLenderLiquidationCetAdaptorSig,
dlcInitTx.cBlock
);
// Create funding tx for the CET, update the CET with an input to unlock it and sign that input.
const fundingUtxos = [{
txId: 'b'.repeat(64),
outputIndex: 0,
satoshis: 1000000,
script: (Script as any).buildWitnessV0Out(borrowerAddress)
}];
feeRate = 6;
const { cet } = fundCetFees(liquidationCet.cetTx, fundingUtxos, borrowerAddress, borrowerAddress, feeRate);
cet.sign(borrowerKey);
// Now the funding and the CET tx are ready to be broadcast.
// Optionaly locally interpret script execution for the CET tx to see if it's valid.
const interpreter = new (Script as any).Interpreter();
const flags = (Script as any).Interpreter.SCRIPT_VERIFY_WITNESS | (Script as any).Interpreter.SCRIPT_VERIFY_TAPROOT;
const witnesses = (cet.inputs[0] as any).witnesses;
const res = interpreter.verify(
new Script(''),
dlcInitTx.tx.outputs[0].script,
cet,
0,
flags,
witnesses,
dlcInitTx.tx.outputs[0].satoshis
);
expect(res).to.be.true;
break;
}
}
});
});