Describe the enhancement
The TxUpdate struct and Wallet::apply_update() method do not document the
requirement that every transaction needs either a ConfirmationBlockTime anchor
or a seen_ats entry to be included in balance calculations. This was flagged
in the Wizardsardine BDK Audit Report (Q4 2024), which noted:
"It's surprising that seen_ats doesn't get populated in the sync result.
The documentation could at least mention the returned TxUpdate will always
have an empty seen_ats mapping and populating that is left as a responsibility
of the caller."
I encountered this independently while building a custom Electrum streaming chain
source (bdk-electrum-streaming-poc)
and wanted to share the concrete developer experience to reinforce the audit recommendation.
Use case
Documentation (The developer experience)
When constructing Update structs manually for a custom chain source:
let mut update = bdk_wallet::Update::default();
let tx: Transaction = /* fetched from Electrum get_history */;
let txid = tx.compute_txid();
// Transaction added without temporal context
update.tx_update.txs.push(tx.into());
// No anchor, no seen_at — developer doesn't know these are needed
wallet.apply_update(update)?; // Returns Ok(())
let balance = wallet.balance();
// balance.total() == 0 sats, despite tx having outputs to wallet addresses
The Ok(()) return provides no indication that the transaction was stored in the
graph but excluded from balance. I spent several hours debugging this before
discovering that the existing chain sources (bdk_esplora, bdk_electrum) handle
temporal context internally — bdk_esplora has an explicit
insert_anchor_or_seen_at_from_status() helper that inserts either an anchor or
seen_at for every transaction.
Once I understood the contract, the fix was straightforward:
// For confirmed transactions (height > 0):
update.tx_update.anchors.insert((
ConfirmationBlockTime {
block_id: BlockId { height, hash },
confirmation_time,
},
txid,
));
// For unconfirmed/mempool transactions:
update.tx_update.seen_ats.insert((txid, unix_timestamp));
Impact
This primarily affects developers building custom chain sources — anyone
constructing Update structs outside of bdk_electrum/bdk_esplora/bdk_bitcoind_rpc.
As the ecosystem grows (streaming Electrum, Nostr relay sync, compact block filters,
custom backends), more developers will encounter this undocumented contract.
Suggested documentation additions
I'm happy to submit a PR for any or all of these:
1. Doc comment on TxUpdate struct
anchors or seen_ats are stored in the graph but do not affect the balance.
///
/// ## Temporal context
/// To contribute to a wallet's balance, transactions must have an entry in either:
/// - [`anchors`]: for confirmed transactions.
/// - [`seen_ats`]: for unconfirmed transactions.
/// The built-in chain (`bdk_electrum`, `bdk_esplora`, `bdk_bitcoind_rpc`) handle this automatically.
/// Transactions lacking temporal context are stored but ignored by canonicalization.
/// Custom chain sources must populate these fields.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct TxUpdate<A = ()> {
seen_ats collection type: TxUpdate::seen_ats is a BTreeSet<(Txid, u64)>, requiring .insert((txid, timestamp)).
///
/// Note: this is a `HashSet<(Txid, u64)>`, not a `HashMap`.
/// Insert with `.insert((txid, timestamp))`, not `.insert(txid, timestamp)`.
pub seen_ats: HashSet<(Txid, u64)>,
2. Doc comment on Wallet::apply_update()
- TxGraph
apply_update: transactions without temporal context note.
///
/// **Note**: Transactions without temporal context (anchors or seen_ats)
/// will be stored but will not be considered canonical.
- IndexedTxGraph
apply_update: transactions without temporal context note.
///
/// **Note**: Transactions without temporal context (anchors or seen_ats)
/// will be stored but will not be considered canonical.
Impact
Are you using BDK in a production project?
Which backend(s) are relevant (if any)?
Project or organization (optional)
Additional context
Describe the enhancement
The
TxUpdatestruct andWallet::apply_update()method do not document therequirement that every transaction needs either a
ConfirmationBlockTimeanchoror a
seen_atsentry to be included in balance calculations. This was flaggedin the Wizardsardine BDK Audit Report (Q4 2024), which noted:
I encountered this independently while building a custom Electrum streaming chain
source (bdk-electrum-streaming-poc)
and wanted to share the concrete developer experience to reinforce the audit recommendation.
Use case
Documentation (The developer experience)
When constructing
Updatestructs manually for a custom chain source:The
Ok(())return provides no indication that the transaction was stored in thegraph but excluded from balance. I spent several hours debugging this before
discovering that the existing chain sources (
bdk_esplora,bdk_electrum) handletemporal context internally —
bdk_esplorahas an explicitinsert_anchor_or_seen_at_from_status()helper that inserts either an anchor orseen_atfor every transaction.Once I understood the contract, the fix was straightforward:
Impact
This primarily affects developers building custom chain sources — anyone
constructing
Updatestructs outside ofbdk_electrum/bdk_esplora/bdk_bitcoind_rpc.As the ecosystem grows (streaming Electrum, Nostr relay sync, compact block filters,
custom backends), more developers will encounter this undocumented contract.
Suggested documentation additions
I'm happy to submit a PR for any or all of these:
1. Doc comment on
TxUpdatestructanchorsorseen_atsare stored in the graph but do not affect the balance.seen_atscollection type:TxUpdate::seen_atsis aBTreeSet<(Txid, u64)>, requiring.insert((txid, timestamp)).2. Doc comment on
Wallet::apply_update()apply_update: transactions without temporal context note.apply_update: transactions without temporal context note.Impact
Are you using BDK in a production project?
Which backend(s) are relevant (if any)?
bdk_chain,bdk_core)Custom chain source (not using bdk_electrum/bdk_esplora)Project or organization (optional)
Additional context
(section on
seen_atsdocumentation)evicted-at/last-evictedtimestamps #1839 — Introducedevicted_at/last_evictedtimestamps (expanded thetemporal context model, further emphasizing the importance of documenting it)