From a5b33ac6801fa9426ed32c41a253e9b1e6d2fe59 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Tue, 27 Jul 2021 15:10:13 +1000 Subject: [PATCH 1/7] Refactor collateral output descriptor into CollateralContract The intention is to explicitly model the central element of the loan protocol. The rest of the protocol boils down to funding and spending the collateral contract, something which we may be able to replace with the PSET protocol down the line. --- src/loan.rs | 346 ++++++++++++++++++++++--------------- src/loan/protocol_tests.rs | 1 + 2 files changed, 208 insertions(+), 139 deletions(-) diff --git a/src/loan.rs b/src/loan.rs index 16a5863..d08f76b 100644 --- a/src/loan.rs +++ b/src/loan.rs @@ -4,19 +4,18 @@ use anyhow::{anyhow, bail, Context, Result}; use bitcoin_hashes::hex::ToHex; use conquer_once::Lazy; use elements::bitcoin::{Amount, Network, PrivateKey, PublicKey}; -use elements::confidential::{Asset, AssetBlindingFactor, ValueBlindingFactor}; +use elements::confidential::{self, Asset, AssetBlindingFactor, ValueBlindingFactor}; use elements::encode::serialize; use elements::secp256k1_zkp::rand::{CryptoRng, RngCore}; use elements::secp256k1_zkp::{Secp256k1, SecretKey, Signing, Verification, SECP256K1}; use elements::sighash::SigHashCache; use elements::{ - Address, AddressParams, AssetId, OutPoint, SigHashType, Transaction, TxIn, TxInWitness, TxOut, - TxOutSecrets, + Address, AddressParams, AssetId, OutPoint, SigHashType, Transaction, TxIn, TxOut, TxOutSecrets, }; use elements_miniscript::descriptor::{CovSatisfier, ElementsTrait}; use elements_miniscript::miniscript::satisfy::After; use elements_miniscript::{Descriptor, DescriptorTrait}; -use secp256k1_zkp::{SurjectionProof, Tag}; +use secp256k1_zkp::{Signature, SurjectionProof, Tag}; use std::collections::HashMap; use std::future::Future; use std::str::FromStr; @@ -42,14 +41,15 @@ static COVENANT_SK: Lazy = Lazy::new(|| { /// transaction which inludes the input itself. static COVENANT_PK: &str = "03b9b6059008e3576aad58e05a3a3e37133b05f68cda8535ec097ef4bae564a6af"; -/// Generate the miniscript descriptor of the collateral output. +/// Contract defining the conditions under which the collateral output +/// can be spent. /// -/// It defines a "liquidation branch" which allows the lender to claim +/// It has a "liquidation branch" which allows the lender to claim /// all the collateral for themself if the `timelock` expires. The /// lender must identify themself by providing a signature on /// `lender_pk`. /// -/// It also defines a "repayment branch" which allows the borrower to +/// It also includes a "repayment branch" which allows the borrower to /// repay the loan to reclaim the collateral. The borrower must /// identify themself by providing a signature on `borrower_pk`. To /// ensure that the borrower does indeed repay the loan, the script @@ -60,21 +60,177 @@ static COVENANT_PK: &str = "03b9b6059008e3576aad58e05a3a3e37133b05f68cda8535ec09 /// `covenant_pk`, which is only used to verify that the transaction /// data on the witness stack matches the transaction which triggered /// the call. -fn collateral_descriptor( +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct CollateralContract { + descriptor: Descriptor, borrower_pk: PublicKey, lender_pk: PublicKey, - timelock: u64, - repayment_output: TxOut, -) -> Result> { - let repayment_output = serialize(&repayment_output).to_hex(); - let desc = Descriptor::::from_str( - &(format!( - "elcovwsh({},or_i(and_v(v:pk({}),after({})),and_v(v:pk({}),outputs_pref({}))))", - COVENANT_PK, lender_pk, timelock, borrower_pk, repayment_output, - )), - )?; - - Ok(desc) + timelock: u32, // TODO: Model timelocks as u32 +} + +impl CollateralContract { + /// Fill in the collateral contract template with the provided arguments. + fn new( + borrower_pk: PublicKey, + lender_pk: PublicKey, + timelock: u64, + repayment_output: TxOut, + ) -> Result { + let repayment_output = serialize(&repayment_output).to_hex(); + let descriptor = Descriptor::::from_str( + &(format!( + "elcovwsh({},or_i(and_v(v:pk({}),after({})),and_v(v:pk({}),outputs_pref({}))))", + COVENANT_PK, lender_pk, timelock, borrower_pk, repayment_output, + )), + ) + .context("invalid collateral output descriptor")?; + + Ok(Self { + descriptor, + borrower_pk, + lender_pk, + timelock: timelock as u32, + }) + } + + fn address(&self) -> Address { + // FIXME: AddressParams should not be hard-coded + self.descriptor + .address(&AddressParams::ELEMENTS) + .expect("descriptor address to exist") + } + + fn blinded_address(&self, blinder: secp256k1_zkp::PublicKey) -> Address { + self.descriptor + .blind_addr(Some(blinder), &AddressParams::ELEMENTS) + .expect("descriptor address to exist") + } + + async fn satisfy_loan_repayment( + &self, + transaction: &mut Transaction, + input_value: confidential::Value, + input_index: u32, + identity_signer: S, + ) -> Result<()> + where + S: FnOnce(secp256k1::Message) -> SF, + SF: Future>, + { + let descriptor_cov = &self.descriptor.as_cov().expect("covenant descriptor"); + + let cov_script = descriptor_cov.cov_script_code(); + let transaction_cloned = transaction.clone(); + let cov_sat = CovSatisfier::new_segwitv0( + &transaction_cloned, + input_index, + input_value, + &cov_script, + SigHashType::All, + ); + + let cov_pk_sat = { + let mut hash_map = HashMap::new(); + let sighash = cov_sat.segwit_sighash()?; + let sighash = elements::secp256k1_zkp::Message::from(sighash); + + let sig = SECP256K1.sign(&sighash, &COVENANT_SK); + hash_map.insert(*descriptor_cov.pk(), (sig, SigHashType::All)); + + hash_map + }; + + let ident_pk_sat = { + let mut hash_map = HashMap::new(); + + let script = &self.descriptor.explicit_script(); + let sighash = SigHashCache::new(&*transaction).segwitv0_sighash( + input_index as usize, + &script, + input_value, + SigHashType::All, + ); + let sighash = elements::secp256k1_zkp::Message::from(sighash); + + let sig = identity_signer(sighash) + .await + .context("could not sign on behalf of borrower")?; + hash_map.insert(self.borrower_pk, (sig, SigHashType::All)); + + hash_map + }; + + self.descriptor.satisfy( + &mut transaction.input[input_index as usize], + (cov_sat, cov_pk_sat, ident_pk_sat), + )?; + + Ok(()) + } + + async fn satisfy_liquidation( + &self, + transaction: &mut Transaction, + input_value: confidential::Value, + input_index: u32, + identity_signer: S, + ) -> Result<()> + where + S: FnOnce(secp256k1::Message) -> SF, + SF: Future>, + { + let descriptor_cov = &self.descriptor.as_cov().expect("covenant descriptor"); + + let cov_script = descriptor_cov.cov_script_code(); + let transaction_cloned = transaction.clone(); + let cov_sat = CovSatisfier::new_segwitv0( + &transaction_cloned, + input_index, + input_value, + &cov_script, + SigHashType::All, + ); + + let cov_pk_sat = { + let mut hash_map = HashMap::new(); + let sighash = cov_sat.segwit_sighash()?; + let sighash = elements::secp256k1_zkp::Message::from(sighash); + + let sig = SECP256K1.sign(&sighash, &COVENANT_SK); + hash_map.insert(*descriptor_cov.pk(), (sig, SigHashType::All)); + + hash_map + }; + + let ident_pk_sat = { + let mut hash_map = HashMap::new(); + + let script = &self.descriptor.explicit_script(); + let sighash = SigHashCache::new(&*transaction).segwitv0_sighash( + input_index as usize, + &script, + input_value, + SigHashType::All, + ); + let sighash = elements::secp256k1_zkp::Message::from(sighash); + + let sig = identity_signer(sighash) + .await + .context("could not sign on behalf of lender")?; + hash_map.insert(self.lender_pk, (sig, SigHashType::All)); + + hash_map + }; + + let after_sat = After(self.timelock as u32); + + self.descriptor.satisfy( + &mut transaction.input[input_index as usize], + (cov_sat, cov_pk_sat, ident_pk_sat, after_sat), + )?; + + Ok(()) + } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -187,13 +343,13 @@ impl Borrower0 { }) .context("no principal txout")?; - let collateral_descriptor = collateral_descriptor( + let collateral_contract = CollateralContract::new( self.keypair.1, loan_response.lender_pk, loan_response.timelock, loan_response.repayment_principal_output.clone(), )?; - let collateral_address = collateral_descriptor.address(&AddressParams::ELEMENTS)?; + let collateral_address = collateral_contract.address(); let collateral_script_pubkey = collateral_address.script_pubkey(); let collateral_blinding_sk = loan_response.repayment_collateral_input.blinding_key; @@ -250,7 +406,7 @@ impl Borrower0 { keypair: self.keypair, loan_transaction: transaction, collateral_amount: self.collateral_amount, - collateral_descriptor, + collateral_contract, principal_tx_out_amount, address: self.address.clone(), repayment_collateral_input: loan_response.repayment_collateral_input, @@ -269,7 +425,7 @@ pub struct Borrower1 { pub loan_transaction: Transaction, #[serde(with = "::elements::bitcoin::util::amount::serde::as_sat")] pub collateral_amount: Amount, - collateral_descriptor: Descriptor, + collateral_contract: CollateralContract, #[serde(with = "::elements::bitcoin::util::amount::serde::as_sat")] pub principal_tx_out_amount: Amount, address: Address, @@ -450,59 +606,20 @@ impl Borrower1 { output: tx_outs, }; - // fulfill collateral input covenant script - { - let descriptor = self.collateral_descriptor.clone(); - let descriptor_cov = descriptor.as_cov()?; - - let collateral_value = self.repayment_collateral_input.original_txout.value; - let cov_script = descriptor_cov.cov_script_code(); - - let cov_sat = - CovSatisfier::new_segwitv0(&tx, 1, collateral_value, &cov_script, SigHashType::All); - - let cov_pk_sat = { - let mut hash_map = HashMap::new(); - let sighash = cov_sat.segwit_sighash()?; - let sighash = elements::secp256k1_zkp::Message::from(sighash); - - let sig = SECP256K1.sign(&sighash, &COVENANT_SK); - hash_map.insert(*descriptor_cov.pk(), (sig, SigHashType::All)); - - hash_map - }; - - let ident_pk_sat = { - let mut hash_map = HashMap::new(); - - let script = descriptor.explicit_script(); - let sighash = SigHashCache::new(&tx).segwitv0_sighash( - 1, - &script, - collateral_value, - SigHashType::All, - ); - let sighash = elements::secp256k1_zkp::Message::from(sighash); - - let sig = SECP256K1.sign(&sighash, &self.keypair.0); - hash_map.insert(self.keypair.1, (sig, SigHashType::All)); - - hash_map - }; - - let (script_witness, _) = - descriptor_cov.get_satisfaction((cov_sat, cov_pk_sat, ident_pk_sat))?; - - tx.input[1].witness = TxInWitness { - amount_rangeproof: None, - inflation_keys_rangeproof: None, - script_witness, - pegin_witness: vec![], - }; - }; + // fulfill collateral input contract + self.collateral_contract + .satisfy_loan_repayment( + &mut tx, + self.repayment_collateral_input.original_txout.value, + 1, + // TODO: Push ownership (and generation) of secret key outside of the library + |message| async move { Ok(SECP256K1.sign(&message, &self.keypair.0)) }, + ) + .await + .context("could not satisfy collateral input")?; // sign repayment input of the principal amount - let tx = { signer(tx).await? }; + let tx = signer(tx).await?; Ok(tx) } @@ -605,15 +722,14 @@ impl Lender0 { let (_, lender_pk) = self.keypair; let (collateral_blinding_sk, collateral_blinding_pk) = make_keypair(rng); - let collateral_descriptor = collateral_descriptor( + let collateral_contract = CollateralContract::new( loan_request.borrower_pk, lender_pk, loan_request.timelock, repayment_principal_output.clone(), ) - .context("could not build collateral descriptor")?; - let collateral_address = collateral_descriptor - .blind_addr(Some(collateral_blinding_pk.key), &AddressParams::ELEMENTS)?; + .context("could not build collateral contract")?; + let collateral_address = collateral_contract.blinded_address(collateral_blinding_pk.key); let inputs_not_last_confidential = inputs .iter() @@ -758,7 +874,7 @@ impl Lender0 { address: self.address, timelock: loan_request.timelock, loan_transaction, - collateral_descriptor, + collateral_contract, collateral_amount: loan_request.collateral_amount, repayment_collateral_input, repayment_collateral_abf, @@ -792,7 +908,7 @@ pub struct Lender1 { address: Address, pub timelock: u64, loan_transaction: Transaction, - collateral_descriptor: Descriptor, + collateral_contract: CollateralContract, collateral_amount: Amount, repayment_collateral_input: Input, repayment_collateral_abf: AssetBlindingFactor, @@ -830,7 +946,7 @@ impl Lender1 { signer(loan_transaction).await } - pub fn liquidation_transaction( + pub async fn liquidation_transaction( &self, rng: &mut R, secp: &Secp256k1, @@ -884,64 +1000,16 @@ impl Lender1 { output: tx_outs, }; - // fulfill collateral input covenant script to liquidate the position - { - let descriptor = self.collateral_descriptor.clone(); - let descriptor_cov = descriptor.as_cov()?; - - let collateral_value = self.repayment_collateral_input.original_txout.value; - let cov_script = descriptor_cov.cov_script_code(); - - let cov_sat = CovSatisfier::new_segwitv0( - &liquidation_transaction, + // fulfill collateral input contract + self.collateral_contract + .satisfy_liquidation( + &mut liquidation_transaction, + self.repayment_collateral_input.original_txout.value, 0, - collateral_value, - &cov_script, - SigHashType::All, - ); - - let cov_pk_sat = { - let mut hash_map = HashMap::new(); - let sighash = cov_sat.segwit_sighash()?; - - let sig = SECP256K1.sign( - &elements::secp256k1_zkp::Message::from(sighash), - &COVENANT_SK, - ); - hash_map.insert(*descriptor_cov.pk(), (sig, SigHashType::All)); - - hash_map - }; - - let ident_pk_sat = { - let mut hash_map = HashMap::new(); - - let script = descriptor.explicit_script(); - let sighash = SigHashCache::new(&liquidation_transaction).segwitv0_sighash( - 0, - &script, - collateral_value, - SigHashType::All, - ); - let sighash = elements::secp256k1_zkp::Message::from(sighash); - - let sig = SECP256K1.sign(&sighash, &self.keypair.0); - hash_map.insert(self.keypair.1, (sig, SigHashType::All)); - - hash_map - }; - - // TODO: Model timelocks as u32 - let after_sat = After(self.timelock as u32); - - let (script_witness, _) = self.collateral_descriptor.get_satisfaction(( - cov_sat, - cov_pk_sat, - ident_pk_sat, - after_sat, - ))?; - liquidation_transaction.input[0].witness.script_witness = script_witness; - } + |message| async move { Ok(SECP256K1.sign(&message, &self.keypair.0)) }, + ) + .await + .context("could not satisfy collateral input")?; Ok(liquidation_transaction) } diff --git a/src/loan/protocol_tests.rs b/src/loan/protocol_tests.rs index dbe859d..5ec0dcc 100644 --- a/src/loan/protocol_tests.rs +++ b/src/loan/protocol_tests.rs @@ -297,6 +297,7 @@ mod tests { let liquidation_transaction = lender .liquidation_transaction(&mut thread_rng(), &SECP256K1, Amount::from_sat(1)) + .await .unwrap(); client From 083a2b1a0f8e0e833b23ab5afbc2a78bc18ac44f Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Tue, 27 Jul 2021 23:38:37 +1000 Subject: [PATCH 2/7] Fix RNG in test for better debugging We have to depend on particular versions of `rand` and `rand_chacha` to make this happen. --- Cargo.toml | 2 ++ src/loan.rs | 2 +- src/loan/protocol_tests.rs | 49 +++++++++++++++++++------------------- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 53219d0..1b0eeac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ env_logger = "0.8.3" hex = "0.4" hmac = "0.10" log = "0.4" +rand = "0.6" rust_decimal = "1" secp256k1 = { version = "0.20", features = [ "bitcoin_hashes", "rand" ] } # TODO: See if we can remove this direct dependency @@ -30,5 +31,6 @@ thiserror = "1" [dev-dependencies] elements-harness = { git = "https://github.com/comit-network/elements-harness" } elements-rpc = { path = "./elements-rpc" } +rand_chacha = "0.1" testcontainers = "0.12" tokio = { version = "1", default-features = false, features = [ "macros" ] } diff --git a/src/loan.rs b/src/loan.rs index d08f76b..3399130 100644 --- a/src/loan.rs +++ b/src/loan.rs @@ -6,7 +6,6 @@ use conquer_once::Lazy; use elements::bitcoin::{Amount, Network, PrivateKey, PublicKey}; use elements::confidential::{self, Asset, AssetBlindingFactor, ValueBlindingFactor}; use elements::encode::serialize; -use elements::secp256k1_zkp::rand::{CryptoRng, RngCore}; use elements::secp256k1_zkp::{Secp256k1, SecretKey, Signing, Verification, SECP256K1}; use elements::sighash::SigHashCache; use elements::{ @@ -15,6 +14,7 @@ use elements::{ use elements_miniscript::descriptor::{CovSatisfier, ElementsTrait}; use elements_miniscript::miniscript::satisfy::After; use elements_miniscript::{Descriptor, DescriptorTrait}; +use rand::{CryptoRng, RngCore}; use secp256k1_zkp::{Signature, SurjectionProof, Tag}; use std::collections::HashMap; use std::future::Future; diff --git a/src/loan/protocol_tests.rs b/src/loan/protocol_tests.rs index 5ec0dcc..26f4a35 100644 --- a/src/loan/protocol_tests.rs +++ b/src/loan/protocol_tests.rs @@ -15,12 +15,16 @@ mod tests { }; use elements_harness::Elementsd; use elements_rpc::ElementsRpc; + use rand::{CryptoRng, RngCore, SeedableRng}; + use rand_chacha::ChaChaRng; use testcontainers::clients::Cli; #[tokio::test] async fn borrow_and_repay() { init_logger(); + let mut rng = ChaChaRng::seed_from_u64(0); + let tc_client = Cli::default(); let (client, _container) = { let blockchain = Elementsd::new(&tc_client, "0.18.1.9").unwrap(); @@ -43,7 +47,7 @@ mod tests { client.generatetoaddress(10, &miner_address).await.unwrap(); let (borrower, borrower_wallet) = { - let mut wallet = Wallet::new(); + let mut wallet = Wallet::new(&mut rng); let collateral_amount = Amount::ONE_BTC; @@ -74,7 +78,7 @@ mod tests { let timelock = 10; let borrower = Borrower0::new( - &mut thread_rng(), + &mut rng, { let wallet = wallet.clone(); |amount, asset| async move { wallet.find_inputs(asset, amount).await } @@ -96,13 +100,8 @@ mod tests { let (lender, _lender_address) = { let address = client.get_new_segwit_confidential_address().await.unwrap(); - let lender = Lender0::new( - &mut thread_rng(), - bitcoin_asset_id, - usdt_asset_id, - address.clone(), - ) - .unwrap(); + let lender = + Lender0::new(&mut rng, bitcoin_asset_id, usdt_asset_id, address.clone()).unwrap(); (lender, address) }; @@ -111,7 +110,7 @@ mod tests { let lender = lender .interpret( - &mut thread_rng(), + &mut rng, &SECP256K1, { let client = client.clone(); @@ -150,7 +149,7 @@ mod tests { let loan_repayment_transaction = borrower .loan_repayment_transaction( - &mut thread_rng(), + &mut rng, &SECP256K1, { let borrower_wallet = borrower_wallet.clone(); @@ -172,6 +171,8 @@ mod tests { async fn lend_and_liquidate() { init_logger(); + let mut rng = ChaChaRng::seed_from_u64(0); + let tc_client = Cli::default(); let (client, _container) = { let blockchain = Elementsd::new(&tc_client, "0.18.1.9").unwrap(); @@ -193,7 +194,7 @@ mod tests { client.generatetoaddress(10, &miner_address).await.unwrap(); let (borrower, borrower_wallet) = { - let mut wallet = Wallet::new(); + let mut wallet = Wallet::new(&mut rng); let collateral_amount = Amount::ONE_BTC; @@ -224,7 +225,7 @@ mod tests { let timelock = client.get_blockcount().await.unwrap() + 2; let borrower = Borrower0::new( - &mut thread_rng(), + &mut rng, { let wallet = wallet.clone(); |amount, asset| async move { wallet.find_inputs(asset, amount).await } @@ -246,13 +247,8 @@ mod tests { let (lender, _lender_address) = { let address = client.get_new_segwit_confidential_address().await.unwrap(); - let lender = Lender0::new( - &mut thread_rng(), - bitcoin_asset_id, - usdt_asset_id, - address.clone(), - ) - .unwrap(); + let lender = + Lender0::new(&mut rng, bitcoin_asset_id, usdt_asset_id, address.clone()).unwrap(); (lender, address) }; @@ -261,7 +257,7 @@ mod tests { let lender = lender .interpret( - &mut thread_rng(), + &mut rng, &SECP256K1, { let client = client.clone(); @@ -296,7 +292,7 @@ mod tests { client.generatetoaddress(2, &miner_address).await.unwrap(); let liquidation_transaction = lender - .liquidation_transaction(&mut thread_rng(), &SECP256K1, Amount::from_sat(1)) + .liquidation_transaction(&mut rng, &SECP256K1, Amount::from_sat(1)) .await .unwrap(); @@ -364,9 +360,12 @@ mod tests { } impl Wallet { - pub fn new() -> Self { - let (sk, pk) = make_keypair(&mut thread_rng()); - let (blinder_sk, blinder_pk) = make_keypair(&mut thread_rng()); + pub fn new(rng: &mut R) -> Self + where + R: RngCore + CryptoRng, + { + let (sk, pk) = make_keypair(rng); + let (blinder_sk, blinder_pk) = make_keypair(rng); let address = Address::p2wpkh(&pk, Some(blinder_pk.key), &AddressParams::ELEMENTS); From fd0f09bc30894b30b9d19f183edc72c4c4c27adf Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Wed, 28 Jul 2021 00:39:07 +1000 Subject: [PATCH 3/7] Extend loan protocol with dynamic liquidation Give the lender the ability to liquidate the collateral if they can prove that its value has fallen below a predetermined threshold. The proof is composed of a message and a signature on the message from a known, trusted oracle. The oracle API is only loosely defined here, in the form of a crate-local module used for testing. --- CHANGELOG.md | 7 + src/loan.rs | 631 ++++++++++++++++++++++++++++++------- src/loan/protocol_tests.rs | 331 ++++++++++++++++++- 3 files changed, 836 insertions(+), 133 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6443c1f..2377326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Dynamic liquidation branch to the loan protocol: + - New `async fn dynamic_liquidation_transaction()` API on `Lender1`. + It takes a message of the form `price:timestamp` and an oracle's signature on the hash of the message, so that a _lender_ can unilaterally liquidate the loan if the `price` falls below a threshold and the `timestamp` is past a certain time. + - `Lender0` constructor now requires blinding key of lender address and an oracle public key. + ## [0.1.1] - 2021-07-23 ## [0.1.0] - 2021-07-16 diff --git a/src/loan.rs b/src/loan.rs index 3399130..65df40f 100644 --- a/src/loan.rs +++ b/src/loan.rs @@ -6,19 +6,23 @@ use conquer_once::Lazy; use elements::bitcoin::{Amount, Network, PrivateKey, PublicKey}; use elements::confidential::{self, Asset, AssetBlindingFactor, ValueBlindingFactor}; use elements::encode::serialize; -use elements::secp256k1_zkp::{Secp256k1, SecretKey, Signing, Verification, SECP256K1}; +use elements::pset::serialize::Serialize; +use elements::script::Builder; +use elements::secp256k1_zkp::{self, Secp256k1, SecretKey, Signing, Verification, SECP256K1}; use elements::sighash::SigHashCache; use elements::{ - Address, AddressParams, AssetId, OutPoint, SigHashType, Transaction, TxIn, TxOut, TxOutSecrets, + Address, AddressParams, AssetId, OutPoint, Script, SigHashType, Transaction, TxIn, TxInWitness, + TxOut, TxOutSecrets, }; -use elements_miniscript::descriptor::{CovSatisfier, ElementsTrait}; +use elements_miniscript::descriptor::CovSatisfier; use elements_miniscript::miniscript::satisfy::After; -use elements_miniscript::{Descriptor, DescriptorTrait}; +use elements_miniscript::{Descriptor, DescriptorTrait, Satisfier}; use rand::{CryptoRng, RngCore}; use secp256k1_zkp::{Signature, SurjectionProof, Tag}; use std::collections::HashMap; use std::future::Future; use std::str::FromStr; +use std::time::SystemTime; #[cfg(test)] mod protocol_tests; @@ -54,18 +58,44 @@ static COVENANT_PK: &str = "03b9b6059008e3576aad58e05a3a3e37133b05f68cda8535ec09 /// identify themself by providing a signature on `borrower_pk`. To /// ensure that the borrower does indeed repay the loan, the script /// will check that the spending transaction has the -/// `repayment_output` as vout 0. +/// `repayment_principal_output` as vout 0. /// -/// The first element of the covenant descriptor is the shared -/// `covenant_pk`, which is only used to verify that the transaction -/// data on the witness stack matches the transaction which triggered -/// the call. +/// Additionally, the contract can be spent by providing a signature +/// (and the corresponding message) from a valid oracle proving that +/// the price of the collateral has dropped below a threshold +/// determined at contract creation time. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -struct CollateralContract { +pub struct CollateralContract { + /// Only includes the two branches that can be expressed using miniscript: + /// + /// - Liquidation after expiration. + /// + /// - Collateral reclamation after repayment. + /// + /// Kept around to more easily construct the witness stack for + /// those two branches. descriptor: Descriptor, + /// The actual script, including the dynamic liquidation branch. + raw_script: Script, + /// The script after `OP_CODESEPARATOR`. + /// + /// This is the only part of the script which is included in the + /// transaction sighash which is signed with `COVENANT_SK`. + /// + /// In a regular `CovenantDescriptor` this is just + /// `OP_CHECKSIGVERIFY` and `OP_CHECKSIGFROMSTACK`. In our case, + /// it's `OP_CHECKSIGVERIFY`, `OP_CHECKSIGFROMSTACK` and + /// `OP_ENDIF`. + cov_script: Script, borrower_pk: PublicKey, lender_pk: PublicKey, - timelock: u32, // TODO: Model timelocks as u32 + repayment_principal_output: TxOut, + repayment_principal_output_blinder: SecretKey, + timelock: u32, + // Dynamic liquidation + oracle_pk: PublicKey, + min_price_btc: u64, + min_timestamp: u64, } impl CollateralContract { @@ -74,38 +104,106 @@ impl CollateralContract { borrower_pk: PublicKey, lender_pk: PublicKey, timelock: u64, - repayment_output: TxOut, + (repayment_principal_output, repayment_principal_output_blinder): (TxOut, SecretKey), + oracle_pk: PublicKey, + min_price_btc: u64, + min_timestamp: u64, ) -> Result { - let repayment_output = serialize(&repayment_output).to_hex(); - let descriptor = Descriptor::::from_str( - &(format!( - "elcovwsh({},or_i(and_v(v:pk({}),after({})),and_v(v:pk({}),outputs_pref({}))))", - COVENANT_PK, lender_pk, timelock, borrower_pk, repayment_output, - )), + use elements::opcodes::all::*; + + let repayment_principal_output_as_hex = serialize(&repayment_principal_output).to_hex(); + + // the first element of the covenant descriptor is the shared + // `covenant_pk`, which is only used to verify that the transaction + // data on the witness stack matches the transaction which triggered + // the call. + let descriptor: Descriptor = format!( + "elcovwsh({},or_i(and_v(v:pk({}),after({})),and_v(v:pk({}),outputs_pref({}))))", + COVENANT_PK, lender_pk, timelock, borrower_pk, repayment_principal_output_as_hex, ) + .parse() .context("invalid collateral output descriptor")?; + let dynamic_liquidation_branch = Builder::new() + // check that the lender authorises the spend + .push_slice(lender_pk.serialize().as_slice()) + .push_opcode(OP_CHECKSIGVERIFY) + // copy message BTC price to top of stack + .push_int(2) + .push_opcode(OP_PICK) + // copy message timestamp to top of stack + .push_int(2) + .push_opcode(OP_PICK) + // reconstruct message as `timestamp:btc_price` + .push_opcode(OP_CAT) + // check if oracle has approved of the hashed message (OP_CSFS + // implicitly hashes the message) + .push_slice(oracle_pk.serialize().as_slice()) + .push_opcode(OP_CHECKSIGFROMSTACKVERIFY) + // check if message is recent enough + .push_int(min_timestamp as i64) + .push_opcode(OP_GREATERTHANOREQUAL) + .push_opcode(OP_VERIFY) + // check if BTC price has dipped below minimum + .push_int(min_price_btc as i64) + .push_opcode(OP_LESSTHANOREQUAL) + .into_script(); + + // add dynamic liquidation branch, making the script look like: + // OP_IF OP_ELSE OP_ENDIF + let raw_script = { + let mut script = vec![OP_IF.into_u8()]; + script.append(&mut dynamic_liquidation_branch.to_bytes()); + script.push(OP_ELSE.into_u8()); + + let mut desc_script = descriptor.explicit_script().to_bytes(); + script.append(&mut desc_script); + + script.push(OP_ENDIF.into_u8()); + + script + }; + + // remove length check for the size of the script included in + // the sighash, because after adding the dynamic liquidation + // branch it no longer holds. We could alternatively replace + // the expected value `3` with `4`, since the rest of the + // script after the `OP_CODESEPARATOR` is `OP_CHECKSIGVERIFY`, + // `OP_CHECKSIGFROMSTACK` and `OP_ENDIF`. + let raw_script = { + let mut script = raw_script; + + let pos = script + .windows(3) + .position(|window| window == [0x82u8, 0x53u8, 0x88u8]) + .expect("OP_SIZE OP_PUSHNUM_3 OP_EQUALVERIFY section to exist"); + + let _ = script.drain(pos..pos + 3); + + Script::from(script) + }; + + let cov_script = Builder::new() + .push_opcode(OP_CHECKSIGVERIFY) + .push_opcode(OP_CHECKSIGFROMSTACK) + .push_opcode(OP_ENDIF) + .into_script(); + Ok(Self { descriptor, + raw_script, + cov_script, borrower_pk, lender_pk, - timelock: timelock as u32, + repayment_principal_output, + repayment_principal_output_blinder, + timelock: timelock as u32, // TODO: Model timelocks as u32 + oracle_pk, + min_price_btc, + min_timestamp, }) } - fn address(&self) -> Address { - // FIXME: AddressParams should not be hard-coded - self.descriptor - .address(&AddressParams::ELEMENTS) - .expect("descriptor address to exist") - } - - fn blinded_address(&self, blinder: secp256k1_zkp::PublicKey) -> Address { - self.descriptor - .blind_addr(Some(blinder), &AddressParams::ELEMENTS) - .expect("descriptor address to exist") - } - async fn satisfy_loan_repayment( &self, transaction: &mut Transaction, @@ -117,74 +215,125 @@ impl CollateralContract { S: FnOnce(secp256k1::Message) -> SF, SF: Future>, { - let descriptor_cov = &self.descriptor.as_cov().expect("covenant descriptor"); - - let cov_script = descriptor_cov.cov_script_code(); let transaction_cloned = transaction.clone(); - let cov_sat = CovSatisfier::new_segwitv0( - &transaction_cloned, - input_index, - input_value, - &cov_script, - SigHashType::All, - ); - - let cov_pk_sat = { - let mut hash_map = HashMap::new(); - let sighash = cov_sat.segwit_sighash()?; - let sighash = elements::secp256k1_zkp::Message::from(sighash); - - let sig = SECP256K1.sign(&sighash, &COVENANT_SK); - hash_map.insert(*descriptor_cov.pk(), (sig, SigHashType::All)); + let satisfiers = self + .descriptor_satisfiers( + identity_signer, + &transaction_cloned, + input_value, + input_index, + self.borrower_pk, + ) + .await?; - hash_map - }; + self.satisfy( + satisfiers, + &mut transaction.input[input_index as usize].witness, + )?; - let ident_pk_sat = { - let mut hash_map = HashMap::new(); + Ok(()) + } - let script = &self.descriptor.explicit_script(); - let sighash = SigHashCache::new(&*transaction).segwitv0_sighash( - input_index as usize, - &script, + async fn satisfy_liquidation( + &self, + transaction: &mut Transaction, + input_value: confidential::Value, + input_index: u32, + identity_signer: S, + ) -> Result<()> + where + S: FnOnce(secp256k1::Message) -> SF, + SF: Future>, + { + let transaction_cloned = transaction.clone(); + let satisfiers = self + .descriptor_satisfiers( + identity_signer, + &transaction_cloned, input_value, - SigHashType::All, - ); - let sighash = elements::secp256k1_zkp::Message::from(sighash); - - let sig = identity_signer(sighash) - .await - .context("could not sign on behalf of borrower")?; - hash_map.insert(self.borrower_pk, (sig, SigHashType::All)); - - hash_map - }; + input_index, + self.lender_pk, + ) + .await?; + let after_sat = After(self.timelock as u32); - self.descriptor.satisfy( - &mut transaction.input[input_index as usize], - (cov_sat, cov_pk_sat, ident_pk_sat), + self.satisfy( + (satisfiers, after_sat), + &mut transaction.input[input_index as usize].witness, )?; Ok(()) } - async fn satisfy_liquidation( + async fn satisfy_dynamic_liquidation( &self, + identity_signer: S, transaction: &mut Transaction, input_value: confidential::Value, input_index: u32, - identity_signer: S, + oracle_msg: oracle::Message, + oracle_sig: Signature, ) -> Result<()> + where + S: FnOnce(secp256k1::Message) -> SF, + SF: Future>, + { + let btc_price = oracle_msg.price_to_bytes(); + let timestamp = oracle_msg.timestamp_to_bytes(); + let oracle_sig = oracle_sig.serialize_der().to_vec(); + + let script = &self.raw_script; + let sighash = SigHashCache::new(&*transaction).segwitv0_sighash( + input_index as usize, + &script, + input_value, + SigHashType::All, + ); + let sighash = secp256k1_zkp::Message::from(sighash); + + let identity_sig = identity_signer(sighash) + .await + .context("could not sign on behalf of lender")?; + let mut identity_sig = identity_sig.serialize_der().to_vec(); + identity_sig.push(SigHashType::All as u8); + + let if_flag = vec![0x01]; + let script = self.raw_script.to_bytes(); + + transaction.input[input_index as usize] + .witness + .script_witness = vec![ + btc_price, + timestamp, + oracle_sig, + identity_sig, + if_flag, + script, + ]; + + Ok(()) + } + + /// Construct the satisfiers which are always required to fulfill + /// the requirements of the covenant descriptor part of the + /// contract. + async fn descriptor_satisfiers<'a, S, SF>( + &'a self, + identity_signer: S, + transaction: &'a Transaction, + input_value: confidential::Value, + input_index: u32, + identity_pk: PublicKey, + ) -> Result + 'a> where S: FnOnce(secp256k1::Message) -> SF, SF: Future>, { let descriptor_cov = &self.descriptor.as_cov().expect("covenant descriptor"); - let cov_script = descriptor_cov.cov_script_code(); - let transaction_cloned = transaction.clone(); + let cov_script = &self.cov_script; let cov_sat = CovSatisfier::new_segwitv0( - &transaction_cloned, + &transaction, input_index, input_value, &cov_script, @@ -194,7 +343,7 @@ impl CollateralContract { let cov_pk_sat = { let mut hash_map = HashMap::new(); let sighash = cov_sat.segwit_sighash()?; - let sighash = elements::secp256k1_zkp::Message::from(sighash); + let sighash = secp256k1_zkp::Message::from(sighash); let sig = SECP256K1.sign(&sighash, &COVENANT_SK); hash_map.insert(*descriptor_cov.pk(), (sig, SigHashType::All)); @@ -205,32 +354,77 @@ impl CollateralContract { let ident_pk_sat = { let mut hash_map = HashMap::new(); - let script = &self.descriptor.explicit_script(); + let script = &self.raw_script; let sighash = SigHashCache::new(&*transaction).segwitv0_sighash( input_index as usize, &script, input_value, SigHashType::All, ); - let sighash = elements::secp256k1_zkp::Message::from(sighash); + let sighash = secp256k1_zkp::Message::from(sighash); let sig = identity_signer(sighash) .await .context("could not sign on behalf of lender")?; - hash_map.insert(self.lender_pk, (sig, SigHashType::All)); + hash_map.insert(identity_pk, (sig, SigHashType::All)); hash_map }; - let after_sat = After(self.timelock as u32); + Ok((cov_sat, cov_pk_sat, ident_pk_sat)) + } - self.descriptor.satisfy( - &mut transaction.input[input_index as usize], - (cov_sat, cov_pk_sat, ident_pk_sat, after_sat), - )?; + /// Satisfy the covenant descriptor based on the satisfier + /// provided, and modify the value returned by + /// `elements-miniscript` to account for the rest of the contract. + /// + /// As you can see from the suspicious code below, this is + /// extremely hacky. We would like to just use + /// `elements-minscript` but it is not (yet) possible to express + /// all of the spending conditions we want. + fn satisfy(&self, satisfier: S, input_witness: &mut TxInWitness) -> Result<()> + where + S: Satisfier, + { + let (mut script_witness, _) = self.descriptor.get_satisfaction(satisfier)?; + + { + script_witness.pop(); + + let if_flag = vec![]; + script_witness.push(if_flag); + script_witness.push(self.raw_script.to_bytes()) + }; + + input_witness.script_witness = script_witness; Ok(()) } + + fn address(&self) -> Address { + // FIXME: AddressParams should not be hard-coded + Address::p2wsh(&self.raw_script, None, &AddressParams::ELEMENTS) + } + + fn blinded_address(&self, blinder: secp256k1_zkp::PublicKey) -> Address { + Address::p2wsh(&self.raw_script, Some(blinder), &AddressParams::ELEMENTS) + } + + fn repayment_amount(&self, secp: &Secp256k1) -> Result + where + C: Verification, + { + let TxOutSecrets { value, .. } = self + .repayment_principal_output + .unblind(secp, self.repayment_principal_output_blinder)?; + + Ok(Amount::from_sat(value)) + } + + /// Get a reference to the collateral contract's timelock. + pub fn timelock(&self) -> &u32 { + &self.timelock + } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -250,12 +444,10 @@ pub struct LoanResponse { // TODO: Use this where needed! #[serde(with = "transaction_as_string")] pub transaction: Transaction, - lender_pk: PublicKey, + collateral_contract: CollateralContract, repayment_collateral_input: Input, repayment_collateral_abf: AssetBlindingFactor, repayment_collateral_vbf: ValueBlindingFactor, - pub timelock: u64, - repayment_principal_output: TxOut, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -331,7 +523,7 @@ impl Borrower0 { { let transaction = loan_response.transaction; - let principal_tx_out_amount = transaction + let principal_amount = transaction .output .iter() .find_map(|out| { @@ -343,16 +535,12 @@ impl Borrower0 { }) .context("no principal txout")?; - let collateral_contract = CollateralContract::new( - self.keypair.1, - loan_response.lender_pk, - loan_response.timelock, - loan_response.repayment_principal_output.clone(), - )?; + let collateral_contract = loan_response.collateral_contract; let collateral_address = collateral_contract.address(); - let collateral_script_pubkey = collateral_address.script_pubkey(); let collateral_blinding_sk = loan_response.repayment_collateral_input.blinding_key; + let collateral_script_pubkey = collateral_address.script_pubkey(); + transaction .output .iter() @@ -407,14 +595,13 @@ impl Borrower0 { loan_transaction: transaction, collateral_amount: self.collateral_amount, collateral_contract, - principal_tx_out_amount, + principal_amount, address: self.address.clone(), repayment_collateral_input: loan_response.repayment_collateral_input, repayment_collateral_abf: loan_response.repayment_collateral_abf, repayment_collateral_vbf: loan_response.repayment_collateral_vbf, bitcoin_asset_id: self.bitcoin_asset_id, usdt_asset_id: self.usdt_asset_id, - repayment_principal_output: loan_response.repayment_principal_output, }) } } @@ -427,7 +614,7 @@ pub struct Borrower1 { pub collateral_amount: Amount, collateral_contract: CollateralContract, #[serde(with = "::elements::bitcoin::util::amount::serde::as_sat")] - pub principal_tx_out_amount: Amount, + pub principal_amount: Amount, address: Address, /// Loan collateral expressed as an input for constructing the /// loan repayment transaction. @@ -436,7 +623,6 @@ pub struct Borrower1 { repayment_collateral_vbf: ValueBlindingFactor, bitcoin_asset_id: AssetId, usdt_asset_id: AssetId, - repayment_principal_output: TxOut, } impl Borrower1 { @@ -464,15 +650,17 @@ impl Borrower1 { SI: FnOnce(Transaction) -> SF, SF: Future>, { - let repayment_amount = self.principal_tx_out_amount; - // construct collateral input let collateral_input = self .repayment_collateral_input .clone() .into_unblinded_input(secp) .context("could not unblind repayment collateral input")?; - let principal_inputs = coin_selector(repayment_amount, self.usdt_asset_id).await?; + let principal_inputs = coin_selector( + self.collateral_contract.repayment_amount(secp)?, + self.usdt_asset_id, + ) + .await?; let unblinded_principal_inputs = principal_inputs .clone() @@ -490,7 +678,9 @@ impl Borrower1 { borrower_inputs }; - let mut repayment_principal_output = self.repayment_principal_output.clone(); + let repayment_amount = self.collateral_contract.repayment_amount(secp)?; + let mut repayment_principal_output = + self.collateral_contract.repayment_principal_output.clone(); let domain = inputs .iter() .map(|(asset, secrets)| { @@ -525,13 +715,13 @@ impl Borrower1 { ) })?; - let principal_repayment_output = TxOutSecrets::new( + let repayment_principal_output_secrets = TxOutSecrets::new( self.usdt_asset_id, self.repayment_collateral_abf, repayment_amount.as_sat(), self.repayment_collateral_vbf, ); - let mut outputs = vec![principal_repayment_output]; + let mut outputs = vec![repayment_principal_output_secrets]; let mut tx_ins: Vec = unblinded_principal_inputs .clone() @@ -628,6 +818,8 @@ impl Borrower1 { pub struct Lender0 { keypair: (SecretKey, PublicKey), address: Address, + address_blinder: SecretKey, + oracle_pk: PublicKey, bitcoin_asset_id: AssetId, usdt_asset_id: AssetId, } @@ -638,6 +830,8 @@ impl Lender0 { bitcoin_asset_id: AssetId, usdt_asset_id: AssetId, address: Address, + address_blinder: SecretKey, + oracle_pk: PublicKey, ) -> Result where R: RngCore + CryptoRng, @@ -647,6 +841,8 @@ impl Lender0 { Ok(Self { keypair, address, + address_blinder, + oracle_pk, bitcoin_asset_id, usdt_asset_id, }) @@ -669,7 +865,12 @@ impl Lender0 { CS: FnOnce(Amount, AssetId) -> CF, CF: Future>>, { - let principal_amount = Lender0::calc_principal_amount(&loan_request, rate)?; + // TODO: This API should change so that the caller can decide on all this outside of this library + let LoanAmounts { + principal: principal_amount, + repayment: repayment_amount, + min_price_btc, + } = Lender0::calculate_loan_amounts(&loan_request, rate, 20, 10)?; let collateral_inputs = loan_request .collateral_inputs .into_iter() @@ -712,7 +913,7 @@ impl Lender0 { TxOut::new_not_last_confidential( rng, secp, - principal_amount.as_sat(), + repayment_amount.as_sat(), self.address.clone(), self.usdt_asset_id, &dummy_inputs, @@ -722,11 +923,20 @@ impl Lender0 { let (_, lender_pk) = self.keypair; let (collateral_blinding_sk, collateral_blinding_pk) = make_keypair(rng); + // TODO: This API should change to allow the caller to + // determine when oracle signatures will start being valid for + // the collateral contract + let now = std::time::SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; + let contract_creation_timestamp = now.as_secs() + 300; + let collateral_contract = CollateralContract::new( loan_request.borrower_pk, lender_pk, loan_request.timelock, - repayment_principal_output.clone(), + (repayment_principal_output, self.address_blinder), + self.oracle_pk, + min_price_btc, + contract_creation_timestamp, ) .context("could not build collateral contract")?; let collateral_address = collateral_contract.blinded_address(collateral_blinding_pk.key); @@ -880,33 +1090,75 @@ impl Lender0 { repayment_collateral_abf, repayment_collateral_vbf, bitcoin_asset_id: self.bitcoin_asset_id, - repayment_principal_output, }) } - fn calc_principal_amount(loan_request: &LoanRequest, rate: u64) -> Result { + fn calculate_loan_amounts( + loan_request: &LoanRequest, + rate: u64, + overcollateralization_percentage: u8, + interest_percentage: u8, + ) -> Result { use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; let sats = loan_request.collateral_amount.as_sat(); let btc = Decimal::from(sats) .checked_div(Decimal::from(Amount::ONE_BTC.as_sat())) - .ok_or_else(|| anyhow!("division overflow"))?; + .context("division overflow")?; let satodollars_per_btc = Decimal::from(rate); - let satodollars = satodollars_per_btc * btc; - let satodollars = satodollars + let overcollateralization_factor = Decimal::from(overcollateralization_percentage + 100) + .checked_div(Decimal::from(100)) + .context("division overflow")?; + + let satodollars_per_btc_adjusted = satodollars_per_btc + .checked_div(overcollateralization_factor) + .context("division overflow")?; + + let principal = satodollars_per_btc_adjusted * btc; + let principal = principal + .to_u64() + .context("decimal cannot be represented as u64")?; + let principal = Amount::from_sat(principal); + + let interest_factor = Decimal::from(interest_percentage + 100) + .checked_div(Decimal::from(100)) + .context("division overflow")?; + let repayment = Decimal::from(principal.as_sat()) * interest_factor; + + let min_price_btc = repayment.checked_div(btc).context("division overflow")?; + let min_price_btc = min_price_btc + .to_u64() + .context("decimal cannot be represented as u64")?; + + let repayment = repayment .to_u64() - .ok_or_else(|| anyhow!("decimal cannot be represented as u64"))?; + .context("decimal cannot be represented as u64")?; + let repayment = Amount::from_sat(repayment); - Ok(Amount::from_sat(satodollars)) + Ok(LoanAmounts { + principal, + repayment, + min_price_btc, + }) } } +struct LoanAmounts { + /// Principal amount, in satodollars. + principal: Amount, + /// Repayment amount, in satodollars. + repayment: Amount, + /// Price under which the lender will be able to unilaterally + /// liquidate the collateral contract, in whole USD. + min_price_btc: u64, +} + pub struct Lender1 { keypair: (SecretKey, PublicKey), address: Address, - pub timelock: u64, + timelock: u64, loan_transaction: Transaction, collateral_contract: CollateralContract, collateral_amount: Amount, @@ -914,19 +1166,16 @@ pub struct Lender1 { repayment_collateral_abf: AssetBlindingFactor, repayment_collateral_vbf: ValueBlindingFactor, bitcoin_asset_id: AssetId, - repayment_principal_output: TxOut, } impl Lender1 { pub fn loan_response(&self) -> LoanResponse { LoanResponse { transaction: self.loan_transaction.clone(), - lender_pk: self.keypair.1, + collateral_contract: self.collateral_contract.clone(), repayment_collateral_input: self.repayment_collateral_input.clone(), repayment_collateral_abf: self.repayment_collateral_abf, repayment_collateral_vbf: self.repayment_collateral_vbf, - timelock: self.timelock, - repayment_principal_output: self.repayment_principal_output.clone(), } } @@ -1013,6 +1262,144 @@ impl Lender1 { Ok(liquidation_transaction) } + + pub async fn dynamic_liquidation_transaction( + &self, + rng: &mut R, + secp: &Secp256k1, + oracle_msg: oracle::Message, + oracle_sig: Signature, + fee_sats_per_vbyte: Amount, + ) -> Result + where + R: RngCore + CryptoRng, + C: Verification + Signing, + { + // construct collateral input + let collateral_input = self + .repayment_collateral_input + .clone() + .into_unblinded_input(secp) + .context("could not unblind repayment collateral input")?; + + let inputs = [(collateral_input.txout.asset, &collateral_input.secrets)]; + + let tx_fee = Amount::from_sat( + estimate_virtual_size(inputs.len() as u64, 4) * fee_sats_per_vbyte.as_sat(), + ); + + let (collateral_output, _, _) = TxOut::new_last_confidential( + rng, + secp, + (self.collateral_amount - tx_fee).as_sat(), + self.address.clone(), + self.bitcoin_asset_id, + &inputs, + &[], + ) + .context("Creation of collateral output failed")?; + + let tx_fee_output = TxOut::new_fee(tx_fee.as_sat(), self.bitcoin_asset_id); + + let tx_ins = vec![TxIn { + previous_output: collateral_input.txin, + is_pegin: false, + has_issuance: false, + script_sig: Default::default(), + sequence: 0, + asset_issuance: Default::default(), + witness: Default::default(), + }]; + let tx_outs = vec![collateral_output, tx_fee_output]; + + let mut liquidation_transaction = Transaction { + version: 2, + lock_time: 0, + input: tx_ins, + output: tx_outs, + }; + self.collateral_contract + .satisfy_dynamic_liquidation( + |message| async move { Ok(SECP256K1.sign(&message, &self.keypair.0)) }, + &mut liquidation_transaction, + self.repayment_collateral_input.original_txout.value, + 0, + oracle_msg, + oracle_sig, + ) + .await?; + + Ok(liquidation_transaction) + } + + /// Get a reference to the collateral contract. + pub fn collateral_contract(&self) -> &CollateralContract { + &self.collateral_contract + } +} + +/// Toy version of the oracle module used to define the parts of the +/// oracle which matter to the loan protocol. +/// +/// In particular, the oracle must encode the message in such a way +/// that it can be decomposed and used from within an Elements script. +pub(crate) mod oracle { + pub struct Message { + /// Price of bitcoin in whole USD. + btc_price: WitnessStackInteger, + /// UNIX timestamp. + timestamp: WitnessStackInteger, + } + + impl Message { + pub fn new(btc_price: u64, timestamp: u64) -> Self { + Self { + btc_price: WitnessStackInteger(btc_price), + timestamp: WitnessStackInteger(timestamp), + } + } + + /// Serialize price as bytes. + pub fn price_to_bytes(&self) -> Vec { + self.btc_price.serialize() + } + + /// Serialize timestamp as bytes. + pub fn timestamp_to_bytes(&self) -> Vec { + self.timestamp.serialize() + } + + #[cfg(test)] + pub fn message_hash(&self) -> secp256k1::Message { + use bitcoin_hashes::{sha256, Hash, HashEngine}; + + let mut sha256d = sha256::Hash::engine(); + sha256d.input(&self.price_to_bytes()); + sha256d.input(&self.timestamp_to_bytes()); + let message_hash = sha256::Hash::from_engine(sha256d); + + secp256k1::Message::from_slice(&message_hash).unwrap() + } + } + + struct WitnessStackInteger(u64); + + impl WitnessStackInteger { + /// Serialize an integer so that it can be included in a Bitcoin witness stack. + /// + /// Said format is a little-endian byte encoding without trailing 0-bytes. + fn serialize(&self) -> Vec { + // to save a reverse operation, we first encode it as big-endian + let bytes = self.0.to_be_bytes().to_vec(); + let mut bytes = bytes + .into_iter() + .skip_while(|byte| *byte == 0) + .collect::>(); + bytes.reverse(); + + bytes + } + } } fn make_keypair(rng: &mut R) -> (SecretKey, PublicKey) diff --git a/src/loan/protocol_tests.rs b/src/loan/protocol_tests.rs index 26f4a35..d94fd4a 100644 --- a/src/loan/protocol_tests.rs +++ b/src/loan/protocol_tests.rs @@ -1,12 +1,13 @@ #[cfg(test)] mod tests { - use crate::loan::{make_keypair, Borrower0, Lender0}; + use std::time::SystemTime; + + use crate::loan::{oracle, Borrower0, Lender0}; use anyhow::{Context, Result}; use bitcoin_hashes::Hash; use elements::bitcoin::util::psbt::serialize::Serialize; - use elements::bitcoin::{Amount, PublicKey}; + use elements::bitcoin::{Amount, Network, PrivateKey, PublicKey}; use elements::script::Builder; - use elements::secp256k1_zkp::rand::thread_rng; use elements::secp256k1_zkp::{SecretKey, SECP256K1}; use elements::sighash::SigHashCache; use elements::{ @@ -37,6 +38,7 @@ mod tests { let bitcoin_asset_id = client.get_bitcoin_asset_id().await.unwrap(); let usdt_asset_id = client.issueasset(40.0, 0.0, false).await.unwrap().asset; + let (_oracle_sk, oracle_pk) = make_keypair(&mut rng); let miner_address = client.get_new_segwit_confidential_address().await.unwrap(); @@ -99,9 +101,17 @@ mod tests { let (lender, _lender_address) = { let address = client.get_new_segwit_confidential_address().await.unwrap(); + let address_blinder = blinding_key(&client, &address).await.unwrap(); - let lender = - Lender0::new(&mut rng, bitcoin_asset_id, usdt_asset_id, address.clone()).unwrap(); + let lender = Lender0::new( + &mut rng, + bitcoin_asset_id, + usdt_asset_id, + address.clone(), + address_blinder, + oracle_pk, + ) + .unwrap(); (lender, address) }; @@ -164,7 +174,7 @@ mod tests { client .send_raw_transaction(&loan_repayment_transaction) .await - .unwrap(); + .expect("could not repay loan to reclaim collateral"); } #[tokio::test] @@ -185,6 +195,7 @@ mod tests { let bitcoin_asset_id = client.get_bitcoin_asset_id().await.unwrap(); let usdt_asset_id = client.issueasset(40.0, 0.0, false).await.unwrap().asset; + let (_oracle_sk, oracle_pk) = make_keypair(&mut rng); let miner_address = client.get_new_segwit_confidential_address().await.unwrap(); client @@ -222,7 +233,7 @@ mod tests { client.generatetoaddress(1, &miner_address).await.unwrap(); - let timelock = client.get_blockcount().await.unwrap() + 2; + let timelock = client.get_blockcount().await.unwrap() + 5; let borrower = Borrower0::new( &mut rng, @@ -246,9 +257,17 @@ mod tests { let (lender, _lender_address) = { let address = client.get_new_segwit_confidential_address().await.unwrap(); + let address_blinder = blinding_key(&client, &address).await.unwrap(); - let lender = - Lender0::new(&mut rng, bitcoin_asset_id, usdt_asset_id, address.clone()).unwrap(); + let lender = Lender0::new( + &mut rng, + bitcoin_asset_id, + usdt_asset_id, + address.clone(), + address_blinder, + oracle_pk, + ) + .unwrap(); (lender, address) }; @@ -289,8 +308,6 @@ mod tests { .await .unwrap(); - client.generatetoaddress(2, &miner_address).await.unwrap(); - let liquidation_transaction = lender .liquidation_transaction(&mut rng, &SECP256K1, Amount::from_sat(1)) .await @@ -299,7 +316,273 @@ mod tests { client .send_raw_transaction(&liquidation_transaction) .await + .expect_err("could liquidate before loan term"); + + client.generatetoaddress(5, &miner_address).await.unwrap(); + + client + .send_raw_transaction(&liquidation_transaction) + .await + .expect("could not liquidate after loan term"); + } + + #[tokio::test] + async fn lend_and_dynamic_liquidate() { + init_logger(); + + let mut rng = ChaChaRng::seed_from_u64(0); + + let tc_client = Cli::default(); + let (client, _container) = { + let blockchain = Elementsd::new(&tc_client, "0.18.1.9").unwrap(); + + ( + elements_rpc::Client::new(blockchain.node_url.clone().into()).unwrap(), + blockchain, + ) + }; + + let bitcoin_asset_id = client.get_bitcoin_asset_id().await.unwrap(); + let usdt_asset_id = client.issueasset(40.0, 0.0, false).await.unwrap().asset; + let (oracle_sk, oracle_pk) = make_keypair(&mut rng); + + let miner_address = client.get_new_segwit_confidential_address().await.unwrap(); + client + .send_asset_to_address(&miner_address, Amount::from_btc(5.0).unwrap(), None) + .await + .unwrap(); + client.generatetoaddress(10, &miner_address).await.unwrap(); + + let (borrower, borrower_wallet) = { + let mut wallet = Wallet::new(&mut rng); + + let collateral_amount = Amount::ONE_BTC; + + let address = wallet.address(); + let address_blinding_sk = wallet.dump_blinding_sk(); + + // fund borrower address with bitcoin + let txid = client + .send_asset_to_address(&address, collateral_amount * 2, Some(bitcoin_asset_id)) + .await + .unwrap(); + + wallet.add_known_utxo(&client, txid).await; + + // fund wallet with some usdt to pay back the loan later on + let txid = client + .send_asset_to_address( + &address, + Amount::from_btc(2.0).unwrap(), + Some(usdt_asset_id), + ) + .await + .unwrap(); + wallet.add_known_utxo(&client, txid).await; + + client.generatetoaddress(1, &miner_address).await.unwrap(); + + let timelock = client.get_blockcount().await.unwrap() + 100; + + let borrower = Borrower0::new( + &mut rng, + { + let wallet = wallet.clone(); + |amount, asset| async move { wallet.find_inputs(asset, amount).await } + }, + address.clone(), + address_blinding_sk, + collateral_amount, + Amount::ONE_SAT, + timelock as u64, + bitcoin_asset_id, + usdt_asset_id, + ) + .await .unwrap(); + + (borrower, wallet) + }; + + let (lender, _lender_address) = { + let address = client.get_new_segwit_confidential_address().await.unwrap(); + let address_blinder = blinding_key(&client, &address).await.unwrap(); + + let lender = Lender0::new( + &mut rng, + bitcoin_asset_id, + usdt_asset_id, + address.clone(), + address_blinder, + oracle_pk, + ) + .unwrap(); + + (lender, address) + }; + + let loan_request = borrower.loan_request(); + + let lender = lender + .interpret( + &mut rng, + &SECP256K1, + { + let client = client.clone(); + |amount, asset| async move { find_inputs(&client, asset, amount).await } + }, + loan_request, + 38_000, // value of 1 BTC as of 18.06.2021 + ) + .await + .unwrap(); + let loan_response = lender.loan_response(); + + let borrower = borrower.interpret(&SECP256K1, loan_response).unwrap(); + let loan_transaction = borrower + .sign(|transaction| async move { Ok(borrower_wallet.sign_all_inputs(transaction)) }) + .await + .unwrap(); + + let loan_transaction = lender + .finalise_loan(loan_transaction, { + let client = client.clone(); + |transaction| async move { client.sign_raw_transaction(&transaction).await } + }) + .await + .unwrap(); + + client + .send_raw_transaction(&loan_transaction) + .await + .unwrap(); + + // Oracle message too early: + { + // before contract creation + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let current_timestamp = now - 3600; + // price dips below minimum + let current_btc_price = 0; + + let oracle_msg = oracle::Message::new(current_btc_price, current_timestamp); + let oracle_sig = SECP256K1.sign(&oracle_msg.message_hash(), &oracle_sk); + + let liquidation_transaction = lender + .dynamic_liquidation_transaction( + &mut rng, + &SECP256K1, + oracle_msg, + oracle_sig, + Amount::ONE_SAT, + ) + .await + .unwrap(); + + client + .send_raw_transaction(&liquidation_transaction) + .await + .expect_err("could liquidate with proof of dip before contract creation"); + } + + // Price too high: + { + // fast forward + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let current_timestamp = now + 3600; + // price remains way above threshold + let current_btc_price = 1_000_000; + + let oracle_msg = oracle::Message::new(current_btc_price, current_timestamp); + let oracle_sig = SECP256K1.sign(&oracle_msg.message_hash(), &oracle_sk); + + let liquidation_transaction = lender + .dynamic_liquidation_transaction( + &mut rng, + &SECP256K1, + oracle_msg, + oracle_sig, + Amount::ONE_SAT, + ) + .await + .unwrap(); + + client + .send_raw_transaction(&liquidation_transaction) + .await + .expect_err("could liquidate with proof of dip above threshold"); + } + + // Not signed by oracle: + { + // fast forward + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let current_timestamp = now + 3600; + // price dips below minimum + let current_btc_price = 0; + + let oracle_msg = oracle::Message::new(current_btc_price, current_timestamp); + + // signed by a fake oracle + let (fake_oracle_sk, _) = make_keypair(&mut rng); + let oracle_sig = SECP256K1.sign(&oracle_msg.message_hash(), &fake_oracle_sk); + + let liquidation_transaction = lender + .dynamic_liquidation_transaction( + &mut rng, + &SECP256K1, + oracle_msg, + oracle_sig, + Amount::ONE_SAT, + ) + .await + .unwrap(); + + client + .send_raw_transaction(&liquidation_transaction) + .await + .expect_err("could liquidate with invalid valid proof of dip"); + } + + // Success: + { + // fast forward + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let current_timestamp = now + 3600; + // price dips below minimum + let current_btc_price = 0; + + let oracle_msg = oracle::Message::new(current_btc_price, current_timestamp); + let oracle_sig = SECP256K1.sign(&oracle_msg.message_hash(), &oracle_sk); + + let liquidation_transaction = lender + .dynamic_liquidation_transaction( + &mut rng, + &SECP256K1, + oracle_msg, + oracle_sig, + Amount::ONE_SAT, + ) + .await + .unwrap(); + + client + .send_raw_transaction(&liquidation_transaction) + .await + .expect("could not liquidate with valid proof of dip"); + } } fn init_logger() { @@ -307,6 +590,15 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); } + async fn blinding_key(client: &elements_rpc::Client, address: &Address) -> Result { + let master_blinding_key = client.dumpmasterblindingkey().await?; + let master_blinding_key = hex::decode(master_blinding_key)?; + + let sk = derive_blinding_key(master_blinding_key, address.script_pubkey())?; + + Ok(sk) + } + async fn find_inputs( client: &elements_rpc::Client, asset: AssetId, @@ -490,4 +782,21 @@ mod tests { tx_to_sign } } + + fn make_keypair(rng: &mut R) -> (SecretKey, PublicKey) + where + R: RngCore + CryptoRng, + { + let sk = SecretKey::new(rng); + let pk = PublicKey::from_private_key( + &SECP256K1, + &PrivateKey { + compressed: true, + network: Network::Regtest, + key: sk, + }, + ); + + (sk, pk) + } } From c5edcb44f263458ad5f0e9ec32ac894276e08213 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Wed, 28 Jul 2021 00:44:11 +1000 Subject: [PATCH 4/7] Replace public fields with getters --- CHANGELOG.md | 14 ++++++++++++++ src/loan.rs | 44 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2377326..ade6e05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 It takes a message of the form `price:timestamp` and an oracle's signature on the hash of the message, so that a _lender_ can unilaterally liquidate the loan if the `price` falls below a threshold and the `timestamp` is past a certain time. - `Lender0` constructor now requires blinding key of lender address and an oracle public key. +### Changed + +- `loan::LoanResponse` fields: + - Made `timelock` private, but accessible via `LoanResponse::collateral_contract(&self).timelock()`. + - Made `transaction` private, accessible via getter. + - Made `collateral_amount` private, but accessible via getter. +- `loan::Borrower1` fields: + - Made `loan_transaction` private, but accessible via getter. + - Made `collateral_amount` private, but accessible via getter. +- `loan::Lender1` fields: + - Made `timelock` private, but accessible via getter. +- `fn liquidation_transaction()` API on `Lender1` is now `async`. + + ## [0.1.1] - 2021-07-23 ## [0.1.0] - 2021-07-16 diff --git a/src/loan.rs b/src/loan.rs index 65df40f..e2b3353 100644 --- a/src/loan.rs +++ b/src/loan.rs @@ -430,7 +430,7 @@ impl CollateralContract { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct LoanRequest { #[serde(with = "::elements::bitcoin::util::amount::serde::as_sat")] - pub collateral_amount: Amount, + collateral_amount: Amount, collateral_inputs: Vec, #[serde(with = "::elements::bitcoin::util::amount::serde::as_sat")] fee_sats_per_vbyte: Amount, @@ -439,17 +439,31 @@ pub struct LoanRequest { borrower_address: Address, } +impl LoanRequest { + /// Get a copy of the collateral amount. + pub fn collateral_amount(&self) -> Amount { + self.collateral_amount + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct LoanResponse { // TODO: Use this where needed! #[serde(with = "transaction_as_string")] - pub transaction: Transaction, + transaction: Transaction, collateral_contract: CollateralContract, repayment_collateral_input: Input, repayment_collateral_abf: AssetBlindingFactor, repayment_collateral_vbf: ValueBlindingFactor, } +impl LoanResponse { + /// Get a reference to the loan transaction. + pub fn transaction(&self) -> &Transaction { + &self.transaction + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Borrower0 { keypair: (SecretKey, PublicKey), @@ -609,12 +623,12 @@ impl Borrower0 { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Borrower1 { keypair: (SecretKey, PublicKey), - pub loan_transaction: Transaction, + loan_transaction: Transaction, #[serde(with = "::elements::bitcoin::util::amount::serde::as_sat")] - pub collateral_amount: Amount, + collateral_amount: Amount, collateral_contract: CollateralContract, #[serde(with = "::elements::bitcoin::util::amount::serde::as_sat")] - pub principal_amount: Amount, + principal_amount: Amount, address: Address, /// Loan collateral expressed as an input for constructing the /// loan repayment transaction. @@ -813,6 +827,26 @@ impl Borrower1 { Ok(tx) } + + /// Get a copy of the collateral amount. + pub fn collateral_amount(&self) -> Amount { + self.collateral_amount + } + + /// Get a copy of the principal amount. + pub fn principal_amount(&self) -> Amount { + self.principal_amount + } + + /// Get a reference to the collateral contract. + pub fn collateral_contract(&self) -> &CollateralContract { + &self.collateral_contract + } + + /// Get a reference to the loan transaction. + pub fn loan_transaction(&self) -> &Transaction { + &self.loan_transaction + } } pub struct Lender0 { From 4a3a2b5b8b87b630a602439e630829d36925cf57 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Wed, 28 Jul 2021 15:26:23 +1000 Subject: [PATCH 5/7] Model the collateral contract's timelock as u32 A `rust_bitcoin::Transaction` only accepts a u32 anyway. --- CHANGELOG.md | 2 +- src/loan.rs | 16 ++++++++-------- src/loan/protocol_tests.rs | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ade6e05..1523434 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `loan::Lender1` fields: - Made `timelock` private, but accessible via getter. - `fn liquidation_transaction()` API on `Lender1` is now `async`. - +- Model the collateral contract's timelock as a `u32`. ## [0.1.1] - 2021-07-23 diff --git a/src/loan.rs b/src/loan.rs index e2b3353..024c199 100644 --- a/src/loan.rs +++ b/src/loan.rs @@ -103,7 +103,7 @@ impl CollateralContract { fn new( borrower_pk: PublicKey, lender_pk: PublicKey, - timelock: u64, + timelock: u32, (repayment_principal_output, repayment_principal_output_blinder): (TxOut, SecretKey), oracle_pk: PublicKey, min_price_btc: u64, @@ -197,7 +197,7 @@ impl CollateralContract { lender_pk, repayment_principal_output, repayment_principal_output_blinder, - timelock: timelock as u32, // TODO: Model timelocks as u32 + timelock, oracle_pk, min_price_btc, min_timestamp, @@ -255,7 +255,7 @@ impl CollateralContract { self.lender_pk, ) .await?; - let after_sat = After(self.timelock as u32); + let after_sat = After(self.timelock); self.satisfy( (satisfiers, after_sat), @@ -435,7 +435,7 @@ pub struct LoanRequest { #[serde(with = "::elements::bitcoin::util::amount::serde::as_sat")] fee_sats_per_vbyte: Amount, borrower_pk: PublicKey, - timelock: u64, + timelock: u32, borrower_address: Address, } @@ -474,7 +474,7 @@ pub struct Borrower0 { collateral_inputs: Vec, #[serde(with = "::elements::bitcoin::util::amount::serde::as_sat")] fee_sats_per_vbyte: Amount, - timelock: u64, + timelock: u32, bitcoin_asset_id: AssetId, usdt_asset_id: AssetId, } @@ -488,7 +488,7 @@ impl Borrower0 { address_blinding_sk: SecretKey, collateral_amount: Amount, fee_sats_per_vbyte: Amount, - timelock: u64, + timelock: u32, bitcoin_asset_id: AssetId, usdt_asset_id: AssetId, ) -> Result @@ -1192,7 +1192,7 @@ struct LoanAmounts { pub struct Lender1 { keypair: (SecretKey, PublicKey), address: Address, - timelock: u64, + timelock: u32, loan_transaction: Transaction, collateral_contract: CollateralContract, collateral_amount: Amount, @@ -1278,7 +1278,7 @@ impl Lender1 { let mut liquidation_transaction = Transaction { version: 2, - lock_time: self.timelock as u32, + lock_time: self.timelock, input: tx_ins, output: tx_outs, }; diff --git a/src/loan/protocol_tests.rs b/src/loan/protocol_tests.rs index d94fd4a..5cebb6f 100644 --- a/src/loan/protocol_tests.rs +++ b/src/loan/protocol_tests.rs @@ -245,7 +245,7 @@ mod tests { address_blinding_sk, collateral_amount, Amount::ONE_SAT, - timelock as u64, + timelock, bitcoin_asset_id, usdt_asset_id, ) @@ -394,7 +394,7 @@ mod tests { address_blinding_sk, collateral_amount, Amount::ONE_SAT, - timelock as u64, + timelock, bitcoin_asset_id, usdt_asset_id, ) From e26838d3bc7ba8e9b4a2d22a6d48808a73566ef9 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Wed, 28 Jul 2021 17:36:11 +1000 Subject: [PATCH 6/7] Prove that COVENANT_PK corresponds to COVENANT_SK in constant_tests With this invariant we can `expect` the covenant descriptor that used `COVENANT_PK` to be valid. --- src/loan.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/loan.rs b/src/loan.rs index 024c199..48091b8 100644 --- a/src/loan.rs +++ b/src/loan.rs @@ -122,7 +122,7 @@ impl CollateralContract { COVENANT_PK, lender_pk, timelock, borrower_pk, repayment_principal_output_as_hex, ) .parse() - .context("invalid collateral output descriptor")?; + .expect("valid collateral output descriptor"); let dynamic_liquidation_branch = Builder::new() // check that the lender authorises the spend @@ -1471,3 +1471,20 @@ pub mod transaction_as_string { Ok(tx) } } + +#[cfg(test)] +mod constant_tests { + use super::{COVENANT_PK, COVENANT_SK}; + use elements::bitcoin::{PrivateKey, PublicKey}; + use secp256k1_zkp::SECP256K1; + + #[test] + fn covenant_pk_is_the_public_key_of_covenant_sk() { + let pk = PublicKey::from_private_key( + SECP256K1, + &PrivateKey::new(*COVENANT_SK, elements::bitcoin::Network::Regtest), + ); + + assert_eq!(format!("{}", pk), COVENANT_PK) + } +} From ed0c50462a77027f795f2f24578aaa2dc7a38394 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Thu, 29 Jul 2021 14:04:52 +1000 Subject: [PATCH 7/7] Add COV_SCRIPT_BYTES constant AFAIK, we cannot construct a `const` Script, so we must use bytes. It is a bit annoying to have to construct the script just to pass it into `descriptor_satisfiers` as an argument, but otherwise we end up returning a value referencing the local variable for the covenant script inside `descriptor_satisfiers`. Alternatives are welcome. --- src/loan.rs | 45 +++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/loan.rs b/src/loan.rs index 48091b8..8ee3d06 100644 --- a/src/loan.rs +++ b/src/loan.rs @@ -82,11 +82,6 @@ pub struct CollateralContract { /// This is the only part of the script which is included in the /// transaction sighash which is signed with `COVENANT_SK`. /// - /// In a regular `CovenantDescriptor` this is just - /// `OP_CHECKSIGVERIFY` and `OP_CHECKSIGFROMSTACK`. In our case, - /// it's `OP_CHECKSIGVERIFY`, `OP_CHECKSIGFROMSTACK` and - /// `OP_ENDIF`. - cov_script: Script, borrower_pk: PublicKey, lender_pk: PublicKey, repayment_principal_output: TxOut, @@ -99,6 +94,15 @@ pub struct CollateralContract { } impl CollateralContract { + /// The bytes of the Script to be included in the sighash which is signed to + /// satisfy the covenant descriptor. + /// + /// In a regular `CovenantDescriptor` this is just + /// `OP_CHECKSIGVERIFY` and `OP_CHECKSIGFROMSTACK`. In our case, + /// it's `OP_CHECKSIGVERIFY`, `OP_CHECKSIGFROMSTACK` and + /// `OP_ENDIF`. + const COV_SCRIPT_BYTES: [u8; 3] = [0xad, 0xc1, 0x68]; + /// Fill in the collateral contract template with the provided arguments. fn new( borrower_pk: PublicKey, @@ -183,16 +187,9 @@ impl CollateralContract { Script::from(script) }; - let cov_script = Builder::new() - .push_opcode(OP_CHECKSIGVERIFY) - .push_opcode(OP_CHECKSIGFROMSTACK) - .push_opcode(OP_ENDIF) - .into_script(); - Ok(Self { descriptor, raw_script, - cov_script, borrower_pk, lender_pk, repayment_principal_output, @@ -216,6 +213,7 @@ impl CollateralContract { SF: Future>, { let transaction_cloned = transaction.clone(); + let cov_script = Script::from(Self::COV_SCRIPT_BYTES.to_vec()); let satisfiers = self .descriptor_satisfiers( identity_signer, @@ -223,6 +221,7 @@ impl CollateralContract { input_value, input_index, self.borrower_pk, + &cov_script, ) .await?; @@ -246,6 +245,7 @@ impl CollateralContract { SF: Future>, { let transaction_cloned = transaction.clone(); + let cov_script = Script::from(Self::COV_SCRIPT_BYTES.to_vec()); let satisfiers = self .descriptor_satisfiers( identity_signer, @@ -253,6 +253,7 @@ impl CollateralContract { input_value, input_index, self.lender_pk, + &cov_script, ) .await?; let after_sat = After(self.timelock); @@ -324,6 +325,7 @@ impl CollateralContract { input_value: confidential::Value, input_index: u32, identity_pk: PublicKey, + cov_script: &'a Script, ) -> Result + 'a> where S: FnOnce(secp256k1::Message) -> SF, @@ -331,7 +333,6 @@ impl CollateralContract { { let descriptor_cov = &self.descriptor.as_cov().expect("covenant descriptor"); - let cov_script = &self.cov_script; let cov_sat = CovSatisfier::new_segwitv0( &transaction, input_index, @@ -1474,9 +1475,7 @@ pub mod transaction_as_string { #[cfg(test)] mod constant_tests { - use super::{COVENANT_PK, COVENANT_SK}; - use elements::bitcoin::{PrivateKey, PublicKey}; - use secp256k1_zkp::SECP256K1; + use super::*; #[test] fn covenant_pk_is_the_public_key_of_covenant_sk() { @@ -1487,4 +1486,18 @@ mod constant_tests { assert_eq!(format!("{}", pk), COVENANT_PK) } + + #[test] + fn cov_script_bytes_represents_correct_script() { + use elements::opcodes::all::*; + + let expected = Builder::new() + .push_opcode(OP_CHECKSIGVERIFY) + .push_opcode(OP_CHECKSIGFROMSTACK) + .push_opcode(OP_ENDIF) + .into_script(); + let actual = Script::from(CollateralContract::COV_SCRIPT_BYTES.to_vec()); + + assert_eq!(actual, expected); + } }