Skip to content

Promote: the address index, the public Zcash fee, and the honest source readings - #51

Merged
bitcoinuniverseadmin merged 49 commits into
mainfrom
develop
Aug 31, 2026
Merged

Promote: the address index, the public Zcash fee, and the honest source readings#51
bitcoinuniverseadmin merged 49 commits into
mainfrom
develop

Conversation

@bitcoinuniverseadmin

Copy link
Copy Markdown

Second promotion of the night. main was cut over to 2d88cefc4 earlier; develop has moved 24 commits past it since.

What this deploys

Product

  • The Bitcoin address index, and an address page that says something true when the index is not there instead of rendering a 405 beside the word OK.
  • The public Zcash fee, on the pages. Verified live against the deployed overlay: 20,000 zatoshi, evidence.source = node-and-value-pool, both readings agreeing, rule ZIP-317-revision-1 resolved to NU6.3 by branch 37a5165b, conventional 10,000 and marginal 5,000.
  • The Mining page states the denominator its means were taken over. On Zcash that is about 920 of a 960 block window, because a shielded coinbase leaves a block whose reward cannot be derived. It used to print "measured over the last 960 collected blocks" under a mean that was not.
  • Every provable protocol asset on chain transaction and address pages.

Evidence and honesty

  • The protocol roster gate: the pinned manifest carries its schema, registry version, source repository and source commit, and --against compares it with what a deployment actually serves. It runs in the hourly smoke and passed there.
  • PROTOCOL-ROSTER.lock, so a protocol cannot leave the roster without an edit that says so.
  • The upstream reconciliation: all 40 outstanding upstream commits classified, the exposure measured directly rather than inferred, and the one advisory that ships named with its reason for not being taken in a release step.
  • A release evidence record for the day.

Operations

  • A cutover refuses to proceed while something answers on a public interface.
  • The gateway restarts before the backend, not after.
  • The address index unit files travel with the release.

Validation

Every one of these merged with the five required checks green: Documentation gates, Backend build and test, Frontend build, Mobile and adaptive layout, WebKit and Firefox, plus the three visual matrix shards on most of them.

The three Analyze jobs are red on every pull request in this repository and are not required. CodeQL needs GitHub-hosted runners, and the organization is on the Free plan as of yesterday evening.

After this merges

release.sh cutover, then the hourly production smoke should lose its last failure: address: this deployment publishes no address capability at all, which is this promotion's content.

🤖 Generated with Claude Code

bitcoinuniverseadmin and others added 30 commits August 30, 2026 17:22
The explorer printed "not public" in the fee column for shielded Zcash
transactions, in the lens tooltip, in the pending table, in the candidate
bucket disclosure and in the chain documentation. That was wrong, and it
taught readers that this chain hides more than it does.

Shielding hides the sender, the recipient, the amount transferred and the
memo. It does not hide the fee, which is the value left in the transparent
transaction value pool once the transparent inputs and outputs, the Sapling,
Orchard and Ironwood value balances and the Sprout JoinSplit public values
are counted. All of those are on chain in the clear.

The copy now says that. A missing fee reads as "not reported" with the read
that failed named beside it, never as a privacy claim. The transaction page
gained the provenance behind the figure: which readings answered, both
readings printed side by side when they disagreed and no fee published, and
the ZIP-317 rule in force with the upgrade that activated it, its activation
height, the marginal fee, the grace actions and the conventional fee. An
unsupported rule is shown as unsupported and never suppresses a fee that
could be read.

The Zcash documentation gained a section on why the fee is public and how it
is established, and the privacy section now names the fee among the public
facts, states the boundary in both directions, and says plainly that this
explorer never asks for a viewing key.

Six new readings tests, updated visual fixtures carrying the real shape, 413
universe tests passing, every documentation and parity gate green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The committed table said 3 of 36 protocols were readable. The manifest the
overlay serves says 6 of 38: dunes and zrc20 joined the registry, and
zerdinals, zrunes and zrc20 are now verified read only with complete
coverage.

Regenerated from the live manifest with
generate-protocol-coverage.mjs --from, so the table and the recorded JSON
agree and the --check gate passes. Nothing here is hand edited.

This is a documentation change and a small one, but a stale coverage table is
the kind of stale that gets quoted: it undercounts what works and it hides
which authorities moved. The remaining 32 are recorded as BLOCKED because
that is what they are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ly say

upstream-base.json carried an empty modifiedSubsystems list and an empty
knownUpstreamConflicts list while UPSTREAM.md described eleven modified
subsystems. The machine-readable file and the human-readable one disagreed,
and the machine-readable one was the one that was wrong.

Both now carry the same eleven subsystems, plus the three files where an
upstream sync predictably conflicts, plus the mirror sha this fork was
measured against.

UPSTREAM.md gains the reconciliation itself. The 40 commits upstream has
added since the recorded mirror were read and classified: 36 dependency
bumps, 2 toolchain, 2 infrastructure, and nothing that changes product
behaviour or fixes a defect this fork carries.

Upstream's bumps do not describe this fork's exposure, because this fork has
its own lockfiles, so the exposure was measured rather than inferred. The
backend has zero advisories. The frontend has eleven, ten of them in build
tooling that never reaches a browser.

The eleventh ships: echarts 5.4.3 carries GHSA-fgmj-fm8m-jvvx, a moderate
XSS advisory fixed only in echarts 6.1.0, a major version upstream has not
taken either. Moving a charting library across a major version touches every
chart on every chain, so it is recorded with its exact identity and left as
the next dependency task rather than slipped into a release unreviewed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A release record of readings rather than intentions: the fee arithmetic
verified against a live node, the Dogecoin coverage figures, the throughput
and disk measurements that show the inscription reindex cannot finish, the
lag defect and what it was reporting, the listener that was public and now is
not, the origin's headers, the dependency exposure measured rather than
inferred, and the protocol coverage production actually serves.

It ends with what was not reached and why, because a release record that only
lists successes is the kind that gets quoted later by someone who needed the
other half.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rendering the page showed three run-on readings: the conventional fee ran
straight into the per-action explanation, and the activation height ran
straight into the branch id, producing "active from block 3,428,143branch
37a5165b". Each secondary reading is a statement about the value above it,
not a suffix to it, so each gets its own line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Mining page printed "Means are measured over the last 960 collected
blocks" under a mean reward that was not measured over 960 blocks. A Zcash
miner may pay its own share to a shielded address, and then neither the
reward nor the fees can be derived from public data; the overlay leaves
those blocks out of the mean rather than counting them as zero, which is
right, and about three percent of every Zcash window is such a block. The
sentence named a denominator the figure did not have.

The overlay now publishes the two counts, so the page states them: the
window, then the blocks the reward mean was taken over and the blocks the
fee mean was taken over, and one line saying why the numbers differ. When
they do not differ it says what it always said.

Both fields are optional. An overlay released before they existed sends
neither, and then `readMeanBasis` reports the window alone, which is the
reading this page had before and is still true. Seven cases cover the
agreeing window, each of the two narrower on its own, a window where
nothing could be read, an overlay that publishes neither, no window at
all, and a count that is not a whole number.

The Zcash visual fixture carries 931 of 960 so the matrix renders the
sentence that only appears when the denominators differ; the Dogecoin one
carries an agreeing window, because every Dogecoin coinbase is
transparent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 record said the volume exhausts before the reindex could finish. Measured
across 90 minutes of indexing, free space did not move: nearly all of that
I/O rewrites pages in place rather than growing the file. Throughput alone is
the blocker.

The second reading also refines the rate to about 33 blocks an hour rather
than 16. Both numbers put the remaining work in years, so the conclusion is
unchanged, but a record that overstates one of its own measurements is worth
less than one that does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
index-doge-tap shipped with HOST=0.0.0.0 and answered /ready to the internet
for as long as it ran, on a host whose firewall is inactive. Nothing caught
it. Every functional check reached the service over loopback, where it behaved
perfectly, and no gate ever asked which interface it was answering on.

release.sh preflight now asks. It reads the listening TCP sockets, subtracts
the three this deployment declares public (SSH, Bitcoin peer to peer, and the
peer restricted Fulcrum endpoint the other host reads), and refuses the
cutover if anything is left, printing the offending sockets.

Docker's bridge address counts as private: it is reachable only from
containers on this host, and the services behind it are the ones loopback
already serves.

