Skip to content

Give Bitcoin addresses an index, and the page a true sentence - #42

Merged
bitcoinuniverseadmin merged 13 commits into
developfrom
agent/fix-bitcoin-address-backend-20260830-203500
Aug 31, 2026
Merged

Give Bitcoin addresses an index, and the page a true sentence#42
bitcoinuniverseadmin merged 13 commits into
developfrom
agent/fix-bitcoin-address-backend-20260830-203500

Conversation

@bitcoinuniverseadmin

Copy link
Copy Markdown

What was on the public site

An address page on explorer.bitcoinuniverse.io:

Error loading address data.
(405 OK: Address lookups cannot be used with bitcoind as backend.)
There are too many transactions on this address, more than the backend can handle. See more on setting up a stronger backend.

Three claims in four lines, none of them true.

  • MEMPOOL.BACKEND was none, so Bitcoin Core answered everything and Core cannot answer an address lookup. The header still invited a reader to search an address, the search box still recognised one, and every address landed here.
  • The status was 405, which says the method is not allowed on a path that answers GET everywhere else. The word beside it came from the proxy chain, which is free to write anything in a reason phrase, and the page pasted the two together and presented the result as a diagnosis.
  • The sentence underneath is the only one that is ever about the address, and it was shown for a condition that had nothing to do with the address at all.

Every release gate was green throughout. That is the part worth fixing: this was not a bug that slipped past the checks, it was a state no check was looking at.

The index

A first-party mempool/electrs v3.3.0 (141215c) runs on the indexer host as universe-explorer-electrs, indexing the same Bitcoin Core the explorer reads. Loopback only, no public listener, no third-party API or fallback anywhere in the path. It runs in light mode, which is what makes the storage fit, and reads Core's block files through a POSIX default ACL on the bitcoin group, documented in DEPLOYMENT.md because it is invisible and its absence fails days later.

MEMPOOL.BACKEND becomes esplora.

The routing

With esplora the explorer backend deliberately does not mount the address, script hash, transaction, block or mempool routes: it expects the edge to send that family to the index. The gateway now owns that split, the same way upstream does in nginx.

/api/v1/universe/*  ->  overlay
/api/v1/chains      ->  overlay
/api/v1/{bitcoin,dogecoin,zcash}/*  ->  overlay
/api/v1/*           ->  explorer backend
/api/internal/*     ->  refused here, never proxied
/api/*              ->  the local index, with /api stripped

Get this wrong and the site loads perfectly while every one of those paths 404s, so there is an exhaustive routing table test, including that the index never sees a /api/v1/ path and that a deployment with no index configured keeps the old behaviour exactly.

The sentences

Failures travel as names now, not as statuses. address-backend-unavailable, address-backend-syncing, address-history-too-large, address-query-timeout, invalid-address, upstream-unavailable. The page renders a different sentence for each, and a syncing index gets a progress report rather than an apology: "Indexed through block X of Y."

The HTTP error pipe no longer reads statusText at all, so "405 OK" cannot be rendered by construction rather than by matching that one string.

Search reads the same capability document and says, before a reader clicks, that address history is unavailable or still indexing. The address stays recognised and stays clickable, because it is still the right destination and it starts working on its own.

The gates

Address lookup is a capability with a readiness rule that lives in one file and is obeyed by the capability document, the release preflight, the cutover verification and the production synthetic check, so those four cannot drift apart.

Ready means: configured, answering, reporting an indexed height, within MAX_BEHIND_TIP of Core, and a real address summary and a real UTXO query both returned usable documents. A listening port is not readiness.

  • MEMPOOL.BACKEND=none is a release blocker.
  • esplora without a configured endpoint is a release blocker.
  • Preflight checks the index's genesis block, its lag, and makes real summary, history and UTXO requests.
  • Every configured source, fallbacks included, must be loopback or a Unix socket.
  • After the switch, verify_live opens the public address contract through the gateway and asks the backend directly for an address, requiring a refusal: a backend that answers means the gateway is routing /api/ to the wrong upstream.
  • The production synthetic check now asks this origin for addresses of every script type, pages one with more history than a page holds, and requires a malformed string to be called malformed.
  • A browser smoke opens real address pages and fails on the sentences that shipped.

Proving the gates can fail

Every new safeguard has a negative test. The address page audit is fed the production page line for line and required to reject it; a failing status beside any success phrase is refused, not just "405 OK"; an empty table over a non-empty index is refused, because that one looks like a working page. The Esplora contract rules are the same functions the deployment's own readiness probe calls, so a fixture that is more generous than the real provider is a failing test rather than a passing one forever.

One thing found on the way

bc1qcx70rmarfudyct7lx0ptrat2c5kgstghx2j69, the address in the screenshot, is not a valid address. It is 41 characters where a P2WPKH is 42 and its bech32 checksum does not verify; Bitcoin Core rejects it. It is kept as a regression case for exactly that reason: it is what a reader gets when an address is mistyped, and what the site did with it was answer a contradiction and then blame their address. It must now be called invalid, and the checks hold the origin to that.

Tests

  • backend: 60 passed, 9 suites
  • frontend: 458 passed, 32 files
  • gateway routing: 24 passed
  • address page audit: 13 passed
  • source gates: text, origins, branding, colours, palettes, fills, build assets

🤖 Generated with Claude Code

Bitcoin Universe and others added 10 commits August 30, 2026 19:18
The public site offered address search over a deployment that could not
answer one. Every address returned 405 "Address lookups cannot be used
with bitcoind as backend", the page rendered that beside the word OK
because it pasted the status number to the proxy's reason phrase, and
underneath it told the reader their address had too many transactions
for the backend to handle. Three claims, none of them true, and every
release gate green throughout, because no gate had an opinion about
address lookup at all.

A first-party mempool/electrs now answers the address family, and the
gateway owns the split: /api/v1/ stays with the explorer backend and the
overlay, /api/ goes to the index with the prefix stripped, and the
index's internal routes are refused rather than proxied. That split is
not optional with BACKEND=esplora, because the backend deliberately does
not mount those routes in that mode.

The rest is making the failure impossible to repeat rather than merely
fixed. Address lookup is a capability now, with a readiness rule that
lives in one place and is obeyed by the capability document, the release
preflight, the cutover verification and the production check, so the
four cannot drift. Readiness means a real address query and a real UTXO
query answered and the index is within two blocks of Core; a listening
port is not readiness. The reasons an address lookup can fail travel as
names rather than as statuses, so a missing index, a timeout and an
address that genuinely has too much history are three different
sentences instead of one wrong one. The pipe no longer reads the status
line's reason phrase at all, so "405 OK" cannot be rendered.

BACKEND=none is a release blocker, a configured index has to answer
genuine summary, history and UTXO queries before a cutover is allowed,
every configured source including fallbacks must be loopback or a Unix
socket, and the production smoke opens real address pages of every
script type in a browser and fails on the sentences that shipped.
The address index is a separate process from the backend and the overlay,
with its own reasons to be down: a rebuild, a compaction, the disk guard
stopping it before it fills a shared volume. What must not happen then is
the rest of the origin going with it, and what must never happen is an
address answering 200 with an empty body, because a zero balance rendered
from a missing index tells somebody their money is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The account was locked for billing, and every hosted job began failing in
two seconds with no steps and no log. One of them is a required check, so
a gate that exists to prove Safari and Chrome on iOS still work stopped
proving anything, and there was nothing to read about why.

The Windows fleet was idle and needs no package to run these engines:
Playwright ships what WebKit and Firefox need there, so the apt command
the Linux fleet has no sudo for is not needed at all. The frontend ng
script becomes portable in the same change, because it invoked a .js file
as a program and only a POSIX shell will do that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every other unit this deployment runs is in production/linux and is packed
into the artifact, because taking a unit from whichever checkout happens to
be on the host is how a service ends up running a configuration nobody can
point at. The index was installed by hand and its units existed only on the
machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A release may move which upstream owns a path, and this one does: the
explorer backend stops mounting the address, transaction, block and
mempool routes and the index starts serving them. The two directions are
not symmetric. A new gateway in front of an old backend is fine. An old
gateway in front of a new backend sends /api/ to a process that no longer
answers it, so every transaction, block and address page 404s until the
gateway catches up.

Restarting the backend first put the origin through that half of the swap
on purpose. Its socket unit holds the port across the gateway's own
restart, so going first costs nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The progress line guarded on the height being non-null with the safe
navigation operator, which answers undefined rather than null when the
document itself is missing, so the guard passed and the interpolation
below it read a property off nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The address capability published Core's own last-checked time as the
feature's last successful update. Core is never behind itself, so that
value stays current while the index it is being compared against goes
stale, which is the one moment the field exists to expose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The browser gate counted class names that do not exist in this build, so
its check for an empty table over a non-empty index would have reported
every working address page as showing nothing. Measured against a live
transaction page: the rows carry data-cy="tx-<index>", and the per-row
link is hidden when the list is inside a transaction page, so counting
links measures which page it is on rather than whether it has content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
electrs finishes its initial index before it binds its HTTP port, so the
first build reports as unavailable rather than syncing: there is nothing
listening to ask how far it has got. Reading that as a fault would send an
operator looking for a broken index that is working exactly as intended.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The account hit its Actions storage quota, and every browser job began
reporting failure with "No mobile failures" printed directly above the
error. The screenshots are a report about the gate, not the gate, and the
step that decides the job has already run by then, so an upload that
cannot store them can no longer invert the verdict. It cannot hide a red
one either, for the same reason.

The release artifact upload is deliberately left alone: there the upload
is the product.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bitcoinuniverseadmin

Copy link
Copy Markdown
Author

#40 is merged, which should clear every check that was failing on this PR for reasons that had nothing to do with it:

  • Frontend build finished in 18m23s on that run instead of reaching the 100 minute cap. The deep visual pass is now three sharded jobs beside it.
  • Mobile and adaptive layout and WebKit and Firefox were failing on Failed to CreateArtifact: Artifact storage quota has been hit, at the last step, after every measurement passed. Screenshot uploads now only run on failure.
  • WebKit and Firefox also lost its GitHub-hosted runner when the organization moved to the Free plan today; develop now targets RunsOn for it.

Merge develop into this branch and the run should go green without any change on your side. Analyze (actions), Analyze (javascript-typescript) and Analyze (rust) will still fail, because CodeQL runs on GitHub-hosted runners that the current plan does not provide. Those are not required checks, so they do not block the merge.

Happy to do the develop merge for you if you have moved on.

Bitcoin Universe and others added 3 commits August 30, 2026 22:22
Two workflow files needed a decision rather than a merge.

The WebKit and Firefox job: develop moved it to RunsOn Spot, which is what
the workspace CI policy asks for and which landed first. That replaces the
move to the Windows fleet made here for the same reason, so develop's
runner, its dependency-install fallback and its comment are what remain,
and the Windows-only shell default goes with them.

The artifact uploads: develop added if-no-files-found, which handles a
directory that is not there. This branch added continue-on-error, which
handles a store that refuses to take one. They are different failures and
both are kept. The address page steps are added beside the chain page
steps they were written to sit next to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
develop's if: failure() keeps a green run from attempting an upload at
all, which is most of the problem. The comment here was written before
that landed and claimed the whole of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four and a half hours of per-batch lines rotated the journal on the
indexer host, taking every other service's history with it. This is the
same failure this deployment has had before, where a noisy component
buried every real error, and it is worth a cap rather than a habit of
reading du instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bitcoinuniverseadmin

Copy link
Copy Markdown
Author

Second correction on top of my earlier note, and this one was my fault.

#40 split the deep visual pass into three shards that each built their own copy, which took a run from two concurrent Angular production builds to five, on a fleet that runs more than one runner per host. Everything that started a build in the same ninety second window was killed with exit code 137, out of memory, including on pull requests that touched nothing but scripts. That is what put Frontend build, Mobile and adaptive layout and the shards red on this PR after I told you #40 would clear them.

#48 is merged and fixes it: the frontend job keeps its build under a commit-keyed cache and the shards take it, so a run is back to two concurrent builds. Verified on its own run, 33340340913: all eight jobs green, every shard's fallback build step skipped because the cache hit, whole run 48 minutes.

Merge develop into this branch now and it should go green. Still expected to fail and still not required: Analyze (actions), Analyze (javascript-typescript), Analyze (rust), which need GitHub-hosted runners the current plan does not provide.

Sorry for the churn. Say the word and I will do the develop merge for you.

@bitcoinuniverseadmin

Copy link
Copy Markdown
Author

Your required checks are green, so this is ready as far as they go. One thing to know before it merges: this branch is from develop as it was at 23:26Z, and #48 landed at 23:43Z. So the run that went green here did not include #48, and #48 is the change that stops the visual shards each building their own copy.

Your universe-ci.yml hunks are additive and sit in the docs and frontend jobs, well away from what #48 touched in the visual job, so I would expect a clean combine rather than a conflict. It is still a green result for a tree nobody has actually run.

One more develop merge and a re-run would make the verdict match what gets merged. I did not push it myself because you were active on the branch twenty minutes ago and I did not want to land on top of you. Say the word if you would rather I did it.

@bitcoinuniverseadmin

Copy link
Copy Markdown
Author

Status: green, and deliberately not merged yet

Every Universe CI check passes on c433e607e.

Check Result
Backend build and test pass
Documentation gates pass
Frontend build pass
Mobile and adaptive layout pass
Visual matrix, all three shards pass
WebKit and Firefox pass

The Analyze jobs are red for a reason unrelated to this branch: the account is locked for billing, so every GitHub-hosted job fails in two seconds with no steps and no log. They are not required checks. An earlier round of red on this PR had the same shape and none of it was code either: Frontend build and the three visual shards all died within ninety seconds of each other with exit 137 and "the runner has received a shutdown signal", and passed on re-run.

Why this is not merged yet

This branch makes MEMPOOL.BACKEND=none a release blocker. Production is on none right now, and its address index is still building. So the moment this lands on develop, the next release cut from develop cannot pass preflight until that index is ready and production has been switched over.

That block is the point of the change. It is also not something to leave sitting in front of other people's releases for hours. So the merge is sequenced with the cutover rather than run ahead of it: the index finishes, production moves to it, and this lands. Nothing is waiting on review, and an open PR blocks nobody.

Where the index is

The tx-store pass is at 485 GB and compacting, writing 1 GB SST files. 945 GB free on the volume, against a guard that stops the index at 120 GB. The history pass follows, and electrs binds its HTTP port only after both are done, which is the readiness signal the cutover waits on.

One operational fix went in along the way: four and a half hours of per-batch index logging rotated the shared journal on the indexer host and took every other service's history with it. The unit now rate-limits its own journal share. It is not being restarted to apply that, because the running pass has hours invested in it and a logging cap is not worth the risk.

@bitcoinuniverseadmin
bitcoinuniverseadmin merged commit 4d13904 into develop Aug 31, 2026
8 of 11 checks passed
@bitcoinuniverseadmin
bitcoinuniverseadmin deleted the agent/fix-bitcoin-address-backend-20260830-203500 branch August 31, 2026 00:33
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.

1 participant