From b6a5b225195a3a3ef3da67f1f3a349a76823fbdc Mon Sep 17 00:00:00 2001 From: chris-belcher Date: Wed, 2 Feb 2022 19:18:46 +0000 Subject: [PATCH] Handle accounting, fees, confirmations & locktimes This commit adds calculations for all the fees. That is coinswap fees paid from taker to makers, miner fees paid by makers and reimbursed by takers. Also handle the protocol for waiting a certain number of confirmations and allow the setting and checking of minimum locktime values. --- src/contracts.rs | 52 +++++++++++++++----- src/lib.rs | 2 + src/maker_protocol.rs | 108 +++++++++++++++++++++++++++--------------- src/messages.rs | 8 +++- src/taker_protocol.rs | 91 ++++++++++++++++++++++++++++++----- src/wallet_sync.rs | 27 +++++++---- 6 files changed, 215 insertions(+), 73 deletions(-) diff --git a/src/contracts.rs b/src/contracts.rs index 683f7894..0389f363 100644 --- a/src/contracts.rs +++ b/src/contracts.rs @@ -26,10 +26,20 @@ use crate::wallet_sync::{ create_multisig_redeemscript, IncomingSwapCoin, OutgoingSwapCoin, Wallet, NETWORK, }; -//TODO should be configurable somehow -//relatively low value for now so that its easier to test on regtest -pub const REFUND_LOCKTIME: u16 = 6; //in blocks -pub const REFUND_LOCKTIME_STEP: u16 = 3; //in blocks +//relatively simple handling of miner fees for now, each funding transaction is considered +// to have the same size, and taker will pay all the maker's miner fees based on that +//taker will choose what fee rate they will use, and how many funding transactions they want +// the makers to create +//this doesnt take into account the different sizes of single-sig, 2of2 multisig or htlc contracts +// but all those complications will go away when we move to ecdsa2p and scriptless scripts +// so theres no point adding complications for something that we'll hopefully get rid of soon +//this size here is for a tx with 2 p2wpkh outputs, 3 singlesig inputs and 1 2of2 multisig input +// if the maker can get stuff confirmed cheaper than this then they can keep that money +// if the maker ends up paying more then thats their problem +// we could avoid this guessing by adding one more round trip to the protocol where the maker +// calculates exactly how big the transactions will be and then taker knows exactly the miner fee +// to pay for +pub const MAKER_FUNDING_TX_VBYTE_SIZE: u64 = 372; //like the Incoming/OutgoingSwapCoin structs but no privkey or signature information //used by the taker to monitor coinswaps between two makers @@ -57,6 +67,18 @@ pub trait SwapCoin { fn is_hash_preimage_known(&self) -> bool; } +pub fn calculate_coinswap_fee( + absolute_fee_sat: u64, + amount_relative_fee_ppb: u64, + time_relative_fee_ppb: u64, + total_funding_amount: u64, + time_in_blocks: u64, +) -> u64 { + absolute_fee_sat + + (total_funding_amount * amount_relative_fee_ppb / 1_000_000_000) + + (time_in_blocks * time_relative_fee_ppb / 1_000_000_000) +} + pub fn calculate_maker_pubkey_from_nonce( tweakable_point: PublicKey, nonce: SecretKey, @@ -214,8 +236,8 @@ fn is_contract_out_valid( timelock_pubkey: &PublicKey, hashvalue: Hash160, locktime: u16, + minimum_locktime: u16, ) -> Result<(), Error> { - let minimum_locktime = 2; //TODO should be in config file or something if minimum_locktime > locktime { return Err(Error::Protocol("locktime too short")); } @@ -243,6 +265,7 @@ pub fn validate_and_sign_senders_contract_tx( funding_input_value: u64, hashvalue: Hash160, locktime: u16, + minimum_locktime: u16, tweakable_privkey: &SecretKey, wallet: &mut Wallet, ) -> Result { @@ -274,6 +297,7 @@ pub fn validate_and_sign_senders_contract_tx( &timelock_pubkey, hashvalue, locktime, + minimum_locktime, )?; //note question mark here propagating the error upwards wallet.add_prevout_and_contract_to_cache( @@ -317,6 +341,7 @@ pub fn verify_proof_of_funding( funding_info: &ConfirmedCoinSwapTxInfo, funding_output_index: u32, next_locktime: u16, + min_contract_react_time: u16, //returns my_multisig_privkey, other_multisig_pubkey, my_hashlock_privkey ) -> Result<(SecretKey, PublicKey, SecretKey), Error> { //check the funding_tx exists and was really confirmed @@ -365,9 +390,7 @@ pub fn verify_proof_of_funding( .ok_or(Error::Protocol("unable to read locktime from contract"))?; //this is the time the maker or his watchtowers have to be online, read // the hash preimage from the blockchain and broadcast their own tx - //TODO put this in a config file perhaps, and have it advertised to takers - const CONTRACT_REACT_TIME: u16 = 3; - if locktime - next_locktime < CONTRACT_REACT_TIME { + if locktime - next_locktime < min_contract_react_time { return Err(Error::Protocol("locktime too short")); } @@ -908,10 +931,15 @@ mod test { let (pub1, pub2) = read_pubkeys_from_contract_reedimscript(&contract_script).unwrap(); // Validates if contract outpoint is correct - assert!( - is_contract_out_valid(&contract_tx.output[0], &pub1, &pub2, hashvalue, locktime) - .is_ok() - ); + assert!(is_contract_out_valid( + &contract_tx.output[0], + &pub1, + &pub2, + hashvalue, + locktime, + 2 + ) + .is_ok()); // Validate if the contract transaction is spending correctl utxo assert!(validate_contract_tx(&contract_tx, Some(&spending_utxo), &contract_script).is_ok()); diff --git a/src/lib.rs b/src/lib.rs index ee26ad9e..10dd172e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -425,6 +425,8 @@ pub fn run_taker( send_amount, maker_count, tx_count, + required_confirms: 1, + fee_rate: 1000, //satoshis per thousand vbytes, i.e. 1000 = 1 sat/vb }, ); } diff --git a/src/maker_protocol.rs b/src/maker_protocol.rs index 53c678ef..6f831466 100644 --- a/src/maker_protocol.rs +++ b/src/maker_protocol.rs @@ -20,7 +20,8 @@ use itertools::izip; use crate::contracts; use crate::contracts::SwapCoin; use crate::contracts::{ - find_funding_output, read_hashvalue_from_contract, read_locktime_from_contract, + calculate_coinswap_fee, find_funding_output, read_hashvalue_from_contract, + read_locktime_from_contract, MAKER_FUNDING_TX_VBYTE_SIZE, }; use crate::error::Error; use crate::messages::{ @@ -33,6 +34,14 @@ use crate::wallet_sync::{IncomingSwapCoin, OutgoingSwapCoin, Wallet, WalletSwapC use crate::watchtower_client::{ping_watchtowers, register_coinswap_with_watchtowers}; use crate::watchtower_protocol::{ContractTransaction, ContractsInfo}; +//TODO this goes in the config file +const ABSOLUTE_FEE_SAT: u64 = 1000; +const AMOUNT_RELATIVE_FEE_PPB: u64 = 10_000_000; +const TIME_RELATIVE_FEE_PPB: u64 = 100_000; +const REQUIRED_CONFIRMS: i32 = 1; +const MINIMUM_LOCKTIME: u16 = 3; +const MIN_SIZE: u64 = 10000; + //used to configure the maker do weird things for testing #[derive(Debug, Clone, Copy)] pub enum MakerBehavior { @@ -301,10 +310,13 @@ async fn handle_message( let tweakable_point = wallet.read().unwrap().get_tweakable_keypair().1; connection_state.allowed_message = ExpectedMessage::SignSendersContractTx; Some(MakerToTakerMessage::Offer(Offer { - absolute_fee: 1000, - amount_relative_fee: 0.005, + absolute_fee_sat: ABSOLUTE_FEE_SAT, + amount_relative_fee_ppb: AMOUNT_RELATIVE_FEE_PPB, + time_relative_fee_ppb: TIME_RELATIVE_FEE_PPB, + required_confirms: REQUIRED_CONFIRMS, + minimum_locktime: MINIMUM_LOCKTIME, max_size, - min_size: 10000, + min_size: MIN_SIZE, tweakable_point, })) } @@ -447,6 +459,7 @@ fn handle_sign_senders_contract_tx( txinfo.funding_input_value, message.hashvalue, message.locktime, + MINIMUM_LOCKTIME, &tweakable_privkey, &mut wallet.write().unwrap(), )?; @@ -498,6 +511,7 @@ fn handle_proof_of_funding( &funding_info, funding_output_index, proof.next_locktime, + MINIMUM_LOCKTIME, )?; incoming_swapcoin_keys.push(verify_result); } @@ -567,49 +581,67 @@ fn handle_proof_of_funding( )); } - //set up the next coinswap address in the route - let coinswap_fees = 10000; //TODO calculate them properly, and output log "potentially earned" + //set up the next coinswap in the route let incoming_amount = funding_outputs.iter().map(|o| o.value).sum::(); - log::debug!("incoming amount = {}", incoming_amount); - let amount = incoming_amount - coinswap_fees; + let coinswap_fees = calculate_coinswap_fee( + ABSOLUTE_FEE_SAT, + AMOUNT_RELATIVE_FEE_PPB, + TIME_RELATIVE_FEE_PPB, + incoming_amount, + 1, //time_in_blocks just 1 for now + ); + let miner_fees_paid_by_taker = + MAKER_FUNDING_TX_VBYTE_SIZE * proof.next_fee_rate * (proof.next_coinswap_info.len() as u64) + / 1000; + let outgoing_amount = incoming_amount - coinswap_fees - miner_fees_paid_by_taker; + + let (my_funding_txes, outgoing_swapcoins, total_miner_fee) = + wallet.write().unwrap().initalize_coinswap( + &rpc, + outgoing_amount, + &proof + .next_coinswap_info + .iter() + .map(|nci| nci.next_coinswap_multisig_pubkey) + .collect::>(), + &proof + .next_coinswap_info + .iter() + .map(|nci| nci.next_hashlock_pubkey) + .collect::>(), + hashvalue, + proof.next_locktime, + proof.next_fee_rate, + )?; log::info!( - concat!( - "proof of funding valid. amount={}, incoming_locktime={} blocks, ", - "hashvalue={}, funding txes = {:?} creating own funding txes, outgoing_locktime={}", - "blocks " - ), - Amount::from_sat(incoming_amount), - read_locktime_from_contract(&proof.confirmed_funding_txes[0].contract_redeemscript) - .unwrap(), - //unwrap() as format of contract_redeemscript already checked in verify_proof_of_funding - hashvalue, + "Proof of funding valid. Incoming funding txes, txids = {:?}", proof .confirmed_funding_txes .iter() .map(|cft| cft.funding_tx.txid()) - .collect::>(), - proof.next_locktime + .collect::>() ); - - let (my_funding_txes, outgoing_swapcoins) = wallet.write().unwrap().initalize_coinswap( - &rpc, - amount, - &proof - .next_coinswap_info - .iter() - .map(|nci| nci.next_coinswap_multisig_pubkey) - .collect::>(), - &proof - .next_coinswap_info - .iter() - .map(|nci| nci.next_hashlock_pubkey) - .collect::>(), - hashvalue, + log::info!( + "incoming_amount={}, incoming_locktime={}, hashvalue={}", + Amount::from_sat(incoming_amount), + read_locktime_from_contract(&proof.confirmed_funding_txes[0].contract_redeemscript) + .unwrap(), + //unwrap() as format of contract_redeemscript already checked in verify_proof_of_funding + hashvalue + ); + log::info!( + concat!( + "outgoing_amount={}, outgoing_locktime={}, miner fees paid by taker={}, ", + "actual miner fee={}, coinswap_fees={}, POTENTIALLY EARNED={}" + ), + Amount::from_sat(outgoing_amount), proof.next_locktime, - )?; - - log::debug!("My Funding Transactions = {:#?}", my_funding_txes); + Amount::from_sat(miner_fees_paid_by_taker), + Amount::from_sat(total_miner_fee), + Amount::from_sat(coinswap_fees), + Amount::from_sat(incoming_amount - outgoing_amount - total_miner_fee) + ); connection_state.pending_funding_txes = Some(my_funding_txes); connection_state.outgoing_swapcoins = Some(outgoing_swapcoins); diff --git a/src/messages.rs b/src/messages.rs index 3c918781..e13c3500 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -66,6 +66,7 @@ pub struct ProofOfFunding { pub confirmed_funding_txes: Vec, pub next_coinswap_info: Vec, pub next_locktime: u16, + pub next_fee_rate: u64, } #[derive(Debug, Serialize, Deserialize)] @@ -124,8 +125,11 @@ pub struct MakerHello { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Offer { - pub absolute_fee: u32, - pub amount_relative_fee: f32, + pub absolute_fee_sat: u64, + pub amount_relative_fee_ppb: u64, + pub time_relative_fee_ppb: u64, + pub required_confirms: i32, + pub minimum_locktime: u16, pub max_size: u64, pub min_size: u64, pub tweakable_point: PublicKey, diff --git a/src/taker_protocol.rs b/src/taker_protocol.rs index 5cf8e2c0..3651ba1b 100644 --- a/src/taker_protocol.rs +++ b/src/taker_protocol.rs @@ -25,9 +25,9 @@ use itertools::izip; use crate::contracts; use crate::contracts::SwapCoin; use crate::contracts::{ - create_contract_redeemscript, create_receivers_contract_tx, find_funding_output, - read_pubkeys_from_multisig_redeemscript, sign_contract_tx, validate_contract_tx, - WatchOnlySwapCoin, REFUND_LOCKTIME, REFUND_LOCKTIME_STEP, + calculate_coinswap_fee, create_contract_redeemscript, create_receivers_contract_tx, + find_funding_output, read_pubkeys_from_multisig_redeemscript, sign_contract_tx, + validate_contract_tx, WatchOnlySwapCoin, MAKER_FUNDING_TX_VBYTE_SIZE, }; use crate::error::Error; use crate::messages::{ @@ -46,11 +46,17 @@ use crate::watchtower_protocol::{ check_for_broadcasted_contract_txes, ContractTransaction, ContractsInfo, }; +//relatively low value for now so that its easier to test on regtest +pub const REFUND_LOCKTIME: u16 = 3; //in blocks +pub const REFUND_LOCKTIME_STEP: u16 = 3; //in blocks + #[derive(Debug, Clone, Copy)] pub struct TakerConfig { pub send_amount: u64, pub maker_count: u16, pub tx_count: u32, + pub required_confirms: i32, + pub fee_rate: u64, } #[tokio::main] @@ -105,7 +111,7 @@ async fn send_coinswap( &first_maker.offer.tweakable_point, config.tx_count, ); - let (my_funding_txes, outgoing_swapcoins) = wallet + let (my_funding_txes, outgoing_swapcoins, _my_total_miner_fee) = wallet .initalize_coinswap( rpc, config.send_amount, @@ -113,6 +119,7 @@ async fn send_coinswap( &first_maker_hashlock_pubkeys, hashvalue, first_swap_locktime, + config.fee_rate, ) .unwrap(); let first_maker_senders_contract_sigs = match request_senders_contract_tx_signatures( @@ -159,13 +166,13 @@ async fn send_coinswap( log::info!("Broadcasting My Funding Tx: {}", txid); assert_eq!(txid, my_funding_tx.txid()); } - log::info!("Waiting for funding Tx to confirm"); let (mut funding_txes, mut funding_tx_merkleproofs) = wait_for_funding_tx_confirmation( rpc, &my_funding_txes .iter() .map(|tx| tx.txid()) .collect::>(), + first_maker.offer.required_confirms, &[], &mut None, ) @@ -244,6 +251,7 @@ async fn send_coinswap( send_proof_of_funding_and_get_contract_txes( &mut socket_reader, &mut socket_writer, + ¤t_maker, &funding_txes, &funding_tx_merkleproofs, &this_maker_multisig_redeemscripts, @@ -253,6 +261,7 @@ async fn send_coinswap( &next_peer_multisig_pubkeys, &next_peer_hashlock_pubkeys, maker_refund_locktime, + config.fee_rate, &this_maker_contract_txes, hashvalue, ) @@ -359,7 +368,6 @@ async fn send_coinswap( }; //scope ends here closing socket active_maker_addresses.push(current_maker.address.clone()); - log::info!("Waiting for funding transaction confirmations",); let wait_for_confirm_result = wait_for_funding_tx_confirmation( rpc, &maker_sign_sender_and_receiver_contracts @@ -371,6 +379,11 @@ async fn send_coinswap( .txid }) .collect::>(), + if is_taker_next_peer { + config.required_confirms + } else { + next_maker.offer.required_confirms + }, &watchonly_swapcoins .iter() .map(|watchonly_swapcoin_list| { @@ -756,11 +769,16 @@ async fn request_receivers_contract_tx_signatures( async fn wait_for_funding_tx_confirmation( rpc: &Client, funding_txids: &[Txid], + required_confirmations: i32, contract_to_watch: &[Vec], last_checked_block_height: &mut Option, ) -> Result, Vec)>, Error> { let mut txid_tx_map = HashMap::::new(); let mut txid_blockhash_map = HashMap::::new(); + log::info!( + "Waiting for funding transaction confirmations ({} conf required)", + required_confirmations + ); loop { for txid in funding_txids { if txid_tx_map.contains_key(txid) { @@ -772,14 +790,18 @@ async fn wait_for_funding_tx_confirmation( Err(_e) => continue, }; //TODO handle confirm<0 - if gettx.info.confirmations >= 1 { + if gettx.info.confirmations >= required_confirmations { txid_tx_map.insert(*txid, deserialize::(&gettx.hex).unwrap()); txid_blockhash_map.insert(*txid, gettx.info.blockhash.unwrap()); - log::debug!("funding tx {} reached 1 confirmation(s)", txid); + log::debug!( + "funding tx {} reached {} confirmation(s)", + txid, + required_confirmations + ); } } if txid_tx_map.len() == funding_txids.len() { - log::info!("Funding Transaction confirmed"); + log::info!("Funding Transactions confirmed"); let txes = funding_txids .iter() .map(|txid| txid_tx_map.get(txid).unwrap().clone()) @@ -858,6 +880,7 @@ fn get_swapcoin_multisig_contract_redeemscripts_txes( async fn send_proof_of_funding_and_get_contract_txes( socket_reader: &mut BufReader>, socket_writer: &mut WriteHalf<'_>, + this_maker: &OfferAddress, funding_txes: &[Transaction], funding_tx_merkleproofs: &[String], this_maker_multisig_redeemscripts: &[Script], @@ -866,7 +889,8 @@ async fn send_proof_of_funding_and_get_contract_txes( this_maker_hashlock_nonces: &[SecretKey], next_peer_multisig_pubkeys: &[PublicKey], next_peer_hashlock_pubkeys: &[PublicKey], - maker_refund_locktime: u16, + next_maker_refund_locktime: u16, + next_maker_fee_rate: u64, this_maker_contract_txes: &[Transaction], hashvalue: Hash160, ) -> Result<(SignSendersAndReceiversContractTxes, Vec