Verified on the indexer host: the gate passes as the host now stands, and
refuses within seconds of a throwaway listener binding 0.0.0.0, which is the
exact shape the index-doge-tap misconfiguration had.

Adding a port to PUBLIC_LISTENERS is a decision with a reason beside it.
Discovering one in production is not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
This sat untracked in the shared checkout for at least three hours while
the release it was written for went out. It loads the deployed origin in a
real browser with nothing mocked and reports what each required route
rendered, across the Bitcoin routes that must not regress and the
Dogecoin and Zcash parity surfaces, which is exactly the check to run
after a frontend cutover.

Committed as it was found, beside the rest of the visual QA harness.
Nothing runs it in CI yet: it needs a deployed origin, so it belongs to
the release procedure rather than to a pull request.

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>
Found by merging on a Windows checkout, where git writes CRLF. JavaScript
counts a carriage return as a line terminator, so the dot in `/#.*$/` will
not cross one and `$` cannot assert an end after one. The comment
stripper matched nothing, and the gate reported all three comment lines
of PROTOCOL-ROSTER.lock as protocols that had disappeared:

  # Every protocol id this explorer has published. is in
  PROTOCOL-ROSTER.lock but no longer in the pinned manifest.

Linux CI never saw it, because Linux checkouts are LF, so the gate would
have stayed green while being unusable for anyone developing on Windows.
The lock is normalised before it is parsed, the way every other file this
gate reads already is, and a case renders the lock with CRLF and asserts
the same answer as LF.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Splitting the deep visual pass into three shards that each build their own
copy took a mempool run from two concurrent Angular production builds to
five, started within ninety seconds of each other, on a fleet that runs
more than one runner per host. Run 33335430574 is what that costs: the
three jobs that started at 21:48 passed, and every build that started at
21:52 was killed with exit code 137. Out of memory, on a pull request
that changed two script files and nothing else.

That is my regression. The split was right, the wall clock proved it, and
paying for it in memory was not part of the trade I wrote down.

The frontend job now keeps its build under a key made from the commit,
and the shards take it. Three builds per run become zero. The cost is
that the shards wait for the frontend job, so a run is about fifty
minutes rather than thirty-six, against the ninety-five it was before the
split and the timeout it usually ended in.

A cache miss builds rather than fails, so a shard can still run on its
own, and it says so in the log because it is paying the memory this
change exists to save. Serving refuses to start without a built frontend,
rather than starting a gateway over an empty directory and failing later
in a way that reads like a page problem.

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>
…isual-shards

Build the frontend once per run, not five times
bitcoinuniverseadmin and others added 19 commits August 30, 2026 23:43
…ine-ending

Read the roster lock the same on either line ending
#48 took the three shard builds out. This takes the last spare one out of
the mobile gate, for the same reason and with the same mechanism.

The comment on that job said it builds its own copy so the two gates
cannot make each other fail or wait. That was written when a run made two
builds and it stopped being free. The fleet runs more than one runner per
host, and a second concurrent Angular build is now enough to have a
runner killed mid-step: run 33342564898 lost this job to "The runner has
received a shutdown signal" and a Killed, with one other run in flight.

Waiting costs nothing here. The visual shards already gate on the
frontend job, so a run is as long as its longest shard either way. What
changes is that a run makes one build instead of two, which is half the
memory it asks a host for.

A cache miss still builds rather than fails, and says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ss-backend-20260830-203500

Give Bitcoin addresses an index, and the page a true sentence
…nominator

Say which blocks the mining means were taken over
The two coverage files conflicted because develop re-recorded them from
the deployed overlay after the roster contract landed: seven readable
protocols rather than six, with the schema, registry version, source
repository and source commit the pin now carries. Those take develop's
version.

Nothing else on this branch is touched. The upstream reconciliation, the
pinned upstream base and the release evidence record are the work this
branch exists for and they carry through unchanged.

Verified on the merged tree: the roster gate passes, its twenty-five
cases pass, and the text gate passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ce-pass

Keep the public-origin acceptance pass
Refuse a cutover while something answers on a public interface
…reconcile

Reconcile the recorded state with production and with upstream
Show the fee a shielded Zcash transaction pays
@bitcoinuniverseadmin
bitcoinuniverseadmin merged commit 2e1bf70 into main Aug 31, 2026
8 of 14 checks passed
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