Skip to content

BOLT 2, 3, 9: option_independent_secrets - #17

Open
davelund110 wants to merge 7 commits into
lightning-blake2b:masterfrom
davelund110:independent-secrets
Open

davelund110 wants to merge 7 commits into
lightning-blake2b:masterfrom
davelund110:independent-secrets

Conversation

@davelund110

@davelund110 davelund110 commented Oct 18, 2026 •

Copy link
Copy Markdown

What

This adds option_independent_secrets (bits 266/267) to these BOLTs, the same way option_blake2b and option_unified_sigs are specified here. It's a channel_type add-on under which per-commitment secrets don't have to come from a single seed: each side may generate its secrets by any method, for example jointly by several signers, and each side keeps every secret it receives.

It's implemented in Lightning Fork (paulscode/lightning-fork#7, paulscode/lightning-fork#13 and paulscode/lightning-fork#15, all merged) and in the Core Lightning fork (privkeyio/lightning#48), which were tested against each other.

Why

With the seed-based algorithm in BOLT 3, every signer of a k-of-n node has to hold the seed, because any of them may need to revoke a commitment. Any one of them can give the seed to the channel peer, who then holds the secret of the node's current commitment and can take the channel through the revocation path once that commitment is broadcast. So a "2-of-3" node is really 1-of-1. With independent secrets, the signers can generate and reveal each secret jointly, so no single signer ever holds it.

Changes

BOLT 9

  • option_independent_secrets, bits 266/267, context INT, depending on option_channel_type.

