mirror of
https://github.com/logos-co/logos-verified-proxy-module.git
synced 2026-08-27 04:51:08 +00:00
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>
189 lines
8.4 KiB
Nix
189 lines
8.4 KiB
Nix
{
|
|
description = "libverifproxy — the C static-library form of nimbus_verified_proxy";
|
|
|
|
# libverifproxy is a large archive and the Nim toolchain behind it is slow to
|
|
# build; pull both from the Logos Attic rather than rebuilding per machine.
|
|
nixConfig = {
|
|
extra-substituters = [ "https://cache.nix.logos.co/public" ];
|
|
extra-trusted-public-keys = [ "public:l4HrXgL4nw246+LBh2SOJyhz64BoGegOYLheT/iIAPU=" ];
|
|
};
|
|
|
|
inputs = {
|
|
logos-module-builder.url = "github:logos-co/logos-module-builder";
|
|
logos-nix.url = "github:logos-co/logos-nix";
|
|
|
|
# git+https, NOT github: — the github: scheme does not carry submodules
|
|
# (NixOS/nix#14982) and nimbus' nix/default.nix asserts on `self.submodules`.
|
|
# Needs Nix >= 2.27 for the flake-level `self = { submodules = true; }`.
|
|
nimbus-eth1.url = "git+https://github.com/status-im/nimbus-eth1?submodules=1&ref=refs/tags/v0.4.0";
|
|
};
|
|
|
|
outputs = inputs@{ self, logos-module-builder, logos-nix, nimbus-eth1 }:
|
|
let
|
|
nixpkgs = logos-nix.inputs.nixpkgs;
|
|
lib = nixpkgs.lib;
|
|
|
|
nativeSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
|
systems = nativeSystems ++ [ "x86_64-windows" ];
|
|
|
|
pkgsFor = system:
|
|
if system == "x86_64-windows"
|
|
then logos-nix.lib.mkWindowsPkgs { buildSystem = "x86_64-linux"; }
|
|
else import nixpkgs { inherit system; };
|
|
|
|
libverifproxyFor = system:
|
|
let
|
|
pkgs = pkgsFor system;
|
|
isWin = system == "x86_64-windows";
|
|
|
|
# Windows cannot go through nimbus' own flake: it does
|
|
# `import nixpkgs { system = "x86_64-windows"; }`, which yields a
|
|
# NATIVE Windows package set — it evaluates, and it is unusable.
|
|
# Call their nix/default.nix ourselves with a real cross pkgs set.
|
|
base =
|
|
if isWin then
|
|
pkgs.callPackage "${nimbus-eth1}/nix/default.nix" {
|
|
self = nimbus-eth1;
|
|
# MUST contain the target: nix/default.nix feeds this straight
|
|
# to meta.platforms, and nixpkgs refuses to evaluate a
|
|
# derivation whose meta.platforms omits the hostPlatform.
|
|
stableSystems = [ "x86_64-windows" ];
|
|
# USE_SYSTEM_NIM=1 wants a BUILD-side Nim; pkgs.nim here is a PE.
|
|
# Under that cross wrapper nimscript's `defined(windows)` is
|
|
# already true, so no --os:windows has to be passed by hand.
|
|
nim = pkgs.buildPackages.nim-2_2;
|
|
targets = [ "libverifproxy" ];
|
|
}
|
|
else
|
|
nimbus-eth1.packages.${system}.nimbus_verified_proxy.override {
|
|
targets = [ "libverifproxy" ];
|
|
};
|
|
in
|
|
base.overrideAttrs (old: {
|
|
pname = "libverifproxy";
|
|
|
|
# Upstream has `perl sqlite python3` in buildInputs. perl and python3
|
|
# are Makefile TOOLS, not target libraries — harmless natively, fatal
|
|
# under cross, because nixpkgs marks the mingw python3 BROKEN and the
|
|
# derivation then refuses to evaluate.
|
|
buildInputs =
|
|
if isWin
|
|
# nixpkgs builds mingw-w64 against mcfgthread, so pthread.h and
|
|
# libpthread.a exist nowhere in the default closure — but the
|
|
# vendored C assumes POSIX threads regardless.
|
|
then [ pkgs.sqlite pkgs.windows.pthreads ]
|
|
else old.buildInputs;
|
|
|
|
nativeBuildInputs = old.nativeBuildInputs
|
|
++ lib.optionals isWin (with pkgs.buildPackages; [
|
|
perl python3 gnumake
|
|
nasm # nim-boringssl's Windows branch shells out to `nasm -f win64`
|
|
]);
|
|
|
|
makeFlags = old.makeFlags ++ lib.optionals isWin [
|
|
# nim-libbacktrace vendors libbacktrace and configures it POSIX-shaped.
|
|
"USE_LIBBACKTRACE=0"
|
|
];
|
|
|
|
# Upstream builds the VENDORED RocksDB in preBuild unconditionally
|
|
# ("takes almost double the time"), although `make libverifproxy`
|
|
# never reaches the rocksdb target: deps is
|
|
# `deps-common nat-libs nimbus.nims build/generate_makefile`.
|
|
# Dropping it beats dynamicRocksDB = true, which would instead put a
|
|
# (cross, on Windows) rocksdb in buildInputs.
|
|
# Verify: nm -u $out/lib/libverifproxy.a | grep -c rocksdb_
|
|
preBuild = lib.optionalString isWin ''
|
|
# --app:staticlib makes Nim shell out to a BARE `ar`, and a cross
|
|
# stdenv has only x86_64-w64-mingw32-ar on PATH. The nixpkgs nim
|
|
# wrapper rewrites gcc.exe/gcc.linkerexe from $CC/$CXX but never the
|
|
# archiver, and nim exposes no config key for it.
|
|
mkdir -p $TMPDIR/arshim
|
|
ln -sf "$(command -v $AR)" $TMPDIR/arshim/ar
|
|
export PATH=$TMPDIR/arshim:$PATH
|
|
|
|
# nimbus-build-system's nat-libs targets branch on $(OS) — the
|
|
# cmd.exe variable, empty on a Linux builder — so a cross build
|
|
# silently takes their POSIX branch and the archives then call
|
|
# their own symbols through __imp_ stubs.
|
|
make -C vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc \
|
|
CC="$CC" AR="$AR" RANLIB="$RANLIB" \
|
|
CFLAGS="-Os -DMINIUPNP_STATICLIB" build/libminiupnpc.a
|
|
make -C vendor/nim-nat-traversal/vendor/libnatpmp-upstream \
|
|
CC="$CC" AR="$AR" RANLIB="$RANLIB" \
|
|
CFLAGS="-Wall -Os -DENABLE_STRNATPMPERR -DNATPMP_MAX_RETRIES=4 -DNATPMP_STATICLIB" \
|
|
libnatpmp.a
|
|
'';
|
|
|
|
env = old.env // {
|
|
NIMFLAGS = old.env.NIMFLAGS
|
|
# library/nim.cfg omits noSignalHandler, so NimMain() would
|
|
# install Nim's SIGINT/SIGSEGV/SIGABRT handlers over the HOST's.
|
|
+ " -d:noSignalHandler"
|
|
+ " -d:release --debugger:off -d:disableLTO"
|
|
# Nim only adds -fPIC when optGenDynLib is set, and --app:staticlib
|
|
# does not set it. The archive is linked into a SHARED plugin.
|
|
# Meaningless on PE.
|
|
+ lib.optionalString (!isWin) " --passC:-fPIC";
|
|
};
|
|
|
|
# Upstream 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".
|
|
installPhase = ''
|
|
runHook preInstall
|
|
mkdir -p $out/lib $out/include
|
|
install -m444 build/libverifproxy/libverifproxy.a $out/lib/
|
|
install -m444 build/libverifproxy/verifproxy.h $out/include/
|
|
runHook postInstall
|
|
'';
|
|
doInstallCheck = false;
|
|
});
|
|
# A flake-SHAPED attrset, not a flake: resolveExtInput only needs
|
|
# `x.packages.${system}.<name>`.
|
|
#
|
|
# Use the structured { input; packages.default; } form and NOT the barer
|
|
# { packages.<sys>.default = drv; } escape hatch: buildCppPlugin accepts
|
|
# both, but mkLogosModuleTests only checks `value ? input` and otherwise
|
|
# hands the raw attrset to mkExternalLib as a `src`. The plugin would
|
|
# build and the unit tests would fail to EVALUATE.
|
|
libverifproxyFlake = {
|
|
packages = lib.genAttrs systems (s: { libverifproxy = libverifproxyFor s; });
|
|
};
|
|
|
|
nimbusRev = nimbus-eth1.rev or nimbus-eth1.shortRev or "unknown";
|
|
|
|
module = logos-module-builder.lib.mkLogosModule {
|
|
src = ./.;
|
|
configFile = ./metadata.json;
|
|
flakeInputs = inputs;
|
|
|
|
# `verifproxy`, not `libverifproxy`: find_library searches lib${name}.a,
|
|
# which maps onto the real libverifproxy.a.
|
|
externalLibInputs.verifproxy = {
|
|
input = libverifproxyFlake;
|
|
packages.default = "libverifproxy";
|
|
};
|
|
|
|
# The library exposes no version symbol, so stamp the upstream revision
|
|
# in at build time for status()/libraryVersion().
|
|
preConfigure = ''
|
|
printf '#define VERIFIED_PROXY_NIMBUS_REV "%s"\n' "${nimbusRev}" \
|
|
> src/verified_proxy_nimbus_rev.h
|
|
'';
|
|
|
|
tests = {
|
|
dir = ./tests;
|
|
# Keeps the ~25-minute upstream build out of the test derivation
|
|
# entirely; unit tests link mocks/mock_libverifproxy.cpp instead.
|
|
mockCLibs = [ "verifproxy" ];
|
|
};
|
|
};
|
|
in
|
|
module // {
|
|
packages = lib.genAttrs systems (system:
|
|
(module.packages.${system} or {}) // {
|
|
libverifproxy = libverifproxyFor system;
|
|
});
|
|
};
|
|
}
|