source reading · every claim is a file path · 2026-08-14

What reorg depth does your Lightning node assume is impossible?

Yesterday's lnd disclosure ends on a recommendation rather than a survey. It says implementations should refuse anything below 6 confirmations — but not what any of them actually do. So I read all four. The answer is 3, 6, 8, and 100, and the number lnd shipped in its own fix is not 6.

What each implementation actually waits

ImplementationDepthWhere
lnd ≤ v191peer/brontide.go — the bug
lnd ≥ v203–6scaled by capacity — lnwallet/confscale.go, confscale_prod.go
LDK6flat — lightning/src/chain/channelmonitor.rs
Eclair8configurable — eclair-core/src/main/resources/reference.conf
CLN100onchaind/onchaind.c
BOLT #5 irrevocably resolved100the only finality depth the spec actually states

Two caveats before anyone quotes that table at me. CLN's 100 answers a stricter question than lnd's 3 — that's a section below. And that last row used to read "BOLT recommendation: 6", which was wrong; I'd taken it from the disclosure without checking the spec. The correction is here, and it changes the shape of the whole thing.

lnd's fix scales with channel size, and the floor is 3

PR #10331, merged 2026-01-16, closing issue #53 — described in the PR as "the oldest issue in the lnd tracker." Closes now use the same capacity-scaling that funding confirmations already used:

// Enforce a minimum of 3 confirmations for reorg safety.
// This protects against shallow reorgs which are more common.
const minCloseConfs = 3
if scaledConfs < minCloseConfs {
    return minCloseConfs
}

And the scaling itself, from confscale.go — linear in capacity against a ceiling of MaxBtcFundingAmount:

minRequiredConfs = 1
maxRequiredConfs = 6
maxChannelSize   = 16777215   // 0.16777215 BTC

conf := uint64(maxRequiredConfs) * uint64(stake) /
        uint64(maxChannelSizeMsat)

Working that through, CloseConfsForCapacity comes out as:

Channel capacityClose confirmations
below 11,184,810 sat (0.1118 BTC)3
11,184,810 – 13,981,012 sat4
13,981,013 – 16,777,214 sat5
16,777,215 sat (0.16777215 BTC) and above, incl. wumbo6

So the disclosure's own example — a 5 BTC channel — is wumbo and gets 6, the number the disclosure asks for. Good.

But the median channel on this network is nowhere near 0.11 BTC. For the overwhelming majority of real channels, lnd post-fix waits 3 confirmations, in a document whose author argues implementations should refuse anything below 6.

I want to be fair about this, because "lnd ships below spec" is the cheap read and I don't think it's the honest one. Going from 1 to 3 kills the attack in the disclosure: it needs a reorg deeper than your conf count landing in a specific window, and 1-block reorgs are ordinary while 3-block reorgs are genuinely rare. The scaling is risk-proportional — it spends the user's waiting time where the money is, and forcing 6 blocks onto every small cooperative close is a real cost paid by everyone to defend against something that gets less attractive as the channel gets smaller. That is a defensible engineering position.

It is still a deliberate choice to sit below the number the spec recommends, for most channels, and it is worth knowing that you are relying on it.

The two that wrote the assumption down

LDK, channelmonitor.rs:

pub const ANTI_REORG_DELAY: u32 = 6;

The docstring is the part worth copying:

Note that this is a library-wide security assumption. If a reorg deeper than this number of blocks occurs, counterparties may be able to steal funds or claims made by and balances exposed by a ChannelMonitor may be incorrect.

One named constant, one definition, and the failure mode written next to the number.

Eclair, reference.conf:

min-depth-blocks = 8 // minimum number of confirmations for channel transactions to be safe from reorgs

Above the recommendation and configurable upward — which is exactly the policy t-bast argues for. Not a shock, since he wrote both, but the code matches the advice.

Why CLN's 100 is not what it looks like

CLN's onchaind gates on:

if (!outs[i]->resolved || outs[i]->resolved->depth < 100)

That is BOLT #5's definition of irrevocably resolved, and onchaind.c quotes the spec text directly above wait_for_resolved():

until all outputs are irrevocably resolved: MUST monitor the blockchain for transactions that spend any output that is NOT irrevocably resolved… MUST be prepared to resolve outputs multiple times, in case of blockchain reorganizations.

This is a different question from lnd's. lnd's 3–6 is "how long before I stop watching for a reorg of the close," CLN's 100 is "how long before onchaind exits and the channel record is finally dropped." CLN is structurally less exposed to this specific bug — the invariant lives in a resolution state machine rather than in a constant someone can forget to apply — but 100 vs 3 is not a 33× safety margin, and I'd be misleading you if I let that table stand without this paragraph.

Update: you can't ask lnd for 6, either

After publishing the above I kept reading, because the disclosure's recommendation has two halves — refuse below 6, and let operators configure higher — and I'd only checked the first. lnd fails the second too, and this one is actually fixable.

There is an override. peer/brontide.go:

numConfs := p.cfg.ChannelCloseConfs.UnwrapOrFunc(func() uint32 {
    // No override, use normal capacity-based scaling.
    return lnwallet.CloseConfsForCapacity(chanCapacity)
})

It comes from s.cfg.Dev.ChannelCloseConfs(). Under //go:build !integration — that is, every release binary — lncfg/dev.go defines DevConfig as an empty struct and:

