Feature/tmosley/scanner upgrades - #63
Open
tmosleyIII wants to merge 4 commits into
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scanner reliability and readability
Surfaces silent scanner failures and makes scan results interpretable by the operators who act on
them. Covers both scanners: the network scanner (why a validator could not be reached) and the
history scanner (why an archive was flagged).
Both halves come from the same failure mode, found in production twice: Radar knew something was
wrong but could not say what, so the diagnosis fell to a human, and in one case the operator was
told to fix something that was not broken.
Motivation: two incidents
A validator became invisible after changing IPs. An operator moved their nodes to new addresses
and asked what Radar needed in order to pick them up. The nodes were healthy and visibly
participating in consensus via SCP, but Radar would not update the address, because it only accepts a
new endpoint after a completed overlay handshake — and the handshake was failing. Radar reported only
that the peer did not connect.
The actual cause was a TLS-terminating reverse proxy in front of port 11625: connecting to the
overlay port produced a TLS handshake instead of stellar-core. Establishing that took a day of manual
work — reproducing from independent validators, capturing terminal output, and writing a diagnostic
report for the operator — to reach a conclusion the scanner already had the bytes to determine.
Nothing in Radar surfaced it.
Healthy archives were reported as corrupt. Separately, Radar was telling archive operators to run
repairs on archives that were provably fine, because the scanner's own hasher could not parse newer
transaction entries and treated its failure as evidence against the archive (§3).
In both cases the information needed to explain the failure existed at the point of failure and was
discarded. This PR keeps it.
1. Network scanner: connection failure diagnostics
Connection attempts previously collapsed into a generic failure. A validator unreachable because of
DNS, a closed port, a TLS listener on the overlay port, or a network-passphrase mismatch all looked
identical in the logs, so operators had nothing actionable — the incident above is exactly this.
packages/crawler/src/connection-attempt.ts) — 14 distinct outcomes(
dns_failed,tcp_refused,tcp_timeout,tls_detected,http_detected,invalid_overlay_frame,overlay_auth_failed,protocol_incompatible,wrong_network,unexpected_public_key, …) attributed to a failure stage (dns,tcp,overlay_hello,overlay_auth,identity).(
packages/node-connector/src/connection/connection-protocol-error.ts) — inspects the frameheader to distinguish
OVERLAY_TLS_DETECTED,OVERLAY_HTTP_DETECTED,OVERLAY_WRONG_NETWORKandOVERLAY_PROTOCOL_INCOMPATIBLEfrom a generic parse failure.OVERLAY_TLS_DETECTEDis theincident above: a reverse proxy answering the overlay port with a TLS handshake is now named at
the point of failure, rather than requiring a day of manual reproduction to identify.
packages/crawler/src/crawl-logger.ts) — each crawl emits oneConnection failure summaryline with counts per outcome, instead of leaving the pattern to bereconstructed from scattered lines.
packages/crawler/src/overlay-probe.ts) — a single endpoint can be probeddirectly and its outcome classified, so reachability can be checked on demand rather than only as
a side effect of a full crawl. This is the automated form of the manual reproduction the incident
required.
2. Network scanner: validator endpoint discovery
Addresses the other half of that incident: a validator whose address changes should not have to wait
on a successful handshake at the old address to become visible again. Adds the machinery behind
"known peers" — candidate endpoints for a validator, how they are discovered, probed, and promoted.
ValidatorEndpointCandidatedomain model with DNS resolution, address policy, probe observations,and a candidate manager (
apps/backend/src/network-scan/domain/node/endpoint/)1785360000000-validator-endpoint-candidates)EndpointCandidateAdminRouter)3. History scanner: a silent failure that misreported healthy archives
The second incident. Radar was telling archive operators to run repairs on archives that were
provably fine — SDF Validator 1 (
GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH) showed144 × TRANSACTION_SET_HASHerrors and a "start repair at ledger …" prompt.Root cause
The deployed WASM hasher was
0.9.1, built againststellar-xdr26, which cannot decodeprotocol-27 transaction entries. The failure path:
.unwrap()→ wasm panic → opaqueRuntimeError: unreachable)processingErrors; no transaction-set hash is ever calculatedverifyTransactionsfinds the hash missing and silently substitutes the empty-transaction-sethash
TRANSACTION_SET_HASHmismatchreported against a healthy archive
A scanner-side parse failure was laundered into an accusation of archive corruption. Any future
protocol bump would have done the same thing to every archive Radar scans.
Evidence the archives were never at fault
rs-stellar-archivist --verify(independent Rust implementation) over the affected range:364 files, 0 missing or corrupt
stellar-xdr26, 27 and 28 in one binary, over 2,048 real transactionentries across 16 checkpoints spanning ledgers 20M–64.08M: hashes identical and correct in
all three; only v26 fails, and only to parse p27 entries
0.9.1and0.9.2WASM artifacts tested directly against real archive dataFixes
Hasher upgraded
0.9.1→0.11.0(^0.11.0in bothapps/history-scannerandapps/backend;the caret bump is required because
^0.9.xcannot cross a0.xminor).0.11.0also returns acatchable
Errorinstead of panicking across the wasm boundary, and fixes a latentUint8Array::viewuse-after-free that aliased wasm memory owned by a freed temporary.Scanner no longer attributes its own faults to the archive:
SCANNER_ERRORcategory (ScanError.ts)processingErrorsnow carry the ledger, recovered from the entry header even when the workerfails (
CategoryXDRProcessor.ts)CategoryVerificationServiceskips ledgers it could not process rather than comparing a hashit never computed — including the knock-on case where an unprocessed ledger breaks the next
ledger's header-chain check
CategoryScannerthat re-reported every processing error a secondtime at a hardcoded
ledger: 0— the source of both double-counted errors and the nonsensicalfirstLedger: 0in stored scan recordsUI stops prompting repairs for scanner faults:
HistoryArchiveScangainsarchiveErrors/scannerErrorsaccessors; the amber "start repair at ledger N" banner now requires a genuine archiveerror, and scanner faults render as a neutral "Archive partially unverified — this is a limitation
of the scanner, not a problem with the archive; no action is needed."
Measured effect
Same ledger range (64081500–64082367) that produced the production errors:
0.9.1(what production ran)TRANSACTION_SET_HASH, ledger 0 → 640823110.9.1+ scanner fixesSCANNER_ERROR(correct ledgers), 0 archive errors0.11.0+ all fixeserrors: []The middle row is the durable part: even with a hasher a protocol behind, healthy archives are no
longer reported as corrupt.
4. History scanner: throughput and correctness fixes found along the way
RangeScannerAdapterdroppedscannedBucketHasheswhen mapping to theIRangeScannerresult, soalreadyScannedBucketHasheswas always empty and every 1M-ledger range re-downloaded and re-hashed buckets it shared with
earlier ranges. On a full pubnet scan (~64 ranges) the large upper-level snap buckets were
re-fetched dozens of times.
ArchivePerformanceTesterhad a hardcoded[50, 35, 25, 20, 15, 10]ladder. Now derived fromHISTORY_MAX_CONCURRENCY(default 100) usingfixed ratios, so the ladder keeps the same six rungs — no extra benchmark cost — and reproduces the
historical list exactly at a ceiling of 50.
uv-thread-pool.ts) — bucket verification runsgunzip → sha256on the libuv pool, which defaults to 4 threads. Now sized from the concurrencyceiling, applied before anything touches the pool. Measured honestly: this made no difference to
end-to-end scan time, which is network-bound at ~41 MiB/s. It removes a ceiling rather than
delivering a speedup.
verify-single-archiveCLI was unrunnable — missing@injectable()and@inject(TYPES.ExceptionLogger)meant it crashed on startup before any scanning. Fixed; this isthe entry point used to reproduce and diagnose archive issues.
uvThreadPoolSize/maxConcurrency, andHISTORY_MAX_CONCURRENCY/UV_THREADPOOL_SIZEdocumented in the history-scanner README.5. Operator documentation
network-scan/ENDPOINT_DISCOVERY_OPERATIONS.md(238 lines) — purpose, current implementation,configuration, operator workflow, acceptance results, regression coverage, remaining work
network-scan/VALIDATOR_DISCOVERY_IMPROVEMENT_PLAN.md(743 lines) — objective, success criteria,non-goals, design principles, target scan flow, domain model
network-scan/README.md— component overview and pointers to the operations docshistory-scanner/README.md— concurrency and thread-pool tuning, and why the two must movetogether
6. Infrastructure
CONTACT_RECIPIENT_EMAILplumbed through the production environment andapp module, enabling an outbound contact path from the backend.
Impact: before and after
Both incidents cost an ecosystem participant real time, and in both cases the scanner already held
the information needed to explain itself.
A validator that changed IP
OVERLAY_TLS_DETECTED/ outcometls_detected, attributed to theoverlay_hellostage. Every crawl emits aConnection failure summarywith counts per outcome, so the same misconfiguration across several validators is visible as a pattern. Any endpoint can be probed on demand viaprobeOverlayEndpointinstead of waiting for a full crawl.An archive reported as corrupt
144 × TRANSACTION_SET_HASHand "Archive verification errors detected. Start repair at ledger 64051774." The operator's rational response is a multi-hour repair and cache purge against an archive with nothing wrong with it. The real cause — the scanner's hasher being one protocol behind — surfaced only as an opaqueRuntimeError: unreachable, swallowed internally and never shown.SCANNER_ERRORagainst the ledgers actually affected, and the UI states plainly that this is a scanner limitation and no action is needed. No repair is ever prompted for a fault on Radar's side. With the hasher upgraded the failures do not occur at all.Measured on the range that produced the production errors (ledgers 64081500–64082367): 8 phantom
archive errors → 0. With the old hasher still in place, 8 → 4 correctly-labelled scanner errors
and 0 archive errors — the guidance to operators is fixed independently of the version bump.
What this changes for ecosystem participants
network view.
network rather than about the scanner.
Testing
history-scannerandshared;tsc --noEmitclean;vue-tsc --noEmitclean across the frontendScanner.test.ts)errors ignored, header chain not blamed on an unprocessed predecessor
(
CategoryVerificationService.test.ts)rungs (
ArchivePerformanceTester.test.ts)history.stellar.org/prd/core-live/core_live_001at each stage, with thepublished
0.11.0artifact pulled from npm and verified on 256 real entriesDeployment notes
maindeclared^0.9.2from 2026-07-09, yetproduction was still serving
0.9.1six weeks later. The version bump alone is not sufficient ifthe same thing swallows this deploy.
overwrites them.
0.11.0is published to npm and crates.io and taggedv0.11.0.Follow-up (not in this PR)
stellar-xdrmajor trails theledgerVersionseen in recent ledgers. Every safeguard here makes the next occurrence legible;this is the one that prevents it. Worth landing before protocol 28 activates.
problem, not a code problem, and will affect something else.
StellarArchivistVerifiertargets the Gostellar-archivistin both argument order and outputparsing; the Rust binary exits 2 for every failure, so it cannot be swapped in without also
reworking the output parsing (its
--reportJSON would be the robust integration).