UNPKG

@covenance/dlc

Version:

Crypto and Bitcoin functions for Covenance DLC implementation

491 lines (412 loc) 17 kB
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; } } }); });