Skip to content

Instantly share code, notes, and snippets.

@ValuedMammal
Last active March 13, 2026 18:09
Show Gist options
  • Select an option

  • Save ValuedMammal/10e282f3a335d06a75a88fbdd11730b0 to your computer and use it in GitHub Desktop.

Select an option

Save ValuedMammal/10e282f3a335d06a75a88fbdd11730b0 to your computer and use it in GitHub Desktop.
Review #337 - Don't fail in `build_fee_bump` for missing parent txid

Review #337 Q&A

  1. Did you review the PR, what did you think (ACK, NACK, etc)? What was your review process?
  2. What is the use case for the TxBuilder::add_foreign_utxo API? (1 pt)
    ans: The normal TxBuilder::add_utxo only accepts wallet UTXOs that are unspent, but there are times when we might want to include inputs from other wallets if building transactions collaboratively with other parties. For that we have add_foreign_utxo.
  3. What is Wallet::build_fee_bump designed to accomplish? Briefly describe the high-level steps (2 pt)
    ans: build_fee_bump takes a transaction ID representing the tx to be fee-bumped (via RBF) and returns a new TxBuilder populated with the parameters of the original transaction (version, recipients, etc).
    Steps:
    • Retrieve the original transaction data from the wallet
    • Recreate the original UTXOs by "processing the inputs"
    • Retain the original parameters (version, recipients) + include the previous fee
    • Make a best effort to remove the change output, since raising the fee means the value of the change has to be recalculated
  4. In what real world situation could the issue arise, i.e., failing to fee-bump a transaction due to a missing parent (1 pt)
    ans: As outlined in issue #325, if the original transaction spends an output of a parent that is external to the wallet, then the parent would be missing. Normally this isn't an issue when the parent is another wallet tx, but it's possible to create a tx sweeping a P2A output of a foreign tx which by definition is "anyone-can-spend". In that case we won't have the parent by default. This situation is common in some off-chain protocols such as Ark.

Implementation

  1. Issue #325 states that build_fee_bump returns an error if the P2A spend doesn't originate in the wallet. Prior to the fix, which line(s) of code proved to be problematic? What is the error returned? (2 pt)
    ans: L1758 begins with graph.get_tx() passing the txid of the input's previous outpoint (i.e. the parent transaction)
    ans: If get_tx returns None, then we return BuildFeeBumpError::UnknownUtxo from the map closure.
  2. How is the fix implemented? Can you think of another approach? (2 pt)
    ans: We query the TxGraph for the P2A output itself, which can exist in the graph without its containing transaction.
    ans: Issue #325 mentions another approach that involves removing (i.e. evicting) the original tx from the wallet and creating the replacement at a higher feerate using add_foreign_utxo. This works around the build_fee_bump limitation but is more of a manual intervention.
  3. Why is it reasonable to assume that previous txouts are available even when the parent transaction isn't part of the wallet? What happens if the previous txout isn't available? (2 pt)
    ans: The previous txouts are needed to calculate the fee/feerate of the original transaction
    ans: Otherwise a BuildFeeBumpError::FeeRateUnavailable error will occur
  4. What determines whether the original transaction input refers to a Utxo::Local or Utxo::Foreign UTXO? hint: what can we say about the previous output's script pubkey? (1 pt)
    ans: If the previous output's script pubkey is indexed by a wallet keychain, then index_of_spk will return Some((keychain, derivation_index)) indicating the script is derived from a wallet descriptor and the UTXO is local to the wallet, otherwise it must be foreign.
  5. Under what condition is the full parent transaction still required? (1 pt)
    ans: In case the UTXO we're looking for is Utxo::Local, we require the parent transaction in order to assign a value of the LocalOutput::chain_position.
  6. How does test_bump_fee_pay_to_anchor_foreign_utxo test that the issue is resolved? Did you spot any flaws in the test setup or execution? (2 pt)
    ans: The test checks that build_fee_bump succeeds when the original transaction contains a P2A spend
    ans: We also check that the replacement includes the expected outpoint among the inputs
    ans: A potential flaw is that the test hardcodes a satisfaction weight of 71 WU, although we know that P2A outputs don't require a signature to spend

