On the feature bit, I think Chris is right, and the part of his plan I had missed is the part that matters most: the asymmetry. I had been arguing for odd everywhere, which is wrong. Peering wants to be permissive, because during a migration you have to be able to talk to nodes that have not moved yet. The places where money moves want to be strict, because the failure there costs money rather than a reconnect. One choice for both cannot be right in both.
Where I ended up differs from his plan in one place. He puts the strict, even bit in invoices and offers. I put it inside channel_type, where a channel type is a set of even bits anyway and where both sides have to agree on the digest they sign, and I set nothing at all in invoices and offers. A payer on the other chain does need refusing, but the invoice prefix and chain_hash already refuse it, and a bit that duplicates a check does not add safety, it adds a second thing to get wrong. If Chris disagrees I would want to hear why, since he may be thinking of a case I am not.
I saw connectd: stop requiring peers to advertise blake2b bit 68 at the tip of blake2b-unified and first read it as settling this. Reading it properly, it settles one half. It stops you requiring the bit from a peer. You still send it compulsorily: in common/features.c, OPT_BLAKE2B is FEATURE_REPRESENT for INIT_FEATURE, so it goes out as even, and BOLT 9 obliges anyone who does not know bit 68 to close the connection. The comment above it says so on purpose, “legacy peers must reject it”, so I take this as a design choice rather than something overlooked.
The half that is left is the half that breaks interop. I built blake2b-unified with my series on top and pointed it at a Lightning Fork node on the same regtest chain:
blake2b-unified @ 24d027310, plus my chain-identity series
init features 90800898880a8a59a1
Lightning Fork -> that node
invalid remote features: feature vector contains unknown required
features: [68]
peered: no, in either direction
That is chain_hash agreement taken out of the picture and the bit still splitting two nodes that are following the same blocks.
Then I changed one word, FEATURE_REPRESENT to FEATURE_REPRESENT_AS_OPTIONAL for INIT_FEATURE, exactly what OPT_UNIFIED_SIGS on the next line already does, rebuilt from the same base commit with the same patches otherwise, and ran it against the same node:
the same, plus one word in common/features.c
init features a0800898880a8a59a1
peered: yes, both directions
One nibble on the wire: 90 becomes a0, which is even bit 68 becoming odd bit 69, and the two nodes find each other. Nothing else differs between those two builds.
I am not asking you to drop the bit or to stop announcing it. Keeping it even in node_announcement costs nothing that I can measure, since lnd does not check required features there at all. It is specifically the init copy that refuses a node on your own chain.
The thing I found while testing that, which matters more
I did not stop at peering, because peering is a low bar: two nodes can finish init and still be unable to do anything. So I tried to open a channel between them, and could not, in either direction:
CLN -> Lightning Fork
lnd: channel type negotiation failed: requested channel type not supported
Lightning Fork -> CLN
CLN: Did not support channel_type [12,22]
[12,22] is option_static_remotekey plus option_anchors_zero_fee_htlc_tx, which is what essentially every channel opened anywhere today is. This is with chain_hash agreed and bit 68 already made optional, so it is a third barrier underneath the other two, and it is the one that survives fixing them.
It is option_unified_sigs. desired_channel_type() in lightningd/channel.c adds bit 70 to the proposed type, and channel_type_accept() in common/channel_type.c refuses an incoming type that lacks it. lightningd/options.c strips both only for elements, so on mainnet, testnet and regtest there is no flag that turns it off. The net effect is that an lnd which does not implement the channel type cannot open a channel with a blake2b-unified node, on any network, in either direction. At the time I ran this, that was every lnd there is.
I want to be careful here, because I do not think this is a bug and I am not reporting it as one. Your proposal document is explicit that a digest test does not establish that a channel transaction is replay-protected, and requiring the unified signer on new channels is a coherent answer to that. We reached the same conclusion about the risk. Lightning Fork carries this, in server_blake2b.go:
Channel %s was funded before the BLAKE2b fork: its funding output exists on both chains and its commitment transactions are valid on both, so a close on one chain can be replayed on the other. Close it and reopen with coins received after block 961640
So we agree on the vector. We differ on where the fix sits. Ours keeps the funding output off the other chain, so a commitment spending it has nothing to replay against, and scans for the channels where that did not happen. Yours binds the signature itself, which is the stronger form, because it does not depend on anyone funding correctly.
Yours is better, so rather than ask you to weaken it, I have implemented it.
Lightning Fork now negotiates option_unified_sigs as a channel type. The rule I followed is the one in your channel_type_sighash(): take the hash type BOLT 3 gives a signature and add SIGHASH_UNIFIED. So the commitment and the cooperative close sign 0x21, and second-level HTLC transactions on an anchor channel sign 0xa3, which is what full_channel.c does. Against your build at 24d027310, with nothing changed on your side:
channel_type [12,22,70] "unified_sigs/even"
payment, fork -> CLN ok
payment, CLN -> fork ok
cooperative close confirmed, witness hash type byte 0x21
force close confirmed, witness hash type byte 0x21
That is the same image that, before this, refused to peer with us at all. I check the byte on chain rather than just that the close confirmed, because a channel that quietly fell back to plain SIGHASH_ALL would still pay and still close, and would not be bound to this chain at all, which is the entire point.
Two things I would still raise, and neither of them is a request to change your security model.
The first is one line, and it no longer affects me, which is why I think it is worth saying. desired_channel_type() adds the bit on
feature_offered(our_features->bits[INIT_FEATURE], OPT_UNIFIED_SIGS)
which is what we offer, while the anchors test three lines above it uses
feature_negotiated(our_features, their_features, OPT_ANCHORS_ZERO_FEE_HTLC_TX)
which is what both sides offer. A peer that never signalled the bit is still sent a channel type containing it, so the open fails where it could have degraded. Every other lnd on this chain, and anything else that implements the channel type later, hits that. Mine no longer does, because it signals the bit.
The second matters more. I matched your encoding by reading your source, and that is a bad way for two implementations to agree on a signature digest. Reading code tells me what you do today, not what is intended, and the places where I had to make a judgement call are exactly the places where we could diverge without either of us noticing until a channel cannot be closed. Two of mine:
- I refuse the bit on taproot channels. There the commitment signature is a MuSig2 partial signature over a BIP341 digest, so opting in is a wire change (a 65-byte signature in
commitment_signed) rather than a different hash type, and I would rather refuse than have two sides sign different digests. I do not know what you intend there.
- Three of my verification paths built their sighash midstate from a fetcher that did not name the spent output. BIP143 never reads it, so this was invisible until the unified digest, which commits to the spent output’s value and
scriptPubKey, started needing it. If your implementation has the same shape anywhere, it will show up as a signature that verifies locally and not on the peer.
Which brings me to a question I think has a good answer already. The .4 release notes say an independent implementation written from doc/unified-sighash.md alone reproduces all 166 Knots digest vectors. That is exactly the document I want, and I cannot find it: it is not in blake2b-unified, and git log --all -- "**/unified-sighash.md" finds nothing in the repository’s history. doc/BLAKE2B-UNIFIED-PROPOSAL.md is thirteen lines and mentions neither channel_type nor a hash type.
Is it on connorslab’s unified-sigs branch, which the notes credit as the basis? If so I would rather implement from it than from your source, and I would like to check my hash types against it rather than against full_channel.c. If it is not published anywhere, I am happy to write up what I have as a first draft and have you correct it. I would rather we disagree on a document now than find the disagreement in a channel later.
For what it is worth, the two independent checks agree so far. Your notes say channels funded past activation are signed with hash type 0x21 and that a full lifecycle against an independently built peer showed 0x21 from both peers. That is what I get, on the commitment and on the cooperative close, and 0xa3 on second-level HTLC transactions on an anchor channel.
One thing I should say plainly about the bit 68 half, since I spent a good deal of this message on it: it is no longer blocking me either. lnd refuses an unknown even bit, and the reason my node refused yours was that lnwire did not name bit 68 at all. It names it now, and sets the odd form (69) rather than the even one, on the reasoning above. So I am not asking you to change it. I am saying that any other lnd on this chain will refuse you until it does the same, and that the cost of the even bit is paid by them rather than by you.
The reason a compulsory bit in init is the wrong tool is the same reason your review of my PR was right about cln-application: an even bit there refuses more than it means to, including clients that speak the wire protocol only to reach a node’s RPC and have no business knowing what chain they are on. That was the same bug arriving from two directions, and the general form is “do not refuse at init for things chain_hash already covers”.
And on the numbers, for whenever the bit is actually set: the highest pair BOLT 9 has assigned is 66/67, option_onion_messages_only_channels, so 68 and 70 are the next ones the spec hands out rather than spare ones. That is an argument for Chris’s range whatever the even/odd split ends up being.
I am not clean here either. My chain identity document reserved 2100/2101, which is further from the frontier than 68 but no better, and a later revision moved that to Chris’s 32769/32768.
I have now given up on my own numbering rather than ask you to give up yours. The document describes 68/69 and 70/71, because those are what is on the wire and a second implementation that picked different numbers would simply not interoperate. What it keeps is the argument: 68 and 70 are the next pairs BOLT 9 hands out, and a custom-range pair sits clear of that.
Your own release notes say the same thing, which I had not registered when I was arguing about it: the numbers are unregistered, they are expected to move, and channels opened under them may have to be closed and reopened. So I do not think we actually disagree here, and the only live question is when to move and whether it is worth doing before more channels exist. The cost rises with every one.
What I did take from Chris is the asymmetry, which matters more than the range and which I had missed: odd in init and node_announcement, even inside channel_type, and neither in invoices or offers, where the prefix and chain_hash already refuse a payer from the other chain.
While testing the connectd change I got a useful confirmation of the argument. With silent peers kept, a stock Bitcoin lnd connects to Core Lightning and still gets nowhere: it sends a gossip_timestamp_filter carrying Bitcoin’s chain_hash and Core Lightning answers “bad chain” and closes. open_channel carries chain_hash too. So the isolation is in chain_hash, and the init rule was only ever making the failure happen earlier.
The larger thing I want to put in front of you, because it is bigger than the bit and I do not think it is a scheduling question.
Chris’s plan keeps chain_hash as the shared genesis hash through the migration and switches to a distinct value together afterwards, with the first BLAKE2b block as the candidate. My PR uses exactly that block. So we agree on the destination.
The part that I think has been overtaken is the timing, and not because I prefer my answer. Lightning Fork has been running the distinct chain_hash on mainnet since release and has channels on it, which have been growing. So right now a Lightning Fork node and a node on one of your builds cannot peer at all: mine advertises the BLAKE2b hash, yours advertises SHA256 chain’s, neither list contains the other’s, and both sides drop at init. One chain, two Lightning networks, no route between them.
I built .4 in the lab and put it on the same regtest chain as a Lightning Fork node to check rather than assume. They do not peer, in either direction:
.4 -> Lightning Fork
connectd: No common chain with this peer
'...1aab09d928da45793d6f6f937b969e3b86f0403862d0de56289a16437bd59425'
Lightning Fork -> .4
invalid remote features: feature vector contains unknown required
features: [68]
.4 invoice : lnbcrt10...
Fork invoice : lnblakert10u...
The hex at the end of the first one is the BLAKE2b chain_hash from my init networks list, which yours has no match for. The second is bit 68, which is where this reply started.
Both of those sit on top of the channel type, which is why I went after that one rather than asking you to change either: with chain_hash agreed and bit 68 out of the way, a node that cannot negotiate the channel type still cannot open a channel. That is the whole of it in one place, and it is the reason I do not think this is a 2027 question. It is true today and it gets more expensive the longer both sides grow separately. The scenarios are scripts/scenario-cln4-split.sh and scripts/scenario-bit68.sh in the lab repository if you want to run them yourself.
For reference, the current network using Lightning Fork is insignificant:
Nodes in graph : 6
Channels : 10
Total capacity : 104,000,000 sats (1.04 BTC)
Last policy update: all 10 within a day
Three of those six nodes are mine. One other peer is in 60% of the channels. At current prices the whole thing is a couple of hundred dollars of capacity.
Ten channels can be easily closed and reopened. If the right answer turns out to be the shared hash, we will do that.
What the number does change is the urgency, in the opposite direction to the one I expected. Neither side has much at stake yet. That is exactly why this is the moment to settle it, while converging is cheap for everybody, rather than after either of us has grown into the split. The cost of getting it wrong is small today and compounds from here.
So the question is which way to converge, and I no longer think sunk cost should decide it. On the merits: converging on the distinct hash costs a one-time restamp, which my PR implements and which I have tested end to end, including a third node relearning the channel afterwards under the new identity. Nothing gets closed. Converging on the shared hash costs us ten channels and costs the chain a Lightning network that shares an identity with Bitcoin for another year. I think the first is better, but that is an argument about the design rather than about who has more to lose.
On the reason for keeping the shared hash: cooperative closes of pre-fork Bitcoin SHA256 channels are a real thing to protect, but I do not think they need the whole BLAKE2b Lightning network to stay on Bitcoin SHA256’s identity to get it. Both of our releases already tell people to fund from post-activation coins, and a user who still has a pre-fork channel with a SHA256 counterparty needs a SHA256 node to close it cooperatively whatever chain_hash we use here.
I considered proposing that nodes advertise both hashes in init during a transition, and discarded it. My check is a membership test over the list so I would accept such a peer with no change, but it only fixes peering: open_channel and gossip still have to agree on one hash, so the two nodes would be connected and still unable to do anything. There is no halfway house that I can find.
One practical note while I was looking: the PR was aimed at v26.06.7-blake2b, and .4 is a different commit on blake2b-unified, so if the next release comes off that branch the PR was pointed at the wrong place. I have rebased onto blake2b-unified and retargeted it. It applies with no conflicts, builds, and run-bolt11 and run-chainparams-blake2b pass there.
It also adds no test failures. tests/test_db.py and tests/test_invoices.py give 21 failures on blake2b-unified without my patches and the same 21 with them, identical by name. I built the branch unpatched specifically to be able to say that, since otherwise I would just be asserting it. Those 21 are yours rather than mine and your .4 notes suggest you already know about them; I mention it only so the number does not look like something I introduced.
BTW, nice work on the PSBT crash and the channel_type refusal. The fundchannel one would have bitten anyone running an LSP flow.