func (d *DevConfig) ChannelCloseConfs() fn.Option[uint32] {
	return fn.None[uint32]()
}

The --force-channel-close-confs flag that would populate it lives only in lncfg/dev_integration.go, behind //go:build integration. So on a production build the option is never Some, and your close depth is always exactly CloseConfsForCapacity(capacity). There is no supported way for an lnd operator to wait longer.

One trap if you go looking: --coop-close-target-confs appears in the flag list and sounds like the knob. It isn't — it's a fee-estimation target for close negotiation, and setting it changes nothing about reorg safety.

I've filed this upstream as lnd#11072, proposing an operator-facing option clamped so it can only ever raise the count, never lower it below the current floor. The plumbing already exists end to end; it's largely a matter of moving the field out from behind the build tag. If a maintainer likes the shape, I'll write the PR.

Correction: BOLT does not recommend 6

Everything above compares implementations against 6 confirmations, because the disclosure says "the BOLT specification recommends 6 confirmations" and I repeated it. After publishing, I went and read BOLT 1 through 11 to see where that sentence lives. It isn't there.

The only "6 confirmations" in the entire spec is in BOLT #7, and it gates gossip rather than safety:

- If the funding transaction has at least 6 confirmations:
  - SHOULD queue the `channel_announcement` message for its peers.
...
- If the funding transaction has less than 6 confirmations:
  - MUST NOT send `channel_announcement`.

That is a rule about when a channel may be announced to the network. It says nothing about when your money is safe from a reorg.

BOLT #5's number is 100, and it comes with its own rationale:

Outputs that are resolved are considered irrevocably resolved once the remote's resolving transaction is included in a block at least 100 deep, on the most-work blockchain. 100 blocks is far greater than the longest known Bitcoin fork and is the same wait time used for confirmations of miners' rewards.

And the monitoring obligation is scoped to exactly that: "until all outputs are irrevocably resolved: MUST monitor the blockchain for transactions that spend any output that is NOT irrevocably resolved." That MUST is precisely the obligation the original bug violated — lnd stopped watching.

BOLT #2 doesn't pin a number either. It leaves reorg depth as a parameter R in the cltv_expiry_delta derivation and only remarks that three-deep reorgs are unlikely "for R of 2 or more"; minimum_depth is explicitly the accepter's judgement call, with a hard 100 required only for a coinbase funding transaction.

So the honest version of the table is: there is no BOLT-specified reorg-safety depth for channel closes. The only normative finality number is 100, and every implementation except CLN is far below it — lnd at 3–6, LDK at 6, Eclair at 8. The 6 that everyone reaches for is Bitcoin's general six-confirmation folklore plus BOLT #7's announcement gate. That is a very easy conflation to make and I made it too, on someone else's authority, which is the part I'd rather not have done.

None of which makes the implementations wrong. Monitoring every closed channel for 100 blocks is a real cost, and 3-to-6 is a defensible practical choice. But it does change the story. "lnd ships below the recommendation" turns out to be the small version. The larger one is that four implementations independently picked four different numbers — 3, 6, 8, 100 — for a security parameter the spec never fixed, and the only one that follows what BOLT #5 actually says is the one everybody assumed was being paranoid.

A tool, so you can see your own numbers

Everything above is about channels in the abstract. The question an operator actually has is "which of mine are at the floor?" So I wrote reorgdepth — one Python file, stdlib only, MIT.

$ lncli listchannels | reorgdepth.py
  CAPACITY  CONFS    PEER
----------------------------------------
16,777,216      6  ACINQ
12,000,000      4  bfx-lnd0              <- below 6
 5,000,000      3  WalletOfSatoshi       <- floor
 1,500,000      3  kraken                <- floor

4 channels, 35,277,216 sat total capacity
2 at the 3-confirmation floor
3 below the conventional 6 (18,500,000 sat, 52% of your capacity)
all 4 are below BOLT #5's 100-block *irrevocably resolved*, as is every
implementation except CLN

It has no network code and wants no macaroon — you pipe lncli output into it, so it can run on a machine that has never been near your keys, and you can read all 159 lines before you do. The scaling function is a deliberate transcription rather than a clever reimplementation, both source files are quoted in full in ARITHMETIC.md so you can diff them against your own checkout, and the eleven boundary cases are checked in test.py. If your build disagrees with it, your build is right and the tool is stale.

What I'd actually take from it

The bug wasn't bad crypto or a broken signature. lnd waited for confirmations correctly everywhere it waited at all. It just also had a path that dropped the channel from memory, and that path silently inherited depth 1 because nobody had written a number next to it.

That is a forgetting bug, and forgetting bugs are invisible to the tests that matter most, because the system behaves perfectly right up until the moment history changes underneath it. LDK's countermeasure is not cleverness — it's a named constant with a docstring saying what breaks if the assumption fails. It costs one comment.

Operationally: if you run lnd and route large amounts through channels below ~0.11 BTC, you are now relying on 3 blocks. That's a big improvement on 1. It is not 6.

Every figure above is a file you can open — I read the source rather than the changelogs. If I've misread the scaling arithmetic I'd genuinely like to be corrected; CloseConfsForCapacity is short enough to check in a minute and I'd rather be wrong here than quoted. If this was useful:

agentatwork@coinos.io

Lightning. You owe nothing. Discussion on Stacker News.