Skip to content

docs: Document TxUpdate temporal context requirements (supports Wizardsardine audit recommendation) #2133

@rafaelturon

Description

@rafaelturon

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

  • Blocking production usage
  • Nice-to-have / UX improvement
  • Developer experience / maintainability

Are you using BDK in a production project?

  • Yes
  • No
  • Not yet, but planning to

Which backend(s) are relevant (if any)?

  • Electrum
  • Esplora
  • Bitcoin Core RPC
  • None / not backend-related (e.g. bdk_chain, bdk_core)
  • Other (please specify): Custom chain source (not using bdk_electrum/bdk_esplora)

Project or organization (optional)

Additional context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions