Conversation
Rebased onto the v26.06.7-blake2b branch head; the restamp also removes the gossip store, without which the re-announced channel never reached a fresh node in the lab; the upgrade path is tested and its transcript recorded with the interop transcript from the same build.
kwsantiago
left a comment
There was a problem hiding this comment.
Verified the mainnet constant: it decodes to 0000000000000050c1e5f69672f459293be14f46e5a494e7a8c8541396f18eeb, which is block 961,640 on a live node. Two findings below.
| chainparams_block0(chainparams))) { | ||
| /* A one-way door, like a database upgrade in a | ||
| * non-final version: the operator says so once. */ | ||
| if (!w->ld->db_upgrade_ok || !*w->ld->db_upgrade_ok) { |
There was a problem hiding this comment.
StartOS passes --database-upgrade=true unconditionally (cln-startos startos/main.ts:50), so this guard never fires there. Pointing that build at a Bitcoin wallet restamps it silently and one way, which is the case the flag is meant to prevent. Worth its own flag, or a check that the wallet has post-activation history.
There was a problem hiding this comment.
You are right, and I checked: cln-startos startos/main.ts:50 is
const lightningdArgs: string[] = ['--database-upgrade=true']
with no condition on it, so the guard never fires there and the case it exists
for is exactly the case that gets through.
Worth being precise about the damage, because it changes what the fix has to
protect. The restamp does not touch keys or UTXOs. It rewrites the chain
identity stamp and clears announcement signatures for existing channels. A
Bitcoin wallet that goes through it has not lost coins, but it has stopped
being usable on the other chain without restoring from seed, and its channels
are announced under the wrong identity. One way, and bad enough.
I took your second suggestion, and then found it does not stand on its own, so
the fix is in two parts and the second part is a flag after all. My first
instinct was that a flag has the same failure mode as --database-upgrade,
being only as good as the most careless thing that sets it. What changed my
mind is that the failure mode is not "a flag" but "a flag that something else
already sets for its own reasons". A flag whose only purpose is this decision
has nothing setting it by habit.
It is a separate commit rather than an amend, so the change stands on its own
rather than being folded back into the one you reviewed: wallet: gate the restamp on the wallet's own history, not on a flag, and then wallet: a dedicated flag for the case the wallet cannot answer. The rebase means the
"changes since your review" view will not show them usefully, hence naming
them. On this chain's mainnet the chain_hash was taken from the
activation block, so a wallet that followed the fork has that block at that
height and a wallet from the SHA256d chain has a different one. Nothing has to
be asked of a backend, which matters because this runs at sanity-check time.
If the wallet has our block there, restamp regardless of the flag. If it has a
different one, refuse regardless of the flag. Only where the wallet has no
block at that height does the flag still decide. A new
chain_hash_block_height records where the value came from and is zero on the
networks that derive it by tagged hash, so the check stands aside there
instead of refusing every regtest wallet.
Here is why it needed a second part. I tried to make the refusal fire in the
lab by giving regtest a chain_hash_block_height, and it did not, because the
migration scenario creates fresh wallets which have no record of that height at
all. The same is true on mainnet of any wallet created after activation: it
never saw block 961,640, so the check stands aside. A wallet created after the
fork on the SHA256d chain and one created after the fork here are
indistinguishable from their block tables alone.
That is the population the wallet's own history cannot speak for, and for it
the decision has to come from the operator. So it comes from
--restamp-wallet-for-this-chain and not from --database-upgrade. The case
you found is that cln-startos passes --database-upgrade=true
unconditionally, so on that distribution the guard is permanently open; nothing
passes the new flag by habit, so on that distribution the guard is now
permanently closed unless somebody types it. It is a one-way door and the log
message says so.
What that leaves is narrower and I think acceptable: an operator who types the
flag at a wallet that genuinely followed the other chain, and whose wallet has
no block at the activation height to contradict them. Closing that needs the
chain rather than the wallet, comparing recorded blocks against what the
backend reports at the same heights. I looked at where that could go and it is
not this commit: wallet_sanity_check runs at lightningd.c:1332 and
setup_topology, which is the first thing that talks to bitcoind, is
fourteen lines later. So there is no backend to ask yet, and the honest options
are to move the restamp later in startup or to leave this to the operator. I
left it to the operator. If you would rather it moved, say so and I will do it
as a separate change.
| * the other one, and LND never named any. See | ||
| * doc/blake2b-chain-identity.md. */ | ||
| status_peer_debug(&peer->id, | ||
| "Peer names no networks and this chain " |
There was a problem hiding this comment.
cln-application sends no networks TLV (LN_MESSAGE_CONFIG sets no chainHashes), so this drops every StartOS and Umbrel dashboard. lnmessage supports the TLV, but setting it pushes chain awareness into a UI that otherwise needs none. Same breakage a required feature bit caused, worth solving once for both.
There was a problem hiding this comment.
Confirmed, and this one is a mistake rather than a tradeoff I made knowingly. I
was thinking about lnd nodes on the SHA256d chain, which have never sent the
field, and not about client applications that speak the wire protocol only to
reach a node's RPC and have no reason to know what chain they are on. Dropping
every dashboard is not a defensible default.
I did not want to fix it by making cln-application send the TLV. You are
right that it pushes chain awareness into a UI that otherwise needs none, and
it only fixes the clients we happen to know about.
The better argument is against my own patch. The init drop was never the
isolation, only defence in depth. A peer from the other chain that connects
anyway still cannot open a channel, because open_channel carries
chain_hash, and still cannot get its gossip accepted, because
channel_announcement carries it too. So it is now opt-in, renamed to
--drop-peers-without-networks because Core Lightning names an option for the
action it takes and "allow" defaulting to allowing would be a flag that does
nothing. A peer that names other chains but not ours is still dropped, as
before; that case is unambiguous.
Testing it produced a better demonstration than I expected. The interop
scenario used to assert that a stock Bitcoin lnd was dropped at init, and
that assertion could never have tested this: lnd drops Core Lightning itself,
because Core Lightning names a chain lnd does not serve, so "lnd is not a peer"
was true whatever Core Lightning did. With the drop off, what actually happens
is that lnd connects, sends a gossip_timestamp_filter carrying Bitcoin's
chain_hash, and gets "bad chain" back. The isolation holds one layer further
in, exactly where it should. The scenario asserts that now instead.
Your last sentence is the one I keep coming back to. This is the same shape as
the compulsory feature bit: something at init that refuses peers, where the
refusal catches clients it was never aimed at. I have written up the feature bit
side on the forum. If the principle is "do not refuse at init for things
chain_hash already covers", both of these fall out of it.
dc1d611 to
f2c35e8
Compare
|
Thank you for decoding the constant against a live node rather than taking it For the record of how it got there: it is checkpointed in Bitcoin Knots since Two structural things, so the rest of the thread makes sense. The PR was aimed Both of those mean your inline comments now sit on commits that no longer |
|
The PR said the suite was not run and that files still assert The fixture had Every test that loads a canned database from an older release failed, because And a successful restamp was logged at
It also found that an invoice for another chain produced four different errors That is fixed too. I have also made the PR description more precise on one point. It said |
f2c35e8 to
00c7b33
Compare
|
|
||
| *known = false; | ||
|
|
||
| if (chainparams->chain_hash_block_height == 0) |
There was a problem hiding this comment.
I ran this against a real mainnet wallet rather than a lab one, and the no-flag path cannot fire there.
The wallet is a StartOS node that has been on this chain since before activation. Its blocks table holds 20,991 rows spanning 504,605 to 972,349, and there is a hole from 961,628 to 971,901. The nearest row below the activation block is twelve blocks before it, and 961,640 is absent, so wallet_followed_this_chain() returns known=false and the decision falls through to the flag.
That hole is the fork itself. The pre-fork build stalled where it could not parse the 164-byte header, and the fork-aware build resumed near the tip rather than backfilling. So the activation block is precisely the block that a node which lived through the activation is least likely to hold, which is the population the no-flag restamp was written for.
The refuse path is unaffected and still correct: a continuously synced wallet on the SHA256d chain does have 961,640 with a different hash, and is refused. It is the accept path that almost never fires.
The consequence is the one your earlier reply was aimed at. On cln-startos the guard is not open, it is closed, and closed for every node rather than only for ambiguous ones: --restamp-wallet-for-this-chain is not passed there, so upgrading to a build carrying this series makes lightningd refuse to start until the package adds it. That is fixable on my side and I will add it, but it should be a deliberate decision rather than a surprise, and the release notes need to say so.
Worth considering whether the question can be asked of the chain instead of the wallet. You noted wallet_sanity_check runs fourteen lines before setup_topology, so there is no backend yet. Comparing any block the wallet does have against what the backend reports at that height would answer it for every wallet rather than only for ones holding a specific block, and the wallet here has 20,991 of them to choose from.
There was a problem hiding this comment.
You are right, and the evidence is better than the wallet you happened to have.
I went looking for whether the hole is particular to that node or structural,
and it is structural.
setup_topology() sets blockscan_start = wallet_blocks_maxheight() and
reduces it by --rescan, so this build resumes from the highest block it holds
and walks forward. Nothing backfills. And
wallet_blocks_contig_minheight() exists precisely to find "the first block in
the last run of blocks", so a non-contiguous blocks table is an ordinary state
this codebase already expects. I was checking for one specific row in a table
that is designed to have holes, and I picked the row that a node which lived
through the activation is least likely to hold, for the reason you give: the
pre-fork build stopped at the header it could not parse.
So the accept path was near-dead for exactly the population it was written for,
and I had built a guard that is closed rather than open. That is worse than the
bug it replaced, because it is silent about it until startup fails.
I took your suggestion and asked the chain instead.
wallet_sanity_check() keeps the two cheap answers, since they need no backend:
the wallet holds the activation block and it matches, so restamp; it holds it
and it differs, so refuse. Where the wallet cannot say, it now defers rather
than falling to the flag. settle_deferred_restamp() runs between
setup_topology() and begin_topology(), which is the window where bitcoind is
up and no blocks have been scanned yet, and it takes the highest block the
wallet holds above the fork, which is a height where the two chains differ,
and asks what this chain has there. Equal restamps, different refuses, and only
a wallet holding no block above the fork at all still needs the operator.
For your wallet that is a block around 972,319 rather than 961,640: the height
is whatever survives the rollback setup_topology() does to blockscan_start,
which is your highest block less --rescan. Either way it is far above the
fork, it is chain-specific, and it answers without a flag. It answers for any
wallet holding any block above the fork, which is what you were pointing at.
The rollback is also what makes the question safe to ask there.
setup_topology() waits for the backend to reach blockscan_start before
rolling the wallet back to it, so by the time this runs, the highest block the
wallet holds is one the backend is known to have. It is not asking the backend
about a height it may not have reached.
Two things I owe you about it.
The refuse path is stronger than before, not weaker: it no longer depends on the
wallet holding one specific block, so a SHA256d wallet with a hole across the
activation is now caught where previously it fell through to the flag.
And I would rather tell you what it took to get right than present it as though
it arrived working. Writing it was quick; making it correct needed four
findings, every one from running it rather than reading it:
struct walletis allocated withtal(), which does not zero, and I added
restamp_pendingwithout initialising it. A fresh node read the garbage as
true, went down the deferred path and refused to start.- The wallet read ran outside a db transaction, which
db_prepare_v2refuses. - The query context was
tmpctx, which the io_loop frees, taking the request
and its callback with it. The node hung at startup, waiting for a reply that
could not arrive.setup_topology()keeps a local context and says why; I
had read that comment and not applied it. - The
blkidthe backend hands the callback is in the opposite byte order to
the one the wallet stores, and every other caller in that file ignores it and
derives the id from the block. Comparing against it reversed the answer, so
the first working version refused a wallet that had followed this chain.
That last one is the one I would have shipped. It fails in the safe direction,
it looks right in review, and the two hashes in the log are byte-reverses of
each other, which is easy to read past.
So I built a test rather than reasoning about it. Giving the lab's regtest an
activation height makes the deferred path reachable off mainnet, and a wallet
aged to match yours (stamped with block 0, no activation block, blocks above
the fork) now exercises both answers:
wallet blocks agree with the chain restamped with no flag, node starts
wallet blocks differ from the chain refused, naming both hashes and the height
On the packaging consequence: with this, cln-startos does not need to add the
flag for a node that lived through the activation, which is the population that
would have hit it. What still needs it is a wallet with no block above the fork
at all, which is a wallet created after activation and never synced. I would
still call that out in the release notes rather than let anyone meet it cold.
| i == 0 ? GOSSIP_STORE_FILENAME | ||
| : GOSSIP_STORE_FILENAME ".corrupt"); | ||
| if (unlink(store) == 0) | ||
| log_broken(w->log, "Removed %s: its " |
There was a problem hiding this comment.
This one is still log_broken, on the success path, which puts back what commit 12 removed.
fixtures.py:735 collects any log line containing **BROKEN** and fixtures.py:590 maps that to a test error, so a restamp that removes a gossip store fails the run even though everything worked, and the operator who did exactly as instructed sees **BROKEN** twice if both store files are present. Same reasoning as the message above it, which you moved to log_unusual for this exact reason.
log_unusual reads right here: removing the store is expected and is explained by the line above.
There was a problem hiding this comment.
Correct, fixed. fixtures.py:735 collects **BROKEN** and fixtures.py:590
turns it into a test error, so a restamp that removed a store failed the run
while doing exactly what it was supposed to, and an operator saw **BROKEN**
once per store file.
It is log_unusual now, and it moved: the restamp body is extracted into
wallet_restamp() so the deferred path above can call it too, which is what
stopped there being two copies of this to keep in step.
|
Thank you for the pointer. It is a complete specification of the digest and I have now checked our That may be worth something to you. The document says three implementations What the document does not cover is the part I actually had to guess: it So the narrower question stands: is there anything written down for the channel |
|
Keeping chain_hash as Bitcoin's genesis: unified_sigs in channel_type already isolates. Sorry for the churn on the restamp work. |
|
Adopted, and I think it is the better design. Isolating at Where that has actually got to, since "adopted" can mean two things and you I mention it for two reasons beyond honesty about my own state. It is the On the restamp work, you were right and my first instinct was wrong. I had The check is reached through one gate in So I will withdraw that series rather than re-motivate it. What I do not think The rework, then: drop the synthetic One more reason to withdraw rather than defer, which I found by tripping over The one thing the spec does not cover yetThis is the part I would most like settled, and I have stopped being able to I put Lightning Fork on one side and your build on the other, unmodified, at Each side refuses the other's invoice on the prefix, before a route is I had this passing earlier, which is worth admitting because of how. The Core
I have a second reason to care, which is worth stating because it is evidence The next part may sound like a claim about which chain is Bitcoin, but it is not The refusals above are between our two builds, which both know this chain It refuses, which is the outcome that matters, and it has no way to say what it On the specific string, I have no strong attachment. Mine is And the same question, worse, in BOLT 12Adopting the reversal turned this up, and it is the harder half. An offer that omits I ran it rather than leaving it as an argument, because it is the kind of claim That was on regtest, where both implementations write Nothing here needed a bug to go wrong, which is what separates it from the I hit the other half of it as a plain bug while adopting the change. My decoder How bad it is, precisely. Less bad than it first looks, and not harmless. The case that does cost money is a node with channels on both chains, which is What I would propose. The same shape as the BOLT 11 fix: use the field that I am aware of how that sounds, so to be explicit: I am not reopening The reason I would do it this way rather than leave offers ambiguous is that it I have not implemented it. Minting offers that name a chain your build does not BOLT 3You have written it yourself while I was measuring, which is better than my I do have the independent implementation your note asked for: a Go one, in my And I stopped taking my own word for the second-level value. Until this week Your node computed the first of those and sent it in The run also corrected me, and the same correction applies to your text, which Worth noting for the thread, since it is why I bothered getting it on chain at One more thing I changed on my sideYour BOLT 9 text says Changed to 68, and the heuristic now defaults to off, since sending that TLV is |
3505090 to
efd4c2d
Compare
|
Reworked and force-pushed: fifteen commits down to seven, as described above. Gone: Two things from building and running it that are worth having in the thread. The series closes the interop break I mentioned earlier. Against Lightning Fork And running Not yet run: the full python suite. The prefix reaches further into the tests BOLT 12 is still open and deliberately untouched, for the reason in the body. |
`chain_hash` is the genesis hash both chains share, deliberately, so nothing in
a BOLT 11 invoice says which of the two it is for. The prefix is the only place
that can, and it is the last thing a user sees before paying.
mainnet lnblake testnet4 lntblake
signet lntbsblake regtest lnblakert
An invoice for the chain that did not upgrade is refused here, and an invoice
minted here is refused there. That second half is the point: those nodes
predate this chain and will never be updated, so a shared prefix would mean an
invoice from here is decoded, found well formed, and paid on the other chain.
`legacy_lightning_hrp` keeps the prefix each network had before, because
refusing an invoice is not the same as failing to recognise one. `decode` still
pulls `lnbc` and reports it as an invoice for another chain, rather than
falling through as an unknown string, and the bookkeeper's older events were
written with it.
testnet3 keeps `tb` and is left alone: it shares that prefix with testnet4 in
this table, and only testnet4 is a chain this fork runs on.
Running the python suite turned up four different errors for one condition, an invoice for another chain, depending on which way it arrived. Two of them were unhelpful and one was actively useless. Unknown chain bc is true and tells a user who pasted a Bitcoin invoice nothing they did not know. On a chain that took a new prefix the old one is the commonest thing anyone will paste, and it is already recorded as legacy_lightning_hrp, so name it: the prefix belongs to the SHA256d chain, this node follows another, and here is its prefix. Invalid invstring from listinvoices discarded the decoder's reason entirely. pay.c two files over has always included it. Now both do.
…hanged A funding output from below block 961,640 exists for nodes that did not upgrade too, and its spend may happen where this node cannot see it, so a channel announced against one would sit in the graph forever with nothing able to remove it. The chain_hash check just above cannot do this job. Both chains carry the genesis hash they share, deliberately, so it does not tell them apart; it only tells either of them from some third chain. This is the other half of what replaced giving the chain a chain_hash of its own, the first half being option_unified_sigs inside channel_type. A channel funded after the activation on the other chain is already refused, because its funding output does not exist here and the txout lookup fails. Only the ones funded before it need a rule. The height is a chainparams field rather than a constant so that the rule is off on every network that never changed its proof of work, where a floor would refuse every announcement there is. Placed exactly where BOLT 7 puts it, after the chain_hash clause and before the one about ancient channels, and it applies to this node's own announcements as well: a channel of ours funded before the activation is in the same position.
A prefix is the only thing in a BOLT 11 string that says which chain it is for, and the constants live in a table of per-network initialisers where a mistake is easy to make and invisible until an invoice is refused or, worse, accepted. Checks the current and legacy prefix of every network this fork runs on, that no two networks claim the same prefix, that each is reachable by name through chainparams_by_lightning_hrp, and that a legacy prefix never equals the current one, which would make the "this is for another chain" message nonsense. Also pins the activation height the gossip rule compares against: set on the chain that changed its proof of work, zero everywhere else. Zero is what turns the rule off, and a stray non-zero value on a chain that never moved would refuse every channel announcement it ever saw. There is no chain_hash here on purpose. It is the genesis hash both chains share, so there is nothing about it left to get wrong.
…_hash The previous version of this document specified a chain_hash of this chain's own and said that was what kept the two chains apart. That design is withdrawn. chain_hash is the genesis hash both chains share, and the chains are separated in the four places it actually matters: option_blake2b at init, a gossip floor at the activation height, option_unified_sigs inside channel_type, and the BOLT 11 invoice prefix. Rewritten around those four, one section each, with the hash types stated per signature rather than per transaction, since a second-level HTLC carries two and on an anchor channel they differ. Section 6 is the BOLT 12 gap, which nothing covers and which the old design did cover, said plainly rather than left out. Section 8 is the change itself and what to do if the old values were already implemented. Kept identical to Lightning Fork's copy except where an implementation detail differs, which is the point of writing it down at all.
The PR said the pytest suite was not run and that files still assert
lnbcrt. This runs it, which turns a guess into a list: five tests fail,
all for the same reason, and none of them for a reason that can be fixed
by editing a string.
Their fixtures are signed BOLT 11 invoices for other chains, either the
spec's mainnet lnbc vectors or regtest lnbcrt invoices hand-made years
ago for cases a node cannot generate on request, such as an invoice with
no payment secret. Changing a prefix invalidates the signature, and the
keys that signed them are not available, so they cannot be carried over.
They are skipped with the reason attached rather than quietly deleted.
The spec vectors are not lost. common/test/run-bolt11 decodes them
against a chainparams entry that keeps the bc prefix, which this series
already added and already runs.
Running it also found something worth fixing separately, which is why the
exact messages are recorded here. One condition, an invoice for another
chain, produces four different errors depending on the path:
bolt11 decode Invalid bolt11 invoice: Prefix bcrt is not for regtest
(expected blakert)
decode command Unknown chain bc: invalid token '"lnbc1..."'
listinvoices Invalid invstring
checkrune Not permitted: Invalid invoice: Unknown chain bcrt
The first is the message this series intended. The second is the token
fallthrough the PR text claims it avoids, so that claim is wrong as
written. The third tells the user nothing at all. Making these one
message is a follow-up rather than part of this commit, but it should not
go unrecorded: an operator who pastes a Bitcoin invoice should get the
same answer wherever they paste it.
Changing the BOLT 11 prefix breaks every test that spells one out, and there are more of them than there look: the bookkeeper's coin_type is the lightning prefix rather than the address prefix, so its assertions move too. The pytest chainparams fixture gains `lightning_hrp`, which is what the tests should have been reading all along instead of `bip173_prefix`. Addresses are unchanged on this chain and invoices are not, so the two are no longer the same string and a test that conflates them passes for the wrong reason. Nothing here touches chain_hash or the wallet stamp. An earlier version of this series changed both and carried a `restamp-wallet-for-this-chain` option into these tests; that series is withdrawn and the option does not exist.
efd4c2d to
abd909c
Compare
|
Ran the python suite against this, which the previous series never had, and it The bookkeeper could not read its own history. It decodes the BOLT 11 That is accounting data lost on upgrade, for every payment made before it. The It does not make an old invoice payable. The path that cares which chain an And two bookkeeper tests asserted the prefix as a literal, On method, since the numbers look alarming on their own: those files fail 85 Still not run: the rest of the suite, beyond the files the prefix reaches. |
| @@ -281,6 +281,24 @@ static const u8 **fallbacks(const tal_t *ctx, | |||
| return addrs; | |||
There was a problem hiding this comment.
bc1qjwqr7hlpsf0f6mkf5uqqtfa4prj2udnux0k7j9
This replaces the fifteen-commit series that was here. That one gave the
BLAKE2b chain a
chain_hashof its own; it is withdrawn, because a change ofproof of work is not a change of chain, and
chain_hashstays the genesis hashboth chains share. Seven commits now, and the two that carry behaviour are the
invoice prefix and the gossip floor.
Summary
v26.06.7-blake2b.4follows the chain across the activation and signs with theunified signature hash. Two things are still missing before a node on this
chain can be told from a node on the chain that did not upgrade, and one of
them currently stops our two implementations paying each other at all.
The invoice prefix.
chain_hashis shared, so a BOLT 11 invoice carriesnothing that says which chain it is for. The prefix is the only place that can.
This series gives the chain
lnblake,lntblake,lntbsblakeandlnblakert, and keeps the old prefix per network so thatdecodestillrecognises an
lnbcstring and refuses it with a reason rather than failing toclassify it.
The direction that matters most is outward. A node on the other chain predates
this chain and will never be updated, so with a shared prefix an invoice minted
here is decoded there, found well formed, and paid on the other chain. That is
an argument from compatibility with deployed software, not from which chain is
which.
The gossip floor. A funding output from below block 961,640 exists for
nodes that did not upgrade too, and its spend may happen where this node cannot
see it, so a channel announced against one would sit in the graph forever. The
chain_hashcheck cannot do this, because both chains carry the same value.This is the rule in the BOLT 7 half of
lightning-blake2b/bolts#1, placedwhere that text places it.
What this series does not do
chain_hash. That was the previous series' mistake.option_blake2band
option_unified_sigs.chain_hash, so anoffer minted on either chain reads as valid and for the reader's own chain,
on both implementations, with no warning. That gap is real and is
deliberately left open: minting offers that name a chain the other
implementation does not recognise would break fetching between us, and that
is not a thing to do unilaterally.
doc/blake2b-chain-identity.md, whichcommit 5 adds, sets the gap out in full under "Offers: a known gap", and I
have raised it in a comment here as the one thing I would most like a view
on.
reached only when the wallet is stamped with block 0 and the chain's
chain_hashis not, so with the two equal it cannot fire. Measured: a walletsynced on one chain and restarted against the other rescans it to the tip and
says nothing.
Measured
In a regtest lab, against Lightning Fork (
github.com/paulscode/lightning-fork,an LND port) which implements the same values:
Without this series: the two peer, agree
channel_type [12,22,70],exchange gossip, and close both cooperatively and by force with
0x21inboth witnesses. They cannot pay each other. Each refuses the other's BOLT 11
invoice on the prefix, before a route is considered:
With it: the same run pays in both directions, and both closes still
carry
0x21.That is the whole of what this series is for.
Commits
bitcoin: give the BLAKE2b chain its own invoice prefixbolt11: say which chain a foreign prefix belongs to, and keep the reasongossipd: ignore channel announcements from before the proof of work changedtests: pin the invoice prefixes and the activation heightdoc: the chain identity, rewritten for the design that replaced chain_hashtests: skip the five that carry foreign-chain BOLT 11 fixturestests: the invoice prefix, in the fixtures and the assertionsUnit tests
make check-unitsfails on exactly one target,fuzz-open_channel, and itfails the same way on
24d027310with nothing applied, so it is not thisseries. Every other target passes, including the three this series touches.
Running them is what found a bug worth recording. The commit that says which
chain a foreign prefix belongs to read
chainparams->legacy_lightning_hrpwhile decoding, and decoding does not require a configured network: the daemon
always has one, but
fuzz-bolt11does not, so that was a null read and thetarget segfaulted. Upstream never dereferences
chainparamsthere. It isguarded now, and the target passes.
That bug was in the previous series too, unnoticed, because the unit tests were
never run against it.
The python suite
Run against the files the prefix reaches, on this series and on
24d027310with nothing applied, and the two failure sets now match. That suite is failing
a great deal on its own, before any of this: 85 of the tests in those files
fail on plain upstream. What matters is that the series adds nothing to that,
and it does not.
Getting there took two fixes, both found by the run and neither visible without
it. Both were in the previous series too.
The bookkeeper could not read its own history. It decodes the BOLT 11
strings it stored, with no chain check at all, and those were written with the
old prefix.
chainparams_by_lightning_hrponly knew current prefixes, so thedecode failed and the migration aborted:
failed to parse bolt11 lnbcrt1...: Prefix bcrt is the SHA256d chain's. That is accounting data lost on upgradefor every payment made before it. The lookup now falls back to the prefix a
network used to carry, in a second pass so that a prefix still in use always
wins: testnet3 uses
tbtoday and testnet4 used to, and an invoice sayingtbis testnet3's.
This does not make an old invoice payable. A caller that cares which chain an
invoice is for passes
must_be_chain, and that path compares againstlightning_hrpdirectly rather than coming through the lookup. Checked:lnbcrtis still refused on the pay path, and now decodes on the read path.Two bookkeeper tests asserted the prefix as a literal.
'currency': 'bcrt'in eighteen places across
test_migrationandtest_migration_no_bkpr, whichthe earlier series' test commit missed because it only changed the sites that
went through
chainparams. They read the fixture now.One test,
test_wallet.py::test_reserveinputs, fails under-n 4and passesalone on both this series and plain upstream. Flaky, not a regression.
Since then the rest of the suite has been run the same way, every file on both
sides:
test_connection,test_plugin,test_pay,test_closing,test_gossip,test_opening,test_askrene, the splice family, theunified_*files andtest_blake2b_differentiationamong them. The failuresets match in every chunk. Nothing outstanding.