winnow

How a payment is made

Choose coins and a fee, build the payment, review and sign, then relay it to Bitcoin peers. Confirmation comes from a later block.

Four steps

  1. Select enough coins for the payment and fee.
  2. Build a transaction and return any change to your wallet.
  3. Review the address, amount and fee before signing.
  4. Broadcast through peers, then distinguish relay from confirmation.

2. One spend shape

Everyday spends are BIP86 P2TR key-path. The signed witness is one 64-byte Schnorr signature (SIGHASH_DEFAULT); fee math uses 66 witness bytes per input (compact-size count + length + 64). Script-path and MuSig2 shapes belong to vaults and plug into the same builder via witnessBytesPerInput.

The phone receives only P2TR. It pays standard P2PKH, P2SH, P2WPKH, P2WSH and P2TR addresses. Destination diversity is a payment-layer concern; the wallet’s own outputs stay one type so coin selection, change, and watch-list code have a single script shape.

Transactions are version 2, empty scriptSigs, and sequence 0xFFFFFFFD (locktime-disabled, BIP125 opt-in RBF). A pending send with change can be fee-bumped in the transaction history; signaling replacement from the first broadcast avoids a policy surprise when it is needed.


3. Coin selection and change

Largest-first, with Bitcoin Core’s dust rule (policy.cpp GetDustThreshold). At the default dust relay feerate (3000 sat/kvB) a 43-byte P2TR output is dust below 330 sats. The often-quoted 294 sats is the P2WPKH threshold; using it here would produce outputs some peers will not relay.

The loop accumulates the largest UTXOs until the selection covers the payment plus the fee of the transaction as currently shaped (each added input grows vsize, so the fee target moves). A change output is created when the remainder clears the change script’s dust threshold; otherwise the remainder is folded into the fee. Feerates outside (0, 10_000] sat/vB are rejected by Winnow’s fee safety limit. This is a wallet policy, not a network-wide fee ceiling. Payment outputs that are themselves dust are rejected before a transaction is built — a sub-dust payment would strand the inputs in a tx no peer will take. A selection whose transaction would exceed Bitcoin Core’s standardness ceiling (MAX_STANDARD_TX_WEIGHT, 400,000 weight units, 100,000 vbytes) is refused before anything is signed, to stay within default relay policy. Coinbase outputs are credited when their block matches, but they are not selected until exactly 100 confirmations (COINBASE_MATURITY), as required by consensus.

Change, when any, goes to the next unused internal address (tr(…/<0;1>/*), choice 1). Its position among the outputs is chosen uniformly at random so change is not “always last” — a well-known chain-analysis fingerprint. After a successful broadcast the spent UTXOs leave the local set immediately and the change UTXO enters it at height 0, so a second send cannot double-spend the unconfirmed inputs. Height 0 is pending, never “confirmed.”

Commit-after-broadcast. Wallet.buildSend produces a signed transaction without mutating wallet state. commit runs only after the bytes have been handed to the broadcaster. Committing first and then failing to leave the device would strand inputs in a locally-spent, on-chain-unspent limbo that forward-only scanning cannot repair.


4. Fees without a mempool

A filter-only client does not see the fee market. Feerates of stranger transactions require their input amounts, which means recursively fetching parents — the bandwidth blowup the read side refused. So this product does not estimate from gossip.

Resolution, strongest first (FeePolicy):

  1. User override (sat/vB), when the send form set one.
  2. Median of this wallet’s own recently confirmed spends. Those feerates are exact: the wallet knows every input amount because it selected them.
  3. Static presets — low 2, medium 5, high 12 sat/vB. Deliberately above typical minima. A mempool-blind wallet must overpay rather than stall.

The result is clamped from below by the strictest BIP133 feefilter among connected peers (the minimum that will relay at all). If every remaining peer’s floor later rises above a pending transaction’s feerate, the broadcaster emits feeFloorExceeded once; the caller decides whether to bump. There is no live histogram and no “arrives in 10 minutes” claim.

This is cruder than any mempool-aware estimator. It is also the only estimator that does not reintroduce a server or a persistent relay subscription. Owned, and said in the send UI as a rate, not as a promise.

4.1 Replacing a stuck send

Winnow persists the exact inputs and raw transaction for each locally-created pending send. A replacement spends those same inputs, preserves every recipient output and its ordering, and pays the increase by shrinking the existing change output. If the remainder falls below the change script’s dust threshold, change is removed and the whole remainder becomes fee. A changeless transaction cannot be bumped without adding an input, so the same-input flow refuses it rather than altering a recipient amount.

The builder enforces both fee dimensions of BIP125 policy: the replacement’s absolute fee and feerate are strictly higher, and the increase pays the default incremental relay fee for the replacement’s own virtual size. It uses the same PSBTv2 key-path signer as the original send. Building is mutation-free; the app broadcasts the replacement, commits the wallet-state swap, then cancels relay of the original. If the wallet commit fails, the replacement broadcaster entry is cancelled and the original keeps relaying.

History retains the old txid as replaced and adds the new pending entry. If the original wins the confirmation race before the replacement propagates, scanning discards the replacement descendant and its locally-counted change before applying the confirmed original. Conversely, if broadcasting succeeds but the replacement’s state commit fails, its exact input set still identifies it as the winner when scanned and removes the original pending change. Neither race can double-count balance.


5. Signing

The everyday path is PSBTv2 even for single-sig (BIP370/371): creator attaches witness UTXOs and Taproot key-origin metadata, the signer writes PSBT_IN_TAP_KEY_SIG, the finalizer extracts a raw transaction. One pipeline with vaults, instead of a private “just sign it” shortcut that would drift.

Secrets:

All secret-key operations go through libsecp256k1 (P256K). The sighash is BIP341 SIGHASH_DEFAULT. Auxiliary nonce randomness is 32 fresh bytes per signature.

Wallet amounts and UTXOs come from locally verified blocks, not Esplora. BIP341 signatures commit to input amounts; explorer pages are external manual checks and never feed the signer.


6. Unsupported wallet data

Winnow restores descriptor-derived coins. It refuses wallet files or import bundles containing legacy signing data that it cannot use, rather than dropping that data and showing coins it cannot spend.

A recovery phrase alone does not recover unsupported coin types. Keep the original wallet and backup and use compatible wallet software for those coins. See import and recovery.


7. Getting it out

TxBroadcaster is P2P relay, not an API client:

While the Send screen is open, a bounded mempool window watches for peers echoing our txid back. An echo is evidence the network has the bytes; it is not confirmation. The UI says “broadcast” / “relayed” / “seen in a block”, never “sent” at 0-conf.

Winnow does not POST transactions to Esplora. A transaction row can open the selected explorer after a warning, but P2P relay remains the broadcast path and the page’s answer is not consumed by the wallet.


8. What still leaks

Filters protect the read. The write is a public announcement.

There is no Dandelion, no Tor requirement, no equal-output coinjoin. A user who needs those wants a different product (or a vault plus a coordinator they bring themselves). This paper will not claim broadcast privacy the implementation does not provide.


9. Use cases

# Use case Mechanism
1 Pay a bech32m / bech32 / base58 address Decode → select → build (random change slot) → PSBT sign → broadcast
2 Pick a fee Override / observed median / preset, clamped by feefilter
3 “Is it out?” Peer getdata + txid echo while Send is open
4 “Is it done?” Filter match on a confirmed block; height 0 change flips to that height
5 App killed mid-relay Persisted pending set resumes backoff
6 Peers will not take this feerate feeFloorExceeded; user may bump (RBF already signaled)
7 Vault spend Same builder and broadcaster; signing is the vault ceremony

Related guides

How confirmation is detected · Shared-wallet approvals · Backup and recovery