Skip to content

Commit

Permalink
feat(electrum): include option for previous TxOuts for fee calculation
Browse files Browse the repository at this point in the history
The previous `TxOut` for transactions received from an external
wallet may be optionally added as floating `TxOut`s to `TxGraph`
to allow for fee calculation.
  • Loading branch information
LagginTimes committed May 8, 2024
1 parent 7a69d35 commit fdffdd0
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 14 deletions.
65 changes: 54 additions & 11 deletions crates/electrum/src/electrum_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ pub trait ElectrumExt {
///
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
/// transactions. `batch_size` specifies the max number of script pubkeys to request for in a
/// single batch request.
/// single batch request. `fetch_prev_txouts` specifies whether or not we want previous `TxOut`s
/// for fee calculation.
fn full_scan<K: Ord + Clone>(
&self,
request: FullScanRequest<K>,
stop_gap: usize,
batch_size: usize,
fetch_prev_txouts: bool,
) -> Result<ElectrumFullScanResult<K>, Error>;

/// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
Expand All @@ -38,13 +40,19 @@ pub trait ElectrumExt {
/// see [`SyncRequest`]
///
/// `batch_size` specifies the max number of script pubkeys to request for in a single batch
/// request.
/// request. `fetch_prev_txouts` specifies whether or not we want previous `TxOut`s for fee
/// calculation.
///
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
/// may include scripts that have been used, use [`full_scan`] with the keychain.
///
/// [`full_scan`]: ElectrumExt::full_scan
fn sync(&self, request: SyncRequest, batch_size: usize) -> Result<ElectrumSyncResult, Error>;
fn sync(
&self,
request: SyncRequest,
batch_size: usize,
fetch_prev_txouts: bool,
) -> Result<ElectrumSyncResult, Error>;
}

impl<E: ElectrumApi> ElectrumExt for E {
Expand All @@ -53,6 +61,7 @@ impl<E: ElectrumApi> ElectrumExt for E {
mut request: FullScanRequest<K>,
stop_gap: usize,
batch_size: usize,
fetch_prev_txouts: bool,
) -> Result<ElectrumFullScanResult<K>, Error> {
let mut request_spks = request.spks_by_keychain;

Expand Down Expand Up @@ -104,6 +113,11 @@ impl<E: ElectrumApi> ElectrumExt for E {
}
}

// Fetch previous `TxOut`s for fee calculation if flag is enabled.
if fetch_prev_txouts {
fetch_prev_txout(self, &mut request.tx_cache, &mut graph_update)?;
}

// check for reorgs during scan process
let server_blockhash = self.block_header(tip.height() as usize)?.block_hash();
if tip.hash() != server_blockhash {
Expand Down Expand Up @@ -133,14 +147,19 @@ impl<E: ElectrumApi> ElectrumExt for E {
Ok(ElectrumFullScanResult(update))
}

fn sync(&self, request: SyncRequest, batch_size: usize) -> Result<ElectrumSyncResult, Error> {
fn sync(
&self,
request: SyncRequest,
batch_size: usize,
fetch_prev_txouts: bool,
) -> Result<ElectrumSyncResult, Error> {
let mut tx_cache = request.tx_cache.clone();

let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
.cache_txs(request.tx_cache)
.set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk)));
let mut full_scan_res = self
.full_scan(full_scan_req, usize::MAX, batch_size)?
.full_scan(full_scan_req, usize::MAX, batch_size, fetch_prev_txouts)?
.with_confirmation_height_anchor();

let (tip, _) = construct_update_tip(self, request.chain_tip)?;
Expand All @@ -165,6 +184,11 @@ impl<E: ElectrumApi> ElectrumExt for E {
request.outpoints,
)?;

// Fetch previous `TxOut`s for fee calculation if flag is enabled.
if fetch_prev_txouts {
fetch_prev_txout(self, &mut tx_cache, &mut full_scan_res.graph_update)?;
}

Ok(ElectrumSyncResult(SyncResult {
chain_update: full_scan_res.chain_update,
graph_update: full_scan_res.graph_update,
Expand Down Expand Up @@ -374,7 +398,7 @@ fn populate_with_outpoints(
client: &impl ElectrumApi,
cps: &BTreeMap<u32, CheckPoint>,
tx_cache: &mut TxCache,
tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
outpoints: impl IntoIterator<Item = OutPoint>,
) -> Result<(), Error> {
for outpoint in outpoints {
Expand All @@ -399,18 +423,18 @@ fn populate_with_outpoints(
continue;
}
has_residing = true;
if tx_graph.get_tx(res.tx_hash).is_none() {
let _ = tx_graph.insert_tx(tx.clone());
if graph_update.get_tx(res.tx_hash).is_none() {
let _ = graph_update.insert_tx(tx.clone());
}
} else {
if has_spending {
continue;
}
let res_tx = match tx_graph.get_tx(res.tx_hash) {
let res_tx = match graph_update.get_tx(res.tx_hash) {
Some(tx) => tx,
None => {
let res_tx = fetch_tx(client, tx_cache, res.tx_hash)?;
let _ = tx_graph.insert_tx(Arc::clone(&res_tx));
let _ = graph_update.insert_tx(Arc::clone(&res_tx));
res_tx
}
};
Expand All @@ -424,7 +448,7 @@ fn populate_with_outpoints(
};

if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
let _ = tx_graph.insert_anchor(res.tx_hash, anchor);
let _ = graph_update.insert_anchor(res.tx_hash, anchor);
}
}
}
Expand Down Expand Up @@ -484,6 +508,25 @@ fn fetch_tx<C: ElectrumApi>(
})
}

// Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions,
// which we do not have by default. This data is needed to calculate the transaction fee.
fn fetch_prev_txout<C: ElectrumApi>(
client: &C,
tx_cache: &mut TxCache,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
) -> Result<(), Error> {
for tx in graph_update.clone().full_txs() {
for vin in tx.input.clone() {
let outpoint = vin.previous_output;
let prev_tx = fetch_tx(client, tx_cache, outpoint.txid)?;
for txout in prev_tx.output.clone() {
let _ = graph_update.insert_txout(outpoint, txout);
}
}
}
Ok(())
}

fn populate_with_spks<I: Ord + Clone>(
client: &impl ElectrumApi,
cps: &BTreeMap<u32, CheckPoint>,
Expand Down
26 changes: 26 additions & 0 deletions crates/electrum/tests/test_electrum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ fn scan_detects_confirmed_tx() -> Result<()> {
SyncRequest::from_chain_tip(recv_chain.tip())
.chain_spks(core::iter::once(spk_to_track)),
5,
true,
)?
.with_confirmation_time_height_anchor(&client)?;

Expand All @@ -83,6 +84,29 @@ fn scan_detects_confirmed_tx() -> Result<()> {
},
);

for tx in recv_graph.graph().full_txs() {
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
// floating txouts available from the transaction's previous outputs.
let fee = recv_graph
.graph()
.calculate_fee(&tx.tx)
.expect("fee must exist");

// Retrieve the fee in the transaction data from `bitcoind`.
let tx_fee = env
.bitcoind
.client
.get_transaction(&tx.txid, None)
.expect("Tx must exist")
.fee
.expect("Fee must exist")
.abs()
.to_sat() as u64;

// Check that the calculated fee matches the fee from the transaction data.
assert_eq!(fee, tx_fee);
}

Ok(())
}

Expand Down Expand Up @@ -132,6 +156,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
.sync(
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
5,
false,
)?
.with_confirmation_time_height_anchor(&client)?;

Expand Down Expand Up @@ -162,6 +187,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
.sync(
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
5,
false,
)?
.with_confirmation_time_height_anchor(&client)?;

Expand Down
4 changes: 2 additions & 2 deletions example-crates/example_electrum/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ fn main() -> anyhow::Result<()> {
};

let res = client
.full_scan::<_>(request, stop_gap, scan_options.batch_size)
.full_scan::<_>(request, stop_gap, scan_options.batch_size, false)
.context("scanning the blockchain")?
.with_confirmation_height_anchor();
(
Expand Down Expand Up @@ -303,7 +303,7 @@ fn main() -> anyhow::Result<()> {
});

let res = client
.sync(request, scan_options.batch_size)
.sync(request, scan_options.batch_size, false)
.context("scanning the blockchain")?
.with_confirmation_height_anchor();

Expand Down
2 changes: 1 addition & 1 deletion example-crates/wallet_electrum/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ fn main() -> Result<(), anyhow::Error> {
.inspect_spks_for_all_keychains(|_, _, _| std::io::stdout().flush().expect("must flush"));

let mut update = client
.full_scan(request, STOP_GAP, BATCH_SIZE)?
.full_scan(request, STOP_GAP, BATCH_SIZE, false)?
.with_confirmation_time_height_anchor(&client)?;

let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
Expand Down

0 comments on commit fdffdd0

Please sign in to comment.