bonus:

  • What security considerations must be taken into account when using add_foreign_utxo?
    ans: In case the PSBT input doesn't include the full previous transaction (non-witness utxo), then we're implicitly trusting the value of the txout to which the outpoint refers. Similarly we're trusting the value of the satisfaction weight since it's provided as an input to the function.

bonus:

  • Conceptually how do you define an input's satisfaction weight?
    ans: The satisfaction weight is the size of a fully signed input's script_sig and witness expressed in weight units
  • What explains the difference in satisfaction weight calculation in the Utxo::Local vs Utxo::Foreign cases?
    ans: If the UTXO is local to the wallet, the satisfaction weight is given by the descriptor's max_weight_to_satisfy. If we don't have the descriptor, we calculate the satisfaction weight by looking directly at the signed input using the formula serialize(script_sig).len() * 4 + serialize(witness).len().
  • What happens if a wallet's descriptor can't be "satisfied"?
    ans: At present the code will panic if a descriptor can't be satisfied, although it is common for the error to be caught upstream in miniscript before a Wallet can be created.

2026-3-9 Review BDK Wallet #337

Topic: Don't fail in build_fee_bump for missing parent txid
PR: bitcoindevkit/bdk_wallet#337
Author: @ValuedMammal

Background and Context

BDK Wallet's add_foreign_utxo API allows developers to include UTXOs not owned by the wallet in transactions, which is useful for collaborative transactions like payjoin. Prior to this PR, attempting to fee-bump (RBF) a transaction containing a foreign UTXO would fail with an UnknownUtxo error because build_fee_bump expected to find the parent transaction in the wallet. This limitation became problematic in real-world scenarios like sweeping pay-to-anchor (P2A) outputs from foreign transactions and then attempting to fee-bump them - evident in protocols like Ark. The fix refactors build_fee_bump to query the transaction graph directly for individual tx outputs rather than requiring the full parent transaction, leveraging the tx graph's support for "floating" txouts (outputs without their containing transaction). This retrofit preserves existing functionality while enabling fee-bumping of transactions with foreign UTXOs, making the API more flexible without breaking changes.

Commits

  • f15582df fix(wallet): Don't fail in build_fee_bump for missing parent txid
  • 992a08fd test: Add test_bump_fee_pay_to_anchor_foreign_utxo

Relevant Issues

Potential Follow-ups

Review

Concepts

  1. Did you review the PR, what did you think (ACK, NACK, etc)? What was your review process?
  2. What is the use case for the TxBuilder::add_foreign_utxo API? (1 pt)
  3. What is Wallet::build_fee_bump designed to accomplish? Briefly describe the high-level steps (2 pt)
  4. In what real world situation could the issue arise, i.e., failing to fee-bump a transaction due to a missing parent (1 pt)

Implementation

  1. Issue #325 states that build_fee_bump returns an error if the P2A spend doesn't originate in the wallet. Prior to the fix, which line(s) of code proved to be problematic? What is the error returned? (2 pt)
  2. How is the fix implemented? Can you think of another approach? (2 pt)
  3. Why is it reasonable to assume that previous txouts are available even when the parent transaction isn't part of the wallet? What happens if the previous txout isn't available? (2 pt)
  4. What determines whether the original transaction input refers to a Utxo::Local or Utxo::Foreign UTXO? hint: what can we say about the previous output's script pubkey? (1 pt)
  5. Under what condition is the full parent transaction still required? (1 pt)
  6. How does test_bump_fee_pay_to_anchor_foreign_utxo test that the issue is resolved? (2 pt)
    • Did you spot any flaws in the test setup or execution?

bonus: What security considerations must be taken into account when using add_foreign_utxo?

bonus: Conceptually how do you define an input's satisfaction weight?
- What explains the difference in satisfaction weight calculation in the Utxo::Local vs Utxo::Foreign cases?
- What happens if a wallet's descriptor can't be "satisfied"?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment