Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

swapd: fee estimate from syncer (electrum) #533

Merged
merged 11 commits into from
Jul 11, 2022
2 changes: 1 addition & 1 deletion doc/sequencediagram.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions doc/sequencediagram.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@ m_swap -> m_wallet : Ctl Hello
m_farcasterd -> m_farcasterd:launch syncer
m_farcasterd -> m_swap : Ctl MakeSwap

m_swap->m_syncer:If Bob, Ctl EstimateFee (btc)
m_swap -> peerd : Msg MakerCommit
t_swap <- peerd : Msg MakerCommit
// TODO: verify that swapd launches no matter what
t_syncer <- t_swap : Ctl WatchHeight
t_syncer <- t_swap : if Bob, Watch Arbitrating Funding Address
t_syncer<-t_swap:If Bob, Ctl EstimateFee (btc)
TheCharlatan marked this conversation as resolved.
Show resolved Hide resolved
t_swap -> t_wallet : Msg MakerCommit
t_wallet -> t_swap : Ctl RevealProof (taker is sender)
t_swap -> peerd : Msg RevealProof (taker is sender)
Expand Down
102 changes: 80 additions & 22 deletions src/swapd/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use crate::databased::checkpoint_handle_multipart_receive;
use crate::databased::checkpoint_send;
use crate::service::Endpoints;
use crate::syncerd::bitcoin_syncer::p2wpkh_signed_tx_fee;
use crate::syncerd::{FeeEstimation, FeeEstimations};
use crate::{
rpc::request::Outcome,
rpc::request::{BitcoinFundingInfo, FundingInfo, MoneroFundingInfo},
Expand Down Expand Up @@ -167,6 +169,7 @@ pub fn run(
bitcoin_amount,
awaiting_funding: false,
xmr_addr_addendum: None,
btc_fee_estimate_sat_per_kvb: None,
};

let runtime = Runtime {
Expand Down Expand Up @@ -510,6 +513,23 @@ impl Runtime {
trace!("received remote commitment");
self.state.t_sup_remote_commit(remote_commit.clone());

let watch_height_btc_task = self.syncer_state.watch_height(Coin::Bitcoin);
endpoints.send_to(
ServiceBus::Ctl,
self.identity(),
self.syncer_state.bitcoin_syncer(),
Request::SyncerTask(watch_height_btc_task),
)?;

let watch_height_xmr_task = self.syncer_state.watch_height(Coin::Monero);

endpoints.send_to(
ServiceBus::Ctl,
self.identity(),
self.syncer_state.monero_syncer(),
Request::SyncerTask(watch_height_xmr_task),
)?;

if self.state.swap_role() == SwapRole::Bob {
let addr = self
.state
Expand All @@ -527,23 +547,16 @@ impl Runtime {
Request::SyncerTask(task),
)?;
}
let btc_fee_task = self.syncer_state.estimate_fee_btc();
endpoints.send_to(
ServiceBus::Ctl,
self.identity(),
self.syncer_state.bitcoin_syncer(),
Request::SyncerTask(btc_fee_task),
)?;
std::thread::sleep(Duration::from_secs_f32(2.0));
}
let watch_height_btc_task = self.syncer_state.watch_height(Coin::Bitcoin);
endpoints.send_to(
ServiceBus::Ctl,
self.identity(),
self.syncer_state.bitcoin_syncer(),
Request::SyncerTask(watch_height_btc_task),
)?;

let watch_height_xmr_task = self.syncer_state.watch_height(Coin::Monero);

endpoints.send_to(
ServiceBus::Ctl,
self.identity(),
self.syncer_state.monero_syncer(),
Request::SyncerTask(watch_height_xmr_task),
)?;
self.send_wallet(msg_bus, endpoints, request)?;
}
Msg::TakerCommit(_) => {
Expand Down Expand Up @@ -642,15 +655,23 @@ impl Runtime {
}
pending_requests.push(pending_request);

if let Some(address) = self.state.b_address().cloned() {
if let (Some(address), Some(sat_per_kvb)) = (
self.state.b_address().cloned(),
self.syncer_state.btc_fee_estimate_sat_per_kvb,
) {
let swap_id = self.swap_id();
let fees = bitcoin::Amount::from_sat(200); // FIXME
let amount = self.syncer_state.bitcoin_amount + fees;
let vsize = 94;
TheCharlatan marked this conversation as resolved.
Show resolved Hide resolved
let nr_inputs = 1;
let total_fees = bitcoin::Amount::from_sat(
p2wpkh_signed_tx_fee(sat_per_kvb, vsize, nr_inputs),
);
let amount = self.syncer_state.bitcoin_amount + total_fees;
info!(
"{} | Send {} to {}",
"{} | Send {} to {}, this includes {} for the Lock transaction network fees",
swap_id.bright_blue_italic(),
amount.bright_green_bold(),
address.addr(),
total_fees,
);
self.state.b_sup_required_funding_amount(amount);
let req = Request::FundingInfo(FundingInfo::Bitcoin(
Expand All @@ -670,6 +691,13 @@ impl Runtime {
req,
)?
}
} else {
error!(
"swap_role: {}, trade_role: {:?}",
self.state.swap_role(),
self.state.trade_role()
);
error!("Not Some(address) or not Some(sat_per_kvb)");
}
}
}
Expand Down Expand Up @@ -728,6 +756,12 @@ impl Runtime {
]) {
let tx = tx.clone().extract_tx();
let txid = tx.txid();
debug!(
"tx_label: {}, vsize: {}, outs: {}",
tx_label,
tx.vsize(),
tx.output.len()
);
if !self.syncer_state.is_watched_tx(&tx_label) {
let task = self.syncer_state.watch_tx_btc(txid, tx_label);
endpoints.send_to(
Expand Down Expand Up @@ -1002,6 +1036,18 @@ impl Runtime {
Some(remote_commit),
);

std::thread::sleep(Duration::from_secs_f32(5.0));
let btc_fee_task = self.syncer_state.estimate_fee_btc();
endpoints.send_to(
ServiceBus::Ctl,
self.identity(),
self.syncer_state.bitcoin_syncer(),
Request::SyncerTask(btc_fee_task),
)?;

// syncer takes too long to give a fee
std::thread::sleep(Duration::from_secs_f32(2.0));

trace!("sending peer MakerCommit msg {}", &local_commit);
self.send_peer(endpoints, Msg::MakerCommit(local_commit))?;
self.state_update(endpoints, next_state)?;
Expand Down Expand Up @@ -1094,7 +1140,9 @@ impl Runtime {
tx,
}) if self.state.swap_role() == SwapRole::Alice
&& self.syncer_state.tasks.watched_addrs.contains_key(id)
&& !self.state.a_xmr_locked() =>
&& !self.state.a_xmr_locked()
&& self.syncer_state.tasks.watched_addrs.get(id).unwrap()
== &TxLabel::AccLock =>
{
debug!(
"Event details: {} {:?} {} {:?} {:?}",
Expand Down Expand Up @@ -1957,8 +2005,18 @@ impl Runtime {
Event::TransactionRetrieved(event) => {
debug!("{}", event)
}
Event::FeeEstimation(event) => {
debug!("{}", event)
Event::FeeEstimation(FeeEstimation {
fee_estimations:
FeeEstimations::BitcoinFeeEstimation {
high_priority_sats_per_kvbyte,
low_priority_sats_per_kvbyte: _,
},
id: _,
}) => {
// FIXME handle low priority as well
info!("fee: {} sat/kvB", high_priority_sats_per_kvbyte);
self.syncer_state.btc_fee_estimate_sat_per_kvb =
Some(*high_priority_sats_per_kvbyte);
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/swapd/syncer_client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::syncerd::BroadcastTransaction;
use crate::syncerd::EstimateFee;
use crate::syncerd::SweepBitcoinAddress;
use crate::syncerd::TransactionBroadcasted;
use crate::{
Expand Down Expand Up @@ -57,6 +58,7 @@ pub struct SyncerState {
pub bitcoin_amount: bitcoin::Amount,
pub xmr_addr_addendum: Option<XmrAddressAddendum>,
pub awaiting_funding: bool,
pub btc_fee_estimate_sat_per_kvb: Option<u64>,
}
impl SyncerState {
pub fn task_lifetime(&self, coin: Coin) -> u64 {
Expand Down Expand Up @@ -98,6 +100,16 @@ impl SyncerState {
})
}

pub fn estimate_fee_btc(&mut self) -> Task {
let id = self.tasks.new_taskid();
let task = Task::EstimateFee(EstimateFee {
id,
lifetime: self.task_lifetime(Coin::Bitcoin),
});
self.tasks.tasks.insert(id, task.clone());
task
}

pub fn watch_tx_btc(&mut self, txid: Txid, tx_label: TxLabel) -> Task {
let id = self.tasks.new_taskid();
self.tasks.watched_txs.insert(id, tx_label);
Expand Down
39 changes: 24 additions & 15 deletions src/syncerd/bitcoin_syncer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -487,17 +487,8 @@ fn sweep_address(

// TODO (maybe): make blocks_until_confirmation or fee_btc_per_kvb configurable by user (see FeeStrategy)
let blocks_until_confirmation = 2;
let fee_btc_per_kvb = client.estimate_fee(blocks_until_confirmation)?;
let fee_sat_per_vb = fee_btc_per_kvb * 1e5;
// Transaction size calculation: https://bitcoinops.org/en/tools/calc-size/
// The items in the witness are discounted by a factor of 4 (witness discount)
// The size used here is ceil(input p2wpkh witness)
// Input Witness:= ceil(nr. of items field + (length field + signature + public key) / p2wpkh witness discount
// Input Witness:= ceil(0.25 + (1 + 73 + 34) / 4))
// := ceil(27.25)
let vsize_per_p2wpkh_input_witness = 28;
let signed_tx_size = unsigned_tx.vsize() + vsize_per_p2wpkh_input_witness * unspent_txs.len();
let fee = (fee_sat_per_vb.ceil() as u64) * signed_tx_size as u64;
let fee_sat_per_kvb = (client.estimate_fee(blocks_until_confirmation)? * 1.0e8).ceil() as u64;
let fee = p2wpkh_signed_tx_fee(fee_sat_per_kvb, unsigned_tx.vsize(), unspent_txs.len());

unsigned_tx.output[0].value = in_amount - fee;
let mut psbt = bitcoin::util::psbt::PartiallySignedTransaction::from_unsigned_tx(unsigned_tx)
Expand Down Expand Up @@ -886,7 +877,7 @@ fn estimate_fee_polling(
if let Ok(client) = Client::new(&electrum_server) {
loop {
match client
.estimate_fee(high_priority_confs)
.estimate_fee(high_priority_confs) // docs say sat/kB, but its BTC/kvB
.and_then(
|high_priority| match client.estimate_fee(low_priority_confs) {
Ok(low_priority) => Ok((high_priority, low_priority)),
Expand All @@ -897,10 +888,9 @@ fn estimate_fee_polling(
let mut state_guard = state.lock().await;
state_guard
.fee_estimated(FeeEstimations::BitcoinFeeEstimation {
high_priority_sats_per_vbyte: (high_priority * 1e5 as f64)
.ceil()
high_priority_sats_per_kvbyte: (high_priority * 1.0e8).ceil()
as u64,
low_priority_sats_per_vbyte: (low_priority * 1e5 as f64).ceil()
low_priority_sats_per_kvbyte: (low_priority * 1.0e8).ceil()
as u64,
})
.await;
Expand Down Expand Up @@ -1124,3 +1114,22 @@ fn logging(txs: &[AddressTx], address: &BtcAddressAddendum) {
);
});
}

/// Input fee in sat_per_kvb, output fee in sat units
pub fn p2wpkh_signed_tx_fee(
fee_sat_per_kvb: u64,
unsigned_tx_vsize: usize,
nr_inputs: usize,
) -> u64 {
// Transaction size calculation: https://bitcoinops.org/en/tools/calc-size/
// The items in the witness are discounted by a factor of 4 (witness discount)
// The size used here is ceil(input p2wpkh witness)
// Input Witness:= ceil(nr. of items field + (length field + signature + public key) / p2wpkh witness discount
// Input Witness:= ceil(0.25 + (1 + 73 + 34) / 4))
// := ceil(27.25)
let vsize_per_p2wpkh_input_witness = 28;
let signed_tx_size = unsigned_tx_vsize + vsize_per_p2wpkh_input_witness * nr_inputs;
let fee = fee_sat_per_kvb as f64 * signed_tx_size as f64 * 1e-3;
// after multiplication we can safely convert
fee.ceil() as u64
}
6 changes: 3 additions & 3 deletions src/syncerd/syncer_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,13 +343,13 @@ impl SyncerState {
.insert(self.task_count.into(), source.clone());

// try to emit an event immediately from the cached values
if let Some(fee_estimations) = self.fee_estimation.clone() {
if let Some(ref fee_estimations) = &self.fee_estimation {
send_event(
&self.tx_event,
&mut vec![(
Event::FeeEstimation(FeeEstimation {
id: task.id,
fee_estimations,
fee_estimations: fee_estimations.clone(),
}),
source,
)],
Expand Down Expand Up @@ -622,7 +622,7 @@ impl SyncerState {

pub async fn fee_estimated(&mut self, fee_estimations: FeeEstimations) {
// Emit fee estimation events
if self.fee_estimation != Some(fee_estimations.clone()) {
if self.fee_estimation.as_ref() != Some(&fee_estimations) {
for (id, task) in self.watch_fee_estimation.iter() {
send_event(
&self.tx_event,
Expand Down
5 changes: 3 additions & 2 deletions src/syncerd/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,11 @@ pub struct FeeEstimation {

#[derive(Clone, Debug, Display, StrictEncode, StrictDecode, Eq, PartialEq, Hash)]
#[display(Debug)]
// the sats per kvB is because we need u64 for Eq, PartialEq and Hash
TheCharlatan marked this conversation as resolved.
Show resolved Hide resolved
pub enum FeeEstimations {
BitcoinFeeEstimation {
high_priority_sats_per_vbyte: u64,
low_priority_sats_per_vbyte: u64,
high_priority_sats_per_kvbyte: u64,
low_priority_sats_per_kvbyte: u64,
},
}

Expand Down
8 changes: 4 additions & 4 deletions tests/functional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,15 @@ fn assert_fee_estimation_received(request: Request) {
Event::FeeEstimation(FeeEstimation {
fee_estimations:
FeeEstimations::BitcoinFeeEstimation {
high_priority_sats_per_vbyte,
low_priority_sats_per_vbyte,
high_priority_sats_per_kvbyte,
low_priority_sats_per_kvbyte,
},
..
}),
..
}) => {
assert!(high_priority_sats_per_vbyte >= 1);
assert!(low_priority_sats_per_vbyte >= 1);
assert!(high_priority_sats_per_kvbyte >= 1000);
assert!(low_priority_sats_per_kvbyte >= 1000);
}
_ => {
panic!("expected syncerd bridge event");
Expand Down