diff --git a/CHANGELOG.md b/CHANGELOG.md index e3b5b65..7ac4cce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 The provided wallet comes with a UTXO cache which this is updated using `Wallet::sync`. This allows users of the library to optimise the number of requests to their backend. Users can also sign said UTXOs by calling `Wallet::sign`. +- `Timelock` type to allow users of the library to explicitly choose the type of timelock they want to use when building the loan transaction. + For the time being, users can still pass in a `u32`, but they are encouraged not to. ### Changed diff --git a/Cargo.toml b/Cargo.toml index 5c38a07..d165916 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ thiserror = "1" [dev-dependencies] elements-consensus = { git = "https://github.com/comit-network/rust-elements-consensus", rev = "ac88dbedcd019eef44f58499417dcdbeda994b0b" } link-cplusplus = "1" +proptest = { version = "1", default-features = false, features = ["std"] } rand_chacha = "0.1" serde_json = "1" tokio = { version = "1", default-features = false, features = ["macros", "rt"] } diff --git a/src/loan.rs b/src/loan.rs index 98b594a..ca459c9 100644 --- a/src/loan.rs +++ b/src/loan.rs @@ -929,7 +929,7 @@ impl Lender0 { secp: &Secp256k1, coin_selector: CS, loan_request: LoanRequest, - timelock: u32, + timelock: Timelock, rate: u64, ) -> Result where @@ -974,13 +974,14 @@ impl Lender0 { repayment_amount: Amount, min_collateral_price: u64, (borrower_pk, borrower_address): (PublicKey, Address), - timelock: u32, + timelock: impl Into, ) -> Result where R: RngCore + CryptoRng, C: Verification + Signing, { let chain = Chain::new(&borrower_address, &self.address)?; + let timelock = Into::::into(timelock); let collateral_inputs = collateral_inputs .into_iter() @@ -1141,7 +1142,7 @@ impl Lender0 { let collateral_contract = CollateralContract::new( borrower_pk, lender_pk, - timelock, + timelock.into(), (repayment_principal_output, self.address_blinder), self.oracle_pk, min_collateral_price, @@ -1292,7 +1293,7 @@ struct LoanAmounts { pub struct Lender1 { keypair: (SecretKey, PublicKey), address: Address, - timelock: u32, + timelock: Timelock, loan_transaction: Transaction, collateral_contract: CollateralContract, collateral_amount: Amount, @@ -1378,7 +1379,7 @@ impl Lender1 { let mut liquidation_transaction = Transaction { version: 2, - lock_time: self.timelock, + lock_time: self.timelock.into(), input: tx_ins, output: tx_outs, }; @@ -1508,6 +1509,61 @@ pub mod transaction_as_string { } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Timelock { + Timestamp(u32), + BlockHeight(u32), +} + +impl Timelock { + // https://github.com/bitcoin/bitcoin/blob/b620b2d58a55a88ad21da70cb2000863ef17b651/src/script/script.h#L37-L39 + const LOCKTIME_THRESHOLD: u32 = 500000000; + + pub fn new_timestamp(n: u32) -> Result { + if n < Self::LOCKTIME_THRESHOLD { + return Err(NotATimestamp(n)); + } + + Ok(Self::Timestamp(n)) + } + + pub fn new_block_height(n: u32) -> Result { + if n >= Self::LOCKTIME_THRESHOLD { + return Err(NotABlockHeight(n)); + } + + Ok(Self::BlockHeight(n)) + } +} + +#[derive(thiserror::Error, Debug, PartialEq)] +#[error("Timelock based on timestamp must be over 500000000, got {0}")] +pub struct NotATimestamp(u32); + +#[derive(thiserror::Error, Debug, PartialEq)] +#[error("Timelock based on block height must be under 500000000, got {0}")] +pub struct NotABlockHeight(u32); + +impl From for u32 { + fn from(timelock: Timelock) -> Self { + match timelock { + Timelock::Timestamp(inner) | Timelock::BlockHeight(inner) => inner, + } + } +} + +impl From for Timelock { + fn from(n: u32) -> Self { + log::warn!("Choose a Timelock type explicitly via constructors"); + + if n < Timelock::LOCKTIME_THRESHOLD { + Self::BlockHeight(n) + } else { + Self::Timestamp(n) + } + } +} + /// Possible networks on which the loan contract may be deployed. #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] enum Chain { @@ -1587,3 +1643,25 @@ mod constant_tests { assert_eq!(actual, expected); } } + +#[cfg(test)] +mod timelock_tests { + use super::{NotABlockHeight, NotATimestamp, Timelock}; + use proptest::prelude::*; + + proptest! { + #[test] + fn locktime_under_threshold_is_blocktime(n in 0u32..=Timelock::LOCKTIME_THRESHOLD) { + prop_assert_eq!(Timelock::new_block_height(n), Ok(Timelock::BlockHeight(n))); + prop_assert_eq!(Timelock::new_timestamp(n), Err(NotATimestamp(n))); + } + } + + proptest! { + #[test] + fn locktime_over_threshold_is_timestamp(n in Timelock::LOCKTIME_THRESHOLD..=u32::MAX) { + prop_assert_eq!(Timelock::new_timestamp(n), Ok(Timelock::Timestamp(n))); + prop_assert_eq!(Timelock::new_block_height(n), Err(NotABlockHeight(n))); + } + } +} diff --git a/tests/loan_protocol.rs b/tests/loan_protocol.rs index c7d2131..a48db19 100644 --- a/tests/loan_protocol.rs +++ b/tests/loan_protocol.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex}; use std::time::SystemTime; use anyhow::{Context, Result}; -use baru::loan::{Borrower0, CollateralContract, Lender0}; +use baru::loan::{Borrower0, CollateralContract, Lender0, Timelock}; use baru::oracle; use elements::bitcoin::Amount; use elements::secp256k1_zkp::SECP256K1; @@ -72,7 +72,7 @@ async fn borrow_and_repay() { (lender, address) }; - let timelock = 10; + let timelock = Timelock::new_block_height(10).unwrap(); let principal_amount = Amount::from_btc(38_000.0).unwrap(); let principal_inputs = wallet.coin_select(principal_amount, usdt_asset_id).unwrap(); let repayment_amount = principal_amount + Amount::from_btc(1_000.0).unwrap(); @@ -208,7 +208,7 @@ async fn lend_and_liquidate() { (lender, address) }; - let timelock = 10; + let timelock = Timelock::new_block_height(10).unwrap(); let principal_amount = Amount::from_btc(38_000.0).unwrap(); let principal_inputs = wallet.coin_select(principal_amount, usdt_asset_id).unwrap(); let repayment_amount = principal_amount + Amount::from_btc(1_000.0).unwrap(); @@ -321,7 +321,7 @@ async fn lend_and_dynamic_liquidate() { (lender, address) }; - let timelock = 10; + let timelock = Timelock::new_block_height(10).unwrap(); let principal_amount = Amount::from_btc(38_000.0).unwrap(); let principal_inputs = wallet.coin_select(principal_amount, usdt_asset_id).unwrap(); let repayment_amount = principal_amount + Amount::from_btc(1_000.0).unwrap(); @@ -561,7 +561,7 @@ async fn can_run_protocol_with_principal_change_outputs() { (lender, address) }; - let timelock = 10; + let timelock = Timelock::new_block_height(10).unwrap(); let principal_amount = Amount::from_btc(38_000.0).unwrap(); let principal_inputs = wallet .coin_select(