Commit Graph
2 Commits
Author SHA1 Message Date
Dario Gabriel LipicarandClaude Opus 5 a1a17acbc5 feat: wire the heartbeat's health signal, and add fetchFinalizedRoot
Three fields — head.blockNumber, head.updatedAt and heartbeatFailures — were
read by statusSnapshot() and never assigned, and State::Degraded appeared only
in stateName(). The heartbeat was fire-and-forget with a comment pointing at a
pollHeartbeat() that does not exist, so nothing ever observed its outcome:
status().head stayed "" for the life of the process and a proxy whose sync had
died still reported "running".

CallSlot now carries a Kind, so the trampoline can tell a user call from a
heartbeat or a head probe. Three consecutive heartbeat failures degrade the
proxy and one success clears it; head is refreshed by a separate
eth_blockNumber probe every fifth beat, since eth_syncing answers a hardcoded
`false` and cannot report it. live() joins Running and Degraded for callers
making lifecycle decisions, leaving running() strict for health.

fetchFinalizedRoot() is new, and lives here rather than in the panel because
Basecamp sandboxes ui_qml plugins away from the network: an XMLHttpRequest from
a view is refused outright. It is a convenience, not a trust anchor, and says
so. Adds libcurl, used for that one request and nothing else.

Tests spin on the condition rather than sleeping a fixed interval — the first
draft was green on an idle machine and red under a parallel build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 14:51:11 -03:00
Dario Gabriel LipicarandClaude Opus 5 421624641a feat: wrap nimbus libverifproxy as a Logos module
Adds `verified_proxy_module`, a universal C++ core module over status-im's
`libverifproxy` — the C library form of nimbus_verified_proxy. Where
`eth_rpc_module` forwards JSON-RPC to a provider and trusts the answer, this
verifies every response against the beacon-chain light client's attested
execution state, so a lying provider produces an error rather than a wrong
value.

Nobody had packaged libverifproxy with Nix before: upstream's flake builds the
verified-proxy *binary* but not the library, and a global code search for
`libverifproxy` in nix files returns nothing. Rather than write a derivation,
flake.nix re-targets upstream's own — `.override { targets = ["libverifproxy"]; }`
composes because callPackage's makeOverridable merges previously-applied args,
so their pinned Nim survives — and then fixes the three things that break:

  * installPhase installs only `-type f -executable` into $out/bin, so a .a and
    a .h yield an EMPTY $out (and installCheckPhase then runs the literal
    string "$out/bin/* --version");
  * env.NIMFLAGS is ASSIGNED, not appended, so ours have to extend it;
  * preBuild builds vendored RocksDB unconditionally although `make
    libverifproxy` never reaches that target. `nm -u` on the result confirms
    zero rocksdb references, so it is dropped rather than swapped for
    dynamicRocksDB (which on Windows would demand a *cross* RocksDB).

Three NIMFLAGS additions are load-bearing rather than tuning:

  * `-d:noSignalHandler` — library/nim.cfg omits it, so NimMain() would install
    Nim's SIGINT/SIGSEGV/SIGABRT handlers over the HOST's. Verified by dlopen'ing
    a probe and comparing sigaction before/after: the host's handler survives.
  * `--passC:-fPIC` — Nim only adds it when optGenDynLib is set, which
    --app:staticlib does not; upstream's dist script adds it for linux-arm64
    only. The archive is linked into a SHARED plugin.
  * `-d:release --debugger:off` — upstream ships debug info, which dominates
    the artifact (~99MB uncompressed in the release tarballs vs 31MB here).

The library can also take the host process down, which a plugin cannot tolerate,
so ProxyConfig whitelists the two fields that reach a Nim `quit()`: an
unrecognised `eth2Network` reaches getMetadataForNetwork's `fatal` + `quit 1`,
and any `logLevel` Nim's updateLogLevel rejects reaches setupLogging's `quit 1`.
Neither is validated upstream. Everything else (bad JSON, missing
trustedBlockRoot, malformed URL) is already caught and turned into a NULL
return, so validating it only improves the message.

ProxyRuntime owns the one thread that may touch the C ABI at all: the library
spawns none, startVerifProxy blocks through an unbounded prologue, and
setupForeignThreadGc/tearDownForeignThreadGc are bound to start/stop. Notable
consequences encoded here:

  * processVerifProxyTasks only poll()s while pendingCalls > 0, so an IDLE PROXY
    DOES NOT ADVANCE ITS LIGHT CLIENT. The heartbeat is
    proxyCall("eth_syncing","[]"), which drives beaconSync() and touches no
    execution backend. Its return value is a hardcoded `false` and useless; its
    error string is the only machine-readable sync-health signal the ABI has.
  * Drain BEFORE stopVerifProxy: it sets ctx.stop, which processVerifProxyTasks
    checks before polling, so afterwards no callback can ever fire.
  * Call slots use joint ownership (waiter + heap CallBox) rather than
    storage-module's `abandoned` flag, so a late callback after a timeout is
    safe by construction. There is no per-call cancel in the C API.
  * concurrency:"multi" spawns a QThread per call rather than using a bounded
    pool, so admission control is mandatory, not a nicety.

All ~60 eth_*/op_* entry points can route through one FFI path, because
proxyCall is a string `case` over the same procs the typed C exports call.
This commit lands 8 representative methods covering every wire type; the rest
are mechanical.

Verified on aarch64-darwin: the archive links into a .dylib; NimMain initialises
under dlopen; a bad config returns NULL rather than quitting; the plugin builds
at 15MB with the archive absorbed (hence `include: []`); and 28/28 unit tests
pass against a mocked C library that — unlike mock_libstorage — queues
completions and drains them only from the pump, so the cross-thread design is
actually exercised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 22:54:33 -03:00