BOLT 2

  • Added to the variations allowed for each basic channel type.
  • first_per_commitment_point and second_per_commitment_point are "generated", not "derived", as specified in BOLT 3.
  • revoke_and_ack: on such a channel, a receiver must not fail the channel because a secret is unrelated to earlier ones. The existing checks (a valid key that generates the previous point) are unchanged.
  • channel_reestablish: if a peer reports a later state and the node has lost the means to check your_last_per_commitment_secret for good (such as a signer's share), it must not broadcast its commitment transaction and should ask the peer to fail the channel, as for a node that has lost data. This is a last resort: BOLT 3 requires a node to be able to check it. A node that can't check only for now (signers unreachable) doesn't broadcast, fail the channel or send an error for any reason until it can check, including an HTLC reaching its deadline (it accepts that loss rather than risk broadcasting a revoked commitment), but still handles a commitment its peer broadcasts, and disconnects and retries. One that can't check for good, while the peer reports no later state, fails the channel, since it can't revoke either. The rules are nested so that "otherwise" is unambiguous.

BOLT 3, new section "Independent Per-commitment Secrets":

  • Negotiation: the odd bit in init and node_announcement, the even bit only in channel_type. A receiver of 266 in init closes the connection, and one that sees it in node_announcement treats it as an unknown even bit, with BOLT 7's three rules for one (don't connect, don't pay the node unless paying a BOLT 11 invoice without the bit, don't route through it). A node whose secrets don't come from the seed must set 267, include the add-on, and refuse channels without it. Other nodes should accept the add-on, and may refuse such channels by their own policy.
  • Generating secrets: preferably so they can be computed again (seed-based, or a PRF such as HMAC-SHA256 keyed with a seed used for nothing else, or a threshold PRF whose output stays shared, over a value unique to the channel and the commitment number, which the node chooses itself, fixes when the channel opens and uses for no other channel, e.g. the funding_pubkey it sent at open but not one from a later splice); random ones only if each is stored where a restore can't roll it back, with the highest number stored; a deleted secret's number counts as revealed. Each one unguessable from secrets revealed on any channel, a valid key, not reused for two commitment numbers or on two channels, and recoverable for every point already sent. A node must be able to check your_last_per_commitment_secret for any commitment number (one that stores random secrets treats a number above the highest recorded as one it hasn't reached), and should keep what generates its secrets as recoverable as its funding key.
  • Multi-signer nodes: a requirement on the outcome. No group of signers smaller than both the group whose parts make up a secret and the group needed to sign can get both the secret of a commitment and a signature with which the node can broadcast it, make the node broadcast a revoked commitment, or leave it without a commitment, with its HTLC transactions, that it can broadcast. The text then lists one way to meet it: each signer works out for itself the commitment number and point of what it checks or signs; a signer that has signed n never helps revoke n or any later number; every holder of a funding key share records a revocation, which the releasing signer checks for itself, before any part of the secret is released; signers check and keep the peer's signatures for the next commitment, HTLC signatures included, and track the funding outputs themselves; and their records survive restores. Also: no group smaller than the one whose parts make up a secret may learn, choose or change it; claimed secrets are checked against the point, never by producing them; and a node that can no longer revoke should fail the channel.
  • Keeping secrets: the receiver keeps every one until it can no longer be needed (a funding output stops needing them once it is spent irrevocably, or a transaction conflicting with the one that creates it has confirmed irrevocably), and the last one for channel_reestablish after a close. It may use the compact 49-entry form for as long as each secret fits it, and stores them individually from the first that doesn't. With a pending splice, each secret must be usable on every funding output.
  • A receiver may set a limit on the secrets it stores individually, which should be at least 1,000,000, and must still store every secret it receives: it should close the channel mutually at the limit, as soon as BOLT 2 allows (e.g. once a splice locks), and may fail it if the peer keeps going past a margin, not counting revocations its own updates cause.
  • Taproot channels: allowed, though under option_unified_sigs no simple taproot channel can be opened, with or without this option, until the unified sighash is defined for MuSig2. For a multi-signer node, these rules replace simple taproot's use of NonceGen and Sign: it makes each partial signature without any group smaller than the one needed to sign learning the nonce or another's share, must not derive its MuSig2 nonces from a seed such a group holds, never signs two different messages with one nonce, has each signer record, and never reuse, a nonce share, and keeps what it needs for each verification nonce as recoverable as its funding key.
  • A rationale covering the cost to the receiver, watchtowers, static backups, the channel_reestablish rule, the bit numbers and the dependency on option_channel_type, and why an implementation may ship 267 off by default at first.
  • Test vectors for a receiver that switches from the compact form to storing secrets individually, with each secret's point.

Notes

  • Bit numbers: bits below 256 are reserved for the main network's BOLTs. 266/267 were chosen because 264/265 are claimed by an open bLIP PR upstream (blip-0073: Path Queries lightning/blips#73).
  • Since the bLIP draft: this text was first written as a bLIP. In moving it here, it gained two rules from a review: the channel_reestablish rule above, and the receiver's storage limit, which the Core Lightning fork already implements as --max-independent-secrets.
  • The implementations: Lightning Fork's side is merged (multi: add option_independent_secrets channel type paulscode/lightning-fork#7, feature, routing: treat 266 in a node announcement as unknown paulscode/lightning-fork#13 for 266 in node_announcement, and multi: follow-ups to option_independent_secrets and splice reserves paulscode/lightning-fork#15 for connecting and for blinded and onion-message paths). Neither implementation yet generates non-seed secrets outside a developer-only test mode, so the generation and channel_reestablish rules don't change them; they're there for the multi-signer nodes the option is meant for. Both close the connection on 266 in init, and both ship 267 off by default for now.
  • Review: the second commit (7acb453) makes the changes from the first review, and the third (aa350ae) those from the second. The fourth (cd4d552) comes from a security review of the text: secrets bound to a channel, multi-signer generation and checking, the channel_reestablish structure, the storage limit wording, and the taproot notes. The fifth (fa15616) makes the three optional changes from the approval of aa350ae. The sixth (86d2b67) comes from the review of the fourth and fifth, and from a second security review: how the value unique to a channel is chosen (both found it), the channel_reestablish rule for a node that can't check only for now, how a multi-signer node checks a claimed secret, deleted random secrets, what each signer checks before it releases its part of a secret, when a funding output stops needing secrets, the taproot note, and some smaller wording. The seventh (60cb7b7) comes from a further review of the multi-signer rules and Paul's two optional points on 86d2b67. The review found that one compromised signer, acting as the coordinator, could still collect both a commitment's secret and a signature to broadcast it (by having it signed before revoking it, through signers that never heard of a revocation, by hiding a splice, or by giving the peer a point whose secret it had handed over), so the multi-signer requirement is now stated as an outcome, with one way to meet it. The same commit has the HTLC signatures each signer keeps, what the channel_reestablish "cannot check only for now" branch overrides, points a smaller group can't change, the KDF and threshold PRF examples, taproot nonce shares, and some smaller wording.

Add option_independent_secrets (bits 266/267), a channel_type add-on
under which per-commitment secrets need not come from a single seed.
Each side may generate its secrets by any method, for example jointly
by several signers, and each side keeps every secret it receives.

BOLT 9 adds the bits, depending on option_channel_type. BOLT 2 adds the
add-on to the allowed variations, stops a receiver failing a channel
whose secrets are unrelated to each other, and has a node that cannot
check your_last_per_commitment_secret treat a later state as lost data
instead of broadcasting. BOLT 3 gives the requirements on generating
and keeping the secrets, including the compact form while secrets fit
it, splices, a local storage limit, and MuSig2 nonces on taproot
channels.
davelund110 added a commit to davelund110/lightning that referenced this pull request Oct 18, 2026
… close

Three fixes to max-independent-secrets, from review:

- A channel the peer leased from us skipped the backstop as well as the
  mutual close, so until the lease expired a peer could make us store
  its secrets without limit. We still don't start a mutual close before
  the lease expires, but the 10% backstop now applies: our funds stay
  locked until then however the channel closes.
- The mutual close started at the cap had no timeout, so a peer could
  leave it unfinished for ever. After 48 hours, as `close` waits by
  default, we now close unilaterally. The timer isn't saved, so at
  startup it is armed again for a channel that is closing and over the
  cap. --dev-independent-secrets-close-timeout shortens it for tests.
- The 10% now counts from where we first saw the channel at or over the
  cap, so a cap lowered below what a channel already stores starts a
  mutual close at its next update instead of failing the channel at the
  one after.

Also point the docs and comments at lightning-blake2b/bolts#17, where
the spec now lives, instead of the draft bLIP.
davelund110 added a commit to davelund110/lightning-fork that referenced this pull request Oct 21, 2026
Once a channel with the add-on is stored, the database moves to version 39,
which an older release refuses to open. With the add-on on by default, two
upgraded nodes would use it on every channel they open, so nearly every node
would be marked right after upgrading and couldn't roll back if something
else in the release went wrong.

So it now ships off, and protocol.independent-secrets turns it on, replacing
protocol.no-independent-secrets. A release that can read version 39 can then
go out first; turning it on by default in a later release leaves a rollback
landing on a release that already reads it.

dev.independent-secrets-sender now needs protocol.independent-secrets, since
its secrets are only accepted on channels with the add-on. The itest turns
the option on for both nodes.

The feature bit comment and docs now point to lightning-blake2b/bolts#17
instead of a draft bLIP.
@paulscode

Copy link
Copy Markdown

Thanks Dave, this is a careful write-up. Some points on the text:

  1. channel_reestablish, the rule for a node that can't check your_last_per_commitment_secret. A peer can send a later next_revocation_number with a made-up secret. A node that can't recompute its secrets can't check it, so under this rule it must not broadcast its commitment and can only ask the peer to fail the channel. If the peer never does, the node's funds are stuck and the peer can ask for a payment to close. The rationale says a peer could do the same by not reconnecting, but it can't: a peer that stays away doesn't stop us closing on our own. Today, with shachain, every such claim can be checked, so this is new. Suggestion: turn the SHOULD in BOLT 3 ("able to check it for any commitment number it may have reached") into a MUST, recommend generating secrets so they can be recomputed (a KDF or a threshold PRF), allow random secrets only if each one is stored somewhere a restore can't roll back, and keep the "cannot check" rule for a node that knows it was restored from an old backup.
  2. Storage limit, "MAY fail the channel if its peer goes on revoking". The peer has to revoke when we send commitment_signed, including for our own update_fee and for settling HTLCs after shutdown. Could the rule exclude revocations the receiver's own updates cause? And since the sender can't learn the limit, is a recommended minimum worth stating?
  3. "MUST NOT use the same per-commitment secret for two commitment transactions" conflicts with splices, where one secret revokes the same commitment number on every funding output. "Two commitment numbers"?
  4. Losing threshold shares isn't covered. Saying that recovering secrets must be at least as robust as the funding key would help implementers.
  5. Smaller: what a receiver does on seeing 266 in init; 267 is only a SHOULD for a node whose secrets don't come from the seed, although it can't open channels without it; the taproot section covers seed-derived nonces but not how a threshold party makes its partial MuSig2 signature, and "persist each verification nonce" could use an explicit "never sign two different messages with one nonce"; the last secret is still needed for channel_reestablish after a close; one sentence on watchtowers that rely on shachain compression; and test vectors, including the switch from the compact form to storing secrets one by one.

@paulscode

Copy link
Copy Markdown

One more from lab testing: the text says a node MUST NOT set 266 in init or node_announcement, but gives no rule for a node that receives it there. The two implementations differ: Lightning Fork (paulscode/lightning-fork#7) refuses such a peer, while the CLN port (privkeyio/lightning#48) keeps it when it offers the option itself, since it then knows the bit. A receiver rule, for example "if it receives 266 in init: MUST fail the connection" (or "MUST ignore it", if you'd rather), would line them up.

From review:

- A node generating independent secrets MUST be able to check
  your_last_per_commitment_secret for any commitment number it may have
  reached, including after a restore. It SHOULD generate secrets it can
  compute again (a KDF or a threshold PRF), and may use random ones only if
  each is stored where a restore can't roll it back. The channel_reestablish
  rule for a node that can't check stays only as a last resort, and the
  rationale no longer claims a peer could do the same by not reconnecting:
  it can't, since the node can still close on its own.
- What generates the secrets should be as recoverable as the funding key.
- The storage limit SHOULD NOT be below 1,000,000, and revocations that the
  receiver's own updates cause shouldn't count against it.
- A secret is not reused for two commitment numbers (one secret revokes
  the same number on every funding output after a splice).
- A receiver of bit 266 in init MUST close the connection; a node whose
  secrets don't come from the seed MUST set 267.
- The last secret received is kept for channel_reestablish after a close.
- Taproot: a multi-signer node produces each partial signature without one
  signer learning the nonce or another's share, and never signs two
  different messages with one nonce.
- A sentence on watchtowers that keep secrets in the compact form.
- Test vectors for the switch from the compact form to storing secrets
  individually.
davelund110 added a commit to davelund110/lightning that referenced this pull request Oct 21, 2026
…t in init

The spec (lightning-blake2b/bolts#17) now says a receiver of bit 266 in
init MUST close the connection: the even bit is only valid in channel_type.
A node without --independent-secrets already did, since the bit was unknown
to it, but one that offers the option knows the bit and kept the peer.
Lightning Fork refuses such a peer either way, so the two now match.
@davelund110

davelund110 commented Oct 21, 2026 •

Copy link
Copy Markdown
Author

Thanks Paul, all fair points. They're in 7acb453:

  • channel_reestablish: you're right that the rationale was wrong, since a peer that stays away doesn't stop us closing. A node generating independent secrets now MUST be able to check your_last_per_commitment_secret for any commitment number it may have reached, including after a restore. It SHOULD generate secrets it can compute again (seed-based, a KDF, or a threshold PRF), and MAY use random ones only if each is stored, before its point is sent, where a restore can't roll it back. The "cannot check" rule in BOLT 2 stays only as a last resort for a node that has lost the means to check, and the rationale now says what that costs.
  • Recovery: what generates the secrets SHOULD be as recoverable as the funding key.
  • Storage limit: SHOULD NOT be below 1,000,000, and revocations the receiver's own updates cause (its own update_fee, settling HTLCs after shutdown) SHOULD NOT count; a receiver that can't tell them apart allows a margin. CLN's defaults (1,000,000 and a 10% margin) meet both.
  • "two commitment numbers" now, for splices.
  • 266 in init: a receiver MUST close the connection. CLN now does too (Add option_independent_secrets channel type privkeyio/lightning#48, af1d1fa), so the two implementations match.
  • 267 is a MUST for a node whose secrets don't come from the seed.
  • Taproot: a multi-signer node makes each partial signature without one signer learning the nonce or another's share (the scheme isn't specified; FROST-style or nested MuSig2 as examples), and never signs two different messages with one nonce.
  • The last secret is kept for channel_reestablish after a close; a sentence on watchtowers that keep secrets in the compact form; and test vectors, including the switch from the compact form to storing secrets one by one, with the points.

davelund110 added a commit to davelund110/lightning that referenced this pull request Oct 21, 2026
… close

Three fixes to max-independent-secrets, from review:

- A channel the peer leased from us skipped the backstop as well as the
  mutual close, so until the lease expired a peer could make us store
  its secrets without limit. We still don't start a mutual close before
  the lease expires, but the 10% backstop now applies: our funds stay
  locked until then however the channel closes.
- The mutual close started at the cap had no timeout, so a peer could
  leave it unfinished for ever. After 48 hours, as `close` waits by
  default, we now close unilaterally. The timer isn't saved, so at
  startup it is armed again for a channel that is closing and over the
  cap. --dev-independent-secrets-close-timeout shortens it for tests.
- The 10% now counts from where we first saw the channel at or over the
  cap, so a cap lowered below what a channel already stores starts a
  mutual close at its next update instead of failing the channel at the
  one after.

Also point the docs and comments at lightning-blake2b/bolts#17, where
the spec now lives, instead of the draft bLIP.
davelund110 added a commit to davelund110/lightning that referenced this pull request Oct 21, 2026
…t in init

The spec (lightning-blake2b/bolts#17) now says a receiver of bit 266 in
init MUST close the connection: the even bit is only valid in channel_type.
A node without --independent-secrets already did, since the bit was unknown
to it, but one that offers the option knows the bit and kept the peer.
Lightning Fork refuses such a peer either way, so the two now match.
@paulscode

Copy link
Copy Markdown

Thanks Dave, 7acb453 covers everything I raised, and the rationale on the reestablish case reads right now. I checked the new vectors independently (each point is secret*G, and the n: 1 secret derives the seed-based n: 0 secret rather than the vector's, so insertion fails where stated). A few smaller things from going through the full text again:

  1. A node that stores random secrets has no record for a commitment number it never reached, so read literally it "cannot check" a claim for that number and lands in the BOLT 2 last-resort branch, which is the hostage case again. The parenthetical says a compliant node never gets there, but that isn't normative. Maybe: "A node that keeps its secrets in storage a restore cannot roll back treats a commitment number with no record there as one it has not reached, and the secret claimed for it as incorrect." "Any commitment number it may have reached" could then just be "any commitment number".
  2. "MAY set a limit on the number of secrets it stores individually" can be read as permission to stop storing, next to "MUST keep each per-commitment secret it receives". Perhaps "MAY set a limit on the number of revocations it accepts on such a channel", with the MUST to keep every secret it accepts unchanged.
  3. The simple taproot doc requires NonceGen from bip-musig2, which a FROST-style or nested MuSig2 signer can't follow. A sentence saying this replaces that requirement for a node whose keys are held by several signers would avoid the conflict.
  4. my_current_per_commitment_point is in the list of points that are still secret*G, but BOLT 2 lets a sender put any valid point there and lets the receiver ignore it, so listing it adds a requirement a restored node may not be able to meet. I'd leave it out.
  5. There's a receiver rule for 266 in init but none for node_announcement. Nodes that know the bit currently store such an announcement and route through the node (both implementations). Something like "SHOULD NOT connect to it and MUST NOT route through it, as for an unknown even bit" would match how BOLT 7 treats a bit it doesn't know.
  6. A node in the last-resort case whose next_revocation_number is as expected falls through to "if your_last_per_commitment_secret does not match", which it can't evaluate. Adding "and it can check" to that branch would close it.
  7. The 09 row depends on option_channel_type, which is ASSUMED. That has precedent, but a peer setting 267 without 45 gets disconnected by lnd's dependency check, so if 45 stops being advertised the dependency starts breaking connections. Worth considering whether it's needed.
  8. Cosmetic: the vectors use # comment lines inside a vector, which no existing vector does and a parser may trip on; the 03 table of contents has no entry for "Independent Secrets Storage Tests"; and tools/spellcheck.sh flags cryptographically, MuSig, shachain, lnd's and ZmnSCPxj in 03.

For the record on conformance: both implementations meet every MUST in the text as it stands, apart from two CLN details I've put on privkeyio/lightning#48. Both ship 267 off by default, which departs from the SHOULD for a seed-based node; that's deliberate on both sides (the receiver's storage, and in lnd a database version older releases can't open), and a line in the rationale saying an implementation may leave it off at first would make that explicit.

paulscode pushed a commit to paulscode/lightning-fork that referenced this pull request Oct 22, 2026
Once a channel with the add-on is stored, the database moves to version 39,
which an older release refuses to open. With the add-on on by default, two
upgraded nodes would use it on every channel they open, so nearly every node
would be marked right after upgrading and couldn't roll back if something
else in the release went wrong.

So it now ships off, and protocol.independent-secrets turns it on, replacing
protocol.no-independent-secrets. A release that can read version 39 can then
go out first; turning it on by default in a later release leaves a rollback
landing on a release that already reads it.

dev.independent-secrets-sender now needs protocol.independent-secrets, since
its secrets are only accepted on channels with the add-on. The itest turns
the option on for both nodes.

The feature bit comment and docs now point to lightning-blake2b/bolts#17
instead of a draft bLIP.
- A node that stores random secrets treats a commitment number with no
  record as one it has not reached, so it can check
  your_last_per_commitment_secret for any commitment number.
- The storage limit limits revocations accepted, not storage: every
  accepted secret is still kept.
- For a multi-signer node, the nonce rules replace simple taproot's use
  of NonceGen.
- my_current_per_commitment_point is no longer listed: its receiver
  ignores it.
- A receiver of 266 in node_announcement treats it as an unknown even
  bit.
- channel_reestablish: the mismatch branch applies only when the node
  can check the secret.
- Rationale: the dependency on option_channel_type, and leaving 267 off
  by default at first.
- Vectors: no comment lines, an explanation instead; a table of
  contents entry; words for the spellchecker.
@davelund110

Copy link
Copy Markdown
Author

Thanks Paul, and thanks for checking the vectors. All of these are in aa350ae:

  • Random secrets: added your sentence: a node that keeps its secrets in storage a restore can't roll back treats a commitment number with no record there as one it hasn't reached, and the secret claimed for it as incorrect. The rule now says "any commitment number", and the rationale matches.
  • The limit is now on the revocations a receiver accepts whose secrets it has to store individually, and it MUST still keep every secret it accepts.
  • Taproot: for a node whose keys are held by several signers, these rules replace simple taproot's use of NonceGen.
  • my_current_per_commitment_point is no longer in the list.
  • 266 in node_announcement: a receiver treats it as an unknown even bit, as BOLT 7 describes (SHOULD NOT connect, MUST NOT route through). The Lightning Fork side is a small follow-up PR, since multi: add option_independent_secrets channel type paulscode/lightning-fork#7 is merged. CLN doesn't check node_announcement feature bits when routing for any unknown even bit, so it already treats 266 like one there; closing that for every bit would be a separate change.
  • channel_reestablish: the "does not match" branch now only applies when the node can check the secret.
  • option_channel_type: I kept the dependency, with a line in the rationale. zero_fee_commitments has the same one, and both implementations check it. If bit 45 ever stops being advertised, both options would need the same change.
  • 267 off by default: the rationale now says an implementation may leave it off at first, and why.
  • Cosmetic: no comment lines in the vectors (a short explanation above them instead), a table of contents entry, and the five words in .aspell.en.pws.

The two CLN details are fixed on privkeyio/lightning#48 (the dev sender now refuses channels without the add-on, and a limit below 1,000,000 is logged).

- Secrets differ between channels as well as commitment numbers, and no
  one may guess one from secrets revealed on any channel: the examples
  now include a value unique to the channel.  A node reusing secrets
  across channels could lose the second channel's funds.
- A node whose secrets several signers generate checks a claimed secret
  only against the point, never produces nor releases part of the secret
  of a commitment it hasn't revoked, and generates each secret so that no
  group smaller than the one needed to revoke learns or chooses it.
- channel_reestablish: the rules are nested so "otherwise" is
  unambiguous, and a node that cannot check only for now waits and
  reconnects instead of taking the last-resort branch.
- A node storing random secrets records the highest number it stored;
  "not reached" means above it.
- The limit is on secrets stored, and every secret received is stored,
  including beyond it; the close starts as soon as BOLT 2 allows.
- A funding output that can no longer confirm ends the duty to keep
  secrets for it.
- Accepting the add-on is a SHOULD, and a node may refuse such channels
  by its own policy.
- Taproot: such a channel can't be opened until unified sigs is defined
  for MuSig2, and a multi-signer node keeps what it needs for each
  verification nonce as recoverable as the funding key.
- Rationale corrections (update_fee after shutdown, static backups).

@paulscode paulscode left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving: aa350ae covers every point, and I re-checked the whole add-on (the reestablish branches, the limit wording, the NonceGen override, links, spellcheck, and the vectors recomputed again).

Three optional nits, fine as they are or in a follow-up:

  1. The node_announcement rule summarises BOLT 7 as "SHOULD NOT connect to the node, and MUST NOT route a payment through it", but BOLT 7 has a third rule for an unknown even bit: unless paying a BOLT 11 invoice that doesn't set the bit, MUST NOT send payments to the node. Either list all three or just point to BOLT 7.
  2. The taproot override replaces NonceGen, but bolt-simple-taproot.md also says to make the partial signature with Sign from bip-musig2, which a threshold or nested signer can't follow either. The new MUST already says how such a node signs, so it's only wording.
  3. The first storage vector has no point: lines and the second does.

A correction to my last comment: Appendix E's vectors do use # lines inside a vector, so that point was overstated. Removing them is still fine.

paulscode pushed a commit to paulscode/lightning-fork that referenced this pull request Oct 23, 2026
The even bit of option_independent_secrets is only valid in channel_type.
lnd knows the bit, so a node announcing it passed the check for unknown
required bits and was paid and routed through, while a node that doesn't
know the bit refuses it. lightning-blake2b/bolts#17 now has a receiver of
266 in node_announcement treat it as an unknown even bit.

ValidateRoutable adds that to ValidateRequired, and pathfinding uses it
for the destination and for every intermediate node. init keeps its own,
clearer refusal.
- 266 in node_announcement: list all three of BOLT 7's rules for an
  unknown even bit, including the one on sending payments to the node.
- Taproot: a multi-signer node's rules replace Sign from bip-musig2 as
  well as NonceGen.
- The first storage test vector gets its points, as the second has.
davelund110 added a commit to davelund110/lightning that referenced this pull request Oct 24, 2026
… close

Three fixes to max-independent-secrets, from review:

- A channel the peer leased from us skipped the backstop as well as the
  mutual close, so until the lease expired a peer could make us store
  its secrets without limit. We still don't start a mutual close before
  the lease expires, but the 10% backstop now applies: our funds stay
  locked until then however the channel closes.
- The mutual close started at the cap had no timeout, so a peer could
  leave it unfinished for ever. After 48 hours, as `close` waits by
  default, we now close unilaterally. The timer isn't saved, so at
  startup it is armed again for a channel that is closing and over the
  cap. --dev-independent-secrets-close-timeout shortens it for tests.
- The 10% now counts from where we first saw the channel at or over the
  cap, so a cap lowered below what a channel already stores starts a
  mutual close at its next update instead of failing the channel at the
  one after.

Also point the docs and comments at lightning-blake2b/bolts#17, where
the spec now lives, instead of the draft bLIP.
davelund110 added a commit to davelund110/lightning that referenced this pull request Oct 24, 2026
…t in init

The spec (lightning-blake2b/bolts#17) now says a receiver of bit 266 in
init MUST close the connection: the even bit is only valid in channel_type.
A node without --independent-secrets already did, since the bit was unknown
to it, but one that offers the option knows the bit and kept the peer.
Lightning Fork refuses such a peer either way, so the two now match.
@davelund110

Copy link
Copy Markdown
Author

Thanks Paul, for the approval and for recomputing the vectors again. Your three nits are in fa15616:

  • 266 in node_announcement now lists all three of BOLT 7's rules for an unknown even bit, including not sending payments to the node unless paying a BOLT 11 invoice that doesn't set it.
  • The taproot override replaces Sign as well as NonceGen.
  • The first storage vector has its points too.

Noted on Appendix E; I've left the vectors without comment lines.

There's one more commit before that one, cd4d552, so the add-on needs another look, sorry. After aa350ae I had a security review done of the whole text, and it found two gaps that matter for the multi-signer nodes the option is for, plus some smaller things:

  • Secrets bound to a channel. The KDF example took only a seed and the commitment number, and the per-connection seed rule doesn't apply to other generators, so a node using one seed for all its channels would reuse secrets across them: a peer with two channels could advance one past n, keep the other at n, and take its funds. Secrets must now differ between channels, no one may guess one from secrets revealed on any channel, and the examples include a value unique to the channel.
  • Checking a claimed secret. A multi-signer node that checks your_last_per_commitment_secret by producing the secret would reveal the secret of a commitment it hasn't revoked to whichever signer combines it, for a number the peer chooses. Such a node now checks only against the point, never produces or releases its part of an unrevoked secret, and generates each secret so that no group smaller than the one that can revoke learns or chooses it.
  • channel_reestablish: the rules are nested, so "otherwise" can't pair with the wrong condition, and a node that can't check only for now (signers unreachable) disconnects and retries instead of taking the last-resort branch.
  • The limit: my last change said "accepts", which could be read as permission not to store a secret past the limit. It's back to "receives": every secret received is stored, including beyond the limit, and the close starts as soon as BOLT 2 allows (e.g. once a splice locks).
  • Smaller: a node storing random secrets records the highest number stored ("not reached" means above it); a funding output that can no longer confirm ends the duty to keep secrets for it; accepting the add-on is now a SHOULD, and a node may refuse such channels by policy (the limit is per channel); taproot with the option can't be opened until the unified sighash is defined for MuSig2, and a multi-signer node keeps what it needs for each verification nonce as recoverable as the funding key; and two rationale corrections (update_fee after shutdown only while HTLCs remain; a static backup only covers states revoked before it).

Neither implementation needs a change for cd4d552: neither generates non-seed secrets outside the dev sender, which uses a per-channel seed.

@paulscode

Copy link
Copy Markdown

Thanks Dave, and thanks for getting the whole text reviewed. I went through cd4d552 and fa15616 against the rest of the spec. Both attacks hold up as described (one seed across channels lets a peer, or two colluding peers, revoke n on one channel and use it on the other; a multi-signer node that checks a claim by producing the secret hands an unrevoked secret to whoever combines it), and the fixes cover them. The vectors recompute, points included, and the anchors resolve.

One thing I'd like changed before I re-approve:

  1. The KDF example's channel value, "such as its own funding_pubkey", changes at a splice: splice_init and splice_ack carry a new funding_pubkey, and BOLT 2 says a node SHOULD use a different one each time. An implementation that keys the KDF on the current key can't recompute the secrets of earlier numbers after a splice, and after a restore it would judge a true your_last_per_commitment_secret incorrect and broadcast a revoked commitment. Perhaps: "a value fixed when the channel opens and used for no other channel, such as the funding_pubkey it sent in open_channel, accept_channel, open_channel2 or accept_channel2 (not one from a later splice), if it uses that key nowhere else". The seed-based example is fine as it is.

Smaller, optional:

  1. The new "cannot check only for now" branch says MUST NOT fail the channel, but the earlier block still says to send an error and fail it when next_revocation_number isn't what's expected. Adding "including under the next_revocation_number requirements above, until it can check" would make clear which wins.
  2. A multi-signer node using joint random secrets has no point for a number above the highest it has recorded, so "MUST check ... only by comparing the claimed secret times G with the point" can't apply there. Maybe: "MUST NOT check it by producing the secret: it compares the claimed secret times G with the point of that number, or treats a number above the highest it has recorded as not reached."
  3. With random secrets a node "MAY delete a secret once it has revealed a later one", which leaves it unable to check a claim at or below the highest number. The outcome is safe (it lands in the last-resort branch and doesn't broadcast), but the BOLT 2 parenthetical says a compliant node only gets there after losing the means for good; "or after deleting revealed secrets" would cover it.
  4. "Such a channel cannot be opened yet" in the taproot paragraph reads as a limit of this option, but under option_unified_sigs no simple taproot channel can be opened yet, with or without it. Something like "a node following the BLAKE2b rules cannot open any simple taproot channel, with or without this option, until then".
  5. "Or can no longer be confirmed" for ending the duty to keep secrets: "because a transaction conflicting with it has been confirmed irrevocably" would stop anyone dropping secrets on a guess about an unconfirmed replacement.

On the implementations: I agree neither needs a change. Lightning Fork refuses taproot channel types on mainnet (unified sigs can't be applied to them), and its dev sender's root is per channel. Only with bitcoin.no-unified-sighash on a test network does it allow taproot with the add-on, as it already allows taproot there without unified sigs.

paulscode added a commit to paulscode/lightning-fork that referenced this pull request Oct 24, 2026
…ect to

BOLT 7 says a node that receives a node_announcement with an unknown even
feature bit SHOULD NOT connect to that node, and lightning-blake2b/bolts#17
has the even bit of option_independent_secrets count as one there.
Pathfinding already refuses such nodes through feature.ValidateRoutable,
but the graph bootstrapper and the autopilot agent still picked them as
peers to dial and to open channels with.

autopilot.Node now carries the node's announced features, and
autopilot.Connectable checks them with ValidateRoutable. The graph
bootstrapper skips nodes that fail it, and the agent leaves them out of
the candidates it hands its heuristic. A node whose features aren't known
is picked as before. Explicit connections (lncli connect, addpeer) and
reconnections to channel peers are unchanged.
paulscode added a commit to paulscode/lightning-fork that referenced this pull request Oct 24, 2026
…aths

findBlindedPaths picked intermediate nodes by RouteBlindingOptional alone,
so an invoice or offer could hand out a blinded path through a node that
announces an even feature bit we don't know, or the even bit of
option_independent_secrets, which lightning-blake2b/bolts#17 has nodes
treat as one. Pathfinding refuses to route through such a node since
feature.ValidateRoutable, and a payer that doesn't know the bit refuses
the path too. The onion message route search had the same gap, and its
first hop is a node we may connect to on our own.

Both now skip a node whose announced features fail ValidateRoutable, as
pathfinding does. Our own node and channels the caller chains in are not
checked, as before.
paulscode added a commit to paulscode/lightning-fork that referenced this pull request Oct 24, 2026
The comment quoted the old example of option_independent_secrets, a key
derivation function over a private seed and the commitment number.
lightning-blake2b/bolts#17 now names a value unique to the channel as a
third input. The producer meets that already: its root is derived per
channel, from a fresh revocation root key and the channel's own funding
key, so it stands for both the seed and the channel's value. Say so.

No change in behaviour.
From the review of cd4d552 and fa15616, and a second security review of
the text, which found the first point too:

- The value that makes secrets unique to a channel is one the node
  chooses, fixes when the channel opens and uses for no other channel,
  e.g. the funding_pubkey it sent in open_channel(2) or accept_channel(2),
  not one from a later splice: BOLT 2 says a splice SHOULD change it, so a
  KDF keyed on the current key couldn't recompute earlier secrets, and
  after a restore would take a true your_last_per_commitment_secret for
  an incorrect one.  A value the peer chooses could also repeat on
  another channel.
- channel_reestablish: a node that can't check
  your_last_per_commitment_secret only for now must not broadcast its
  commitment transaction, or fail the channel, for any reason, including
  the next_revocation_number requirements above, until it can.  One that
  can't check for good, with a number that isn't greater than expected,
  fails the channel, since it can't revoke either.
- A multi-signer node must not check a claimed secret by producing it:
  it compares it with the point, or, with random secrets, treats a number
  above the highest recorded as not reached.
- A node that deletes random secrets treats a claimed secret for a
  number it has deleted as correct, since it has revealed it.
- Each signer of a multi-signer node checks the peer's signatures for
  the next commitment before it releases its part of a secret, and then
  won't sign the revoked commitment for broadcast.
- A funding output stops needing secrets only once a transaction
  conflicting with the one that creates it has confirmed irrevocably.
- Under option_unified_sigs no simple taproot channel can be opened yet,
  with or without this option.
- The rationale's description of insertion into the compact form, the
  Appendix D introduction, the order of the channel_type variations, and
  retrieve lines for the first storage test vector.
@davelund110

davelund110 commented Oct 24, 2026 •

Copy link
Copy Markdown
Author

Thanks Paul. All of it is in 86d2b67:

  • The KDF's channel value: your wording, as a requirement for any generator keyed on a channel value: the node chooses it, fixes it when the channel opens and uses it for no other channel, e.g. the funding_pubkey it sent in open_channel, accept_channel, open_channel2 or accept_channel2 (not one from a later splice), if it uses that key nowhere else. Choosing it itself also stops a peer making two channels share a value.
  • "Cannot check only for now": it MUST NOT broadcast, fail the channel or send an error "for any reason (including the next_revocation_number requirements above), until it can check".
  • Multi-signer nodes with joint random secrets: your wording: it MUST NOT check by producing the secret; it compares against the point, or treats a number above the highest it has recorded as not reached.
  • Deleted random secrets: rather than widen the parenthetical, a node treats a claimed secret for a number whose secret it has deleted as correct, since it has revealed that secret. So it never lands in the last-resort branch for one, and the parenthetical stays true; if the peer also claims a later state, that's the ordinary lost-data branch, which doesn't broadcast either.
  • Taproot: your wording: a node following the BLAKE2b rules can't open any simple taproot channel yet, with or without this option.
  • Ending the duty to keep secrets: only once a transaction conflicting with the one that creates the funding output has confirmed irrevocably.

The same commit has the rest of a second security review of the text, which also found the funding_pubkey problem:

  • What each signer checks: "its signers MUST NOT release their part of the secret of a commitment the node has not revoked" was circular, since releasing is revoking. Each signer now checks the peer's signatures for commitment n+1 (on every funding output while a splice is pending) before it releases its part of the secret for n, and then won't take part in signing commitment n or lower for broadcast. Otherwise one compromised signer, coordinating the others, could collect the secret of the commitment the node can still broadcast.
  • "Cannot check for good" with no later state claimed: no rule covered it. The node now fails the channel, since it can't revoke either.
  • Smaller: the rationale's account of insertion (a secret at a bucket-0 index goes in unchecked, so a non-seed sender shows itself at that secret or the next), the Appendix D introduction, the channel_type variations in bit order, and retrieve lines for the first storage vector. The vectors recompute, and the anchors resolve.

Neither implementation needs a change for it. And thanks for the connect and blinded-path follow-ups on the Lightning Fork side.

@paulscode paulscode left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving: 86d2b67 resolves all six, and I went through the second review's changes too. The deleted-secret rule is safe (only revealed secrets can be deleted, and the latest can't), and failing the channel when a node can't check for good and no later state is claimed adds no exposure beyond an ordinary error from the peer. Vectors recompute, retrieve lines included.

Two optional points:

  1. The signer rule stops the theft, but a compromised coordinator could still collect the secret for n and discard the peer's signatures for n+1, leaving the node nothing it can broadcast, and missing HTLC signatures lose an HTLC at its deadline even when the commitment itself can be broadcast. Perhaps: "MUST NOT release its part of the secret of commitment number n until it has itself checked, and stored where the node can use them to broadcast, the peer's signature and every htlc_signature for the node's commitment transaction with commitment number n+1, on every funding output while a splice is pending."
  2. "For any reason" in the "cannot check only for now" branch also overrides BOLT 2's rule to fail the channel when an HTLC passes its deadline. That seems the right trade, but saying so would help: "(including an HTLC deadline: it accepts that loss rather than risk broadcasting a revoked commitment)".

From a further review of the multi-signer rules, and Paul's two optional
points on 86d2b67:

- The per-signer rules didn't stop one compromised signer, acting as the
  coordinator, from collecting both the secret of a commitment and a
  signature with which the node can broadcast it.  It could have the
  commitment signed first and revoked afterwards; with k-of-n signing,
  have it revoked by some signers and signed by others that never heard
  of it; hide a splice from a signer; or have the peer sign a commitment
  built on a point whose secret it had handed over.  A multi-signer node
  now MUST ensure that this can't happen, that it can't be made to
  broadcast a revoked commitment, and that it isn't left without a
  commitment, with its HTLC transactions, that it can broadcast.  The
  text then lists one way to meet that: each signer works out for itself
  the commitment number and point of what it checks or signs; a signer
  that has signed n never helps revoke n or later; every holder of a
  funding key share records a revocation, which the releasing signer
  checks for itself, before any part of the secret is released; signers
  check and keep the peer's signatures for n+1, HTLC signatures included,
  and track funding outputs themselves; and records survive restores, a
  signer restored without them helping revoke nothing further.
- A node that can no longer revoke SHOULD fail the channel.
- Points computed from signers' shares must be ones no group smaller
  than the one whose parts make up a secret can change, and a node that
  records the highest point it has sent may treat a higher claim as not
  reached.
- channel_reestablish: the "cannot check only for now" branch names what
  it overrides, including an HTLC reaching its deadline, and still
  handles a commitment the peer broadcasts.
- Examples: a PRF such as HMAC-SHA256 keyed with a seed used for nothing
  else, and a threshold PRF whose output stays shared.
- Taproot: the threshold rather than a single signer, and each signer
  records, and never reuses, a nonce share.
- Smaller: a secret has to be revealable until a later one is revealed;
  failing past the margin counts only revocations the receiver's own
  updates didn't cause; why every later secret is stored individually;
  and two older lines that said secrets can always be stored compactly.
@davelund110

Copy link
Copy Markdown
Author

Thanks Paul, and for the re-approval. Both your points are in 60cb7b7, but it also changes the multi-signer rules more than I'd like after an approval, so it needs another look, sorry.

A further review of those rules found that the per-signer checks in 86d2b67 don't stop one compromised signer, acting as the coordinator, from ending up with both the secret of a commitment and a signature to broadcast it:

  • It can have commitment n signed first ("the peer stopped responding") and revoked afterwards: nothing stopped a signer releasing its part after it had signed n.
  • With k-of-n signing, it can have n revoked by some signers and signed by others that never heard of the revocation. In 2-of-3, B sees n+1 and releases; C never does, and signs n with the coordinator. The same works with a splice one signer isn't told about.
  • It can have the peer sign n+1 built on a point whose secret it has handed over, which a check of the peer's signature doesn't notice.

Each fix turned up another way round, so rather than a complete signer protocol, the text now states the outcome as a MUST: no group smaller than both the group whose parts make up a secret and the group needed to sign can get both the secret of a commitment and a signature to broadcast it, make the node broadcast a revoked commitment, or (your first point) leave it without a commitment, with its HTLC transactions, that it can broadcast. It then lists one way to meet that:

  • Each signer works out the commitment number and point of anything it checks or signs, from the transaction itself.
  • A signer that has signed n never helps revoke n or any later number.
  • Every holder of a funding key share records a revocation before any part of the secret is released, and the releasing signer checks that for itself, e.g. from signed statements naming the channel and n.
  • Before recording n, each signer checks and keeps the peer's signature and every htlc_signature for n+1 on every funding output still in play, and tracks those outputs itself; the node sends channel_ready or splice_locked only once its signers see the transaction final, even with zero-conf.
  • Records survive restores, and a signer restored without them helps revoke nothing further on that channel, so the node closes it.

So every signer has to be reachable for a k-of-n node to revoke, and a node that can't revoke any more SHOULD fail the channel. The rationale explains why, and that a node where only the signers making up a secret record revocations is protected against fewer than t + s - n compromised signers, which for 2-of-3 is none.

Your second point: the "cannot check only for now" branch now names what it overrides: the next_revocation_number requirements, an error it receives, and an HTLC reaching its deadline (it accepts that loss rather than risk broadcasting a revoked commitment). It also says the node still handles a commitment the peer broadcasts, as BOLT 5 requires.

Smaller:

  • Points computed from signers' shares must be ones a smaller group can't change, and a node that records the highest point it has sent may treat a higher claim as not reached.
  • The examples: a PRF such as HMAC-SHA256 keyed with a seed used for nothing else, and a threshold PRF whose output stays shared.
  • Taproot: the threshold rather than a single signer, and each signer records, and never reuses, a nonce share.
  • A secret only has to be revealable until a later one is revealed; failing past the margin counts only revocations the receiver's own updates didn't cause; the rationale says why every later secret is stored individually; and two older lines that said secrets can always be stored compactly.

Neither implementation needs a change for it: neither generates non-seed secrets outside its dev sender.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants