Skip to content

Commit

Permalink
Handle accounting, fees, confirmations & locktimes
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
chris-belcher committed Feb 2, 2022
1 parent 55dac4a commit b6a5b22
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 73 deletions.
52 changes: 40 additions & 12 deletions src/contracts.rs
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"));
}
Expand Down Expand Up @@ -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<Signature, Error> {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"));
}

Expand Down Expand Up @@ -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());
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Expand Up @@ -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
},
);
}
Expand Down
108 changes: 70 additions & 38 deletions src/maker_protocol.rs
Expand Up @@ -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::{
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
}))
}
Expand Down Expand Up @@ -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(),
)?;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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::<u64>();
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::<Vec<PublicKey>>(),
&proof
.next_coinswap_info
.iter()
.map(|nci| nci.next_hashlock_pubkey)
.collect::<Vec<PublicKey>>(),
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::<Vec<Txid>>(),
proof.next_locktime
.collect::<Vec<Txid>>()
);

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::<Vec<PublicKey>>(),
&proof
.next_coinswap_info
.iter()
.map(|nci| nci.next_hashlock_pubkey)
.collect::<Vec<PublicKey>>(),
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);
Expand Down
8 changes: 6 additions & 2 deletions src/messages.rs
Expand Up @@ -66,6 +66,7 @@ pub struct ProofOfFunding {
pub confirmed_funding_txes: Vec<ConfirmedCoinSwapTxInfo>,
pub next_coinswap_info: Vec<NextCoinSwapTxInfo>,
pub next_locktime: u16,
pub next_fee_rate: u64,
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit b6a5b22

Please sign in to comment.