Skip to content

Book fee estimate-vs-actual drift for on-chain BTC, not only Lightning #9

Description

@OliverD25

Money path. This one changes what the ledger records. Read the money-path
rule in CONTRIBUTING.md
before starting, and expect the pull-request template to ask which of the
nine invariants still hold and why. Labelled help wanted rather than
good first issue for that reason — the code is small, the reasoning is not.

The machinery already exists and only Lightning uses it.

post_settle_entry (src/crypto_processing_api/services/withdrawals.py:493)
takes an optional actual_wallet_fee, books the difference against the
estimate, and logs withdrawal.fee_drift_booked. The capability protocol is
ReportsActualFee (src/crypto_processing_api/services/backends.py:159).
LightningPayoutBackend implements it; BtcpayPayoutBackend does not.

So for on-chain BTC the ledger still books the estimated fee. Estimate and
actual differ on every transaction — fee markets move between quote and mine —
and the difference accumulates in the hot wallet with no entry explaining it.
Job C reports it as an unexplained custody difference, which is
docs/design/06-adversarial-critique.md
items #8 and #9. It is small per withdrawal and it never goes away.

The unknown to resolve first

Does BTCPay Greenfield expose the actual fee of a payout's transaction?
src/crypto_processing_api/gateway/btcpay_client.py reads a fee rate
(get_fee_rate, line 330) and nothing about a mined transaction's fee. The
wallet transactions endpoint is a candidate, as is looking the txid up
elsewhere.

Answering that question with evidence — an endpoint, a real response body from
the regtest stack, and what it contains for a payout — is a genuinely useful
outcome even if no code follows. Post it here.

If the answer is yes

Implement actual_wallet_fee on BtcpayPayoutBackend. Everything downstream
already works: withdrawals.py calls it when the backend has it, and
post_settle_entry books the drift.

  • Fee reads happen outside the settle transaction. The lock-decide-commit-
    then-call-out rule in CONTRIBUTING is not negotiable in this path.
  • Returning None must stay safe, and must mean "unknown", never "zero". A
    missing number that reads as zero is a silent loss.
  • Tests: tests/integration/test_fee_drift.py has the existing shape, and
    tests/unit/test_greenfield_fixtures.py shows how recorded real payloads are
    asserted rather than invented ones.

If the answer is no

Say so here with the evidence, and this becomes a documentation change instead:
the estimate-versus-actual gap is stated honestly in docs/security.md as a
known, bounded, unexplained drift, rather than left for an operator to
discover from a custody report.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: ledgerThe double-entry ledger and the money pathshelp wantedExtra attention is neededmoney-pathTouches ledger/ or services/ — the invariants apply

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions