mirror of
https://github.com/logos-co/logos-verified-proxy-module.git
synced 2026-08-27 04:51:08 +00:00
test: an executable tutorial that verifies against live Sepolia
Adds doctests/verified-proxy-runtime.test.yaml and the workflow that runs it: package this commit as an .lgx, install it, start a logoscore daemon, load and introspect the module, fetch a trusted root, configure with TWO fields, bootstrap the light client against live Sepolia, make verified calls, and shut down. It hits the real network on purpose. A verified proxy that cannot reach a beacon node and prove its way to the chain head is not doing the one thing it exists for, and mocking that away would assert nothing worth asserting. The price is that an endpoint outage turns the job red for reasons outside this repo — hence a separate workflow from CI, so CI stays meaningful on its own. Two steps earn their place beyond "it ran": the config check proves the endpoint defaults were filled in from fields the tutorial never supplied, and the last call puts the verified head next to the provider's directly, so the couple of blocks of lag is something the reader SEES rather than something the prose claims. That gap is the whole point — the proxy reports the head it has proof for, not the one the provider asserts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
35fc0365ce
commit
c4728671f9
@@ -0,0 +1,94 @@
|
||||
name: verified-proxy Doc-Tests
|
||||
|
||||
# Runs the executable doc-test end-to-end via the shared doctest CLI:
|
||||
# - verified-proxy-runtime.test.yaml: packages and installs THIS commit as an
|
||||
# .lgx, starts a logoscore daemon, loads the module, configures the proxy
|
||||
# from a freshly fetched trusted root, bootstraps the light client against
|
||||
# LIVE Sepolia, makes verified calls, and shuts down.
|
||||
#
|
||||
# This spec talks to the real network on purpose. A verified proxy that cannot
|
||||
# reach a beacon node and prove its way to the chain head is not doing the one
|
||||
# thing it exists to do, and no amount of mocking would tell us it works. The
|
||||
# cost is that a Sepolia or endpoint outage turns this job red for reasons
|
||||
# outside the repo — which is why it is a separate workflow from CI, and why CI
|
||||
# stays green on its own.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master, main]
|
||||
push:
|
||||
branches: [master, main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: doctests-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
doctests:
|
||||
name: verified-proxy doc-tests (${{ matrix.os }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
# Generous: on a cold cache this builds libverifproxy, which is a source
|
||||
# build of nimbus's Nim toolchain, before the light client has even started.
|
||||
timeout-minutes: 150
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Setup Cachix
|
||||
uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: logos-co
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
# Resolve the commit under test, so the spec packages THIS commit rather
|
||||
# than the latest published flake. Fork PRs are the exception: their head
|
||||
# commit lives in the fork, so `github:logos-co/...#<sha>` could not be
|
||||
# fetched — blank the SHA there and run against master instead.
|
||||
- name: Resolve commit under test
|
||||
id: commit
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ] && \
|
||||
[ "${{ github.event.pull_request.head.repo.fork }}" = "true" ]; then
|
||||
echo "sha=" >> "$GITHUB_OUTPUT"
|
||||
echo "Fork PR detected — doc-test will run against latest master."
|
||||
else
|
||||
echo "sha=${{ github.event.pull_request.head.sha || github.sha }}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Run verified-proxy doc-tests
|
||||
run: |
|
||||
nix run github:logos-co/logos-doctest -- run \
|
||||
doctests/verified-proxy-runtime.test.yaml \
|
||||
--verbose \
|
||||
--continue-on-fail \
|
||||
--release-for logos-verified-proxy-module=${{ steps.commit.outputs.sha }} \
|
||||
--report "${{ runner.temp }}/verified-proxy-doctest-report.html"
|
||||
|
||||
- name: Stage report for upload
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p report-out
|
||||
if [ -f "${{ runner.temp }}/verified-proxy-doctest-report.html" ]; then
|
||||
cp "${{ runner.temp }}/verified-proxy-doctest-report.html" report-out/index.html
|
||||
else
|
||||
echo "<h1>No report produced</h1>" > report-out/index.html
|
||||
fi
|
||||
|
||||
- name: Upload execution report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: verified-proxy-doctest-report-${{ matrix.os }}
|
||||
path: report-out/index.html
|
||||
@@ -247,6 +247,24 @@ single `eth_blockNumber` in that run took **12.6 s**, against a 30 s default
|
||||
root taken from the same endpoint you are about to verify against anchors
|
||||
nothing. For anything holding real value, obtain the root independently.
|
||||
|
||||
## Executable tutorial
|
||||
|
||||
`doctests/verified-proxy-runtime.test.yaml` is a tutorial that is also a test.
|
||||
It packages this commit as an `.lgx`, installs it, starts a `logoscore` daemon,
|
||||
configures the proxy from a freshly fetched trusted root, bootstraps the light
|
||||
client against **live Sepolia**, makes verified calls, and compares one against
|
||||
the untrusted provider so the head lag is visible rather than asserted.
|
||||
|
||||
It talks to the real network deliberately. A verified proxy that cannot reach a
|
||||
beacon node and prove its way to the chain head is not doing the one thing it
|
||||
exists for, and mocking that away would prove nothing. The cost is that a
|
||||
network or endpoint outage turns the doc-test red for reasons outside this repo,
|
||||
which is why it is a separate workflow from CI.
|
||||
|
||||
```bash
|
||||
nix run github:logos-co/logos-doctest -- run doctests/verified-proxy-runtime.test.yaml --verbose
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
name: "Running a Light-Client-Verified Ethereum Proxy"
|
||||
output: verified-proxy-runtime.md
|
||||
release: ""
|
||||
|
||||
intro: |
|
||||
In this tutorial you will run a **trust-minimised** Ethereum RPC endpoint from
|
||||
source.
|
||||
|
||||
Ordinary RPC asks a provider for an answer and believes it. This module runs
|
||||
[nimbus's verified proxy](https://github.com/status-im/nimbus-eth1/tree/master/nimbus_verified_proxy)
|
||||
instead: it syncs the beacon-chain light client from a trusted block root and
|
||||
verifies every `eth_*` response against the attested execution state,
|
||||
requesting Merkle proofs from the provider it does not trust.
|
||||
|
||||
You will:
|
||||
|
||||
1. Build the Logos runtime (`logoscore`) and the local package manager (`lgpm`).
|
||||
2. Build **this** module as an installable `.lgx` package and install it.
|
||||
3. Start the daemon, load the module, and introspect its 60-method surface.
|
||||
4. Fetch a finalized block root, configure the proxy with **two fields**, and
|
||||
start it — watching the light client bootstrap against a live network.
|
||||
5. Make verified calls and compare one against the untrusted provider directly,
|
||||
to see the light client's lag for yourself.
|
||||
|
||||
This is an **executable tutorial**: it runs automatically on every module
|
||||
change, against the live Sepolia network.
|
||||
|
||||
what_you_build: "A running, light-client-verified Ethereum RPC proxy on Sepolia — configured, started, queried, and stopped through the `logoscore` CLI."
|
||||
|
||||
what_you_learn:
|
||||
- How to build the Logos runtime and the `lgpm` package manager from source
|
||||
- How a module's Nix flake exposes a ready-to-install `.lgx` via its `#lgx` output
|
||||
- How to start the `logoscore` daemon, load a module, and introspect its methods
|
||||
- Why a *trusted block root* is the anchor of the whole trust model, and how to get one
|
||||
- How per-network defaults make a two-field configuration sufficient
|
||||
- How to make verified `eth_*` calls, and why the verified head lags the provider's
|
||||
|
||||
prerequisites:
|
||||
- |
|
||||
**Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/nix
|
||||
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
|
||||
```
|
||||
|
||||
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
|
||||
- "**A Linux or macOS machine.**"
|
||||
- "**`jq`** on your `PATH` — used to pull values out of the JSON replies. Verify: `jq --version`"
|
||||
- "**Network access.** This tutorial syncs a real light client against Sepolia."
|
||||
|
||||
sections:
|
||||
- title: "Build the Logos daemon"
|
||||
step: true
|
||||
text: |
|
||||
Build the Logos runtime CLI from its published flake. `logoscore` is the
|
||||
headless frontend for [`logos-liblogos`](https://github.com/logos-co/logos-liblogos);
|
||||
it runs as a daemon and brings in the whole module-runtime stack.
|
||||
steps:
|
||||
- title: "Build the CLI"
|
||||
run: "nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos"
|
||||
code_block: |
|
||||
nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos
|
||||
check_file: "logos/bin/logoscore"
|
||||
post_text: |
|
||||
The build produces `logos/bin/logoscore` plus a `logos/modules/`
|
||||
directory containing the built-in `capability_module`, which the auth
|
||||
handshake needs when loading modules.
|
||||
|
||||
- title: "Build the lgpm package manager"
|
||||
step: true
|
||||
text: |
|
||||
`lgpm` installs `.lgx` packages into a modules directory and scans what is
|
||||
installed.
|
||||
steps:
|
||||
- title: "Build lgpm"
|
||||
run: "nix build 'github:logos-co/logos-package-manager#cli' -o lgpm"
|
||||
check_file: "lgpm/bin/lgpm"
|
||||
|
||||
- title: "Build and install this module"
|
||||
step: true
|
||||
text: |
|
||||
Build **this** module's `.lgx` straight from its flake's `#lgx` output.
|
||||
|
||||
Be warned that the first build is genuinely slow: the module links
|
||||
`libverifproxy`, which is a source build of nimbus's Nim toolchain. The
|
||||
Logos binary cache usually has it already.
|
||||
steps:
|
||||
- title: "Build the module's .lgx"
|
||||
run: "nix build 'github:logos-co/logos-verified-proxy-module{release}#lgx' -o proxy-lgx"
|
||||
code_block: |
|
||||
# From inside the clone this is simply: nix build '.#lgx'
|
||||
nix build 'github:logos-co/logos-verified-proxy-module{release}#lgx' -o proxy-lgx
|
||||
post_text: "The `.lgx` package is now under `./proxy-lgx/`:"
|
||||
extra_run:
|
||||
run: "ls proxy-lgx/*.lgx"
|
||||
|
||||
- title: "Install the .lgx with lgpm"
|
||||
text: |
|
||||
`verified_proxy_module` is a `core` module, so it goes to
|
||||
`--modules-dir`. The package is unsigned (a local dev build), so we
|
||||
pass `--allow-unsigned`.
|
||||
run: "./lgpm/bin/lgpm --modules-dir ./modules --allow-unsigned install --file proxy-lgx/*.lgx"
|
||||
expect_contains:
|
||||
- "Installed to:"
|
||||
|
||||
- title: "Confirm the install"
|
||||
run: "./lgpm/bin/lgpm --modules-dir ./modules list"
|
||||
expect_contains:
|
||||
- "verified_proxy_module"
|
||||
check_file: "modules/verified_proxy_module/manifest.json"
|
||||
|
||||
- title: "Start the daemon and load the module"
|
||||
step: true
|
||||
text: |
|
||||
Start `logoscore` in daemon mode pointed at `./modules`. Daemon output goes
|
||||
to `logs.txt` — worth watching, because the light client's own progress is
|
||||
logged there and nowhere else.
|
||||
steps:
|
||||
- title: "Start the daemon"
|
||||
run: "sh -c './logos/bin/logoscore -D -m ./modules > logs.txt 2>&1 &'"
|
||||
code_block: "logoscore -D -m ./modules > logs.txt &"
|
||||
|
||||
- run: "sleep 3"
|
||||
|
||||
- title: "Load the module"
|
||||
run: "./logos/bin/logoscore load-module verified_proxy_module"
|
||||
code_block: "logoscore load-module verified_proxy_module"
|
||||
expect_contains:
|
||||
- "verified_proxy_module"
|
||||
|
||||
- title: "Introspect the method surface"
|
||||
text: |
|
||||
The module mirrors the library's whole dispatch table: 30 `eth*`
|
||||
methods and 30 `op*` mirrors, plus configuration and lifecycle. They
|
||||
are generated from the library's own table rather than hand-written.
|
||||
run: "./logos/bin/logoscore module-info verified_proxy_module"
|
||||
code_block: "logoscore module-info verified_proxy_module"
|
||||
expect_contains:
|
||||
- "verified_proxy_module"
|
||||
- "ethBlockNumber"
|
||||
- "ethGetBalance"
|
||||
- "supportedNetworks"
|
||||
|
||||
- title: "Configure the proxy"
|
||||
step: true
|
||||
text: |
|
||||
The proxy needs to know which chain to verify, where to reach a beacon and
|
||||
an execution endpoint, and — critically — a **trusted block root**.
|
||||
steps:
|
||||
- title: "See which networks are supported"
|
||||
text: |
|
||||
Each entry carries its chain id and a default endpoint pair. Build a UI
|
||||
selector from this rather than hardcoding a list: an unrecognised
|
||||
`network` reaches a `quit()` inside the Nim library and would take the
|
||||
whole host process down.
|
||||
run: "./logos/bin/logoscore call verified_proxy_module supportedNetworks"
|
||||
code_block: "logoscore call verified_proxy_module supportedNetworks"
|
||||
expect_contains:
|
||||
- "sepolia"
|
||||
- "mainnet"
|
||||
- "hoodi"
|
||||
|
||||
- title: "Look at a default configuration"
|
||||
text: |
|
||||
`defaultConfig` returns a complete, ready-to-edit template for a
|
||||
network. Note that `trustedBlockRoot` is deliberately **empty** — it
|
||||
anchors the entire trust model, so it is the one field that cannot be
|
||||
defaulted.
|
||||
run: "./logos/bin/logoscore call verified_proxy_module defaultConfig sepolia"
|
||||
code_block: "logoscore call verified_proxy_module defaultConfig sepolia"
|
||||
expect_contains:
|
||||
- "lodestar-sepolia"
|
||||
- "trustedBlockRoot"
|
||||
|
||||
- title: "Fetch a finalized block root"
|
||||
text: |
|
||||
The root is where trust enters the system. Here we ask the module to
|
||||
fetch the current finalized root from a beacon node, which is fine for
|
||||
getting started — but understand what it is not: a root taken from the
|
||||
same endpoint you are about to verify against proves nothing. For
|
||||
anything holding real value, obtain the root from a source you
|
||||
independently trust and paste it in.
|
||||
run: "./logos/bin/logoscore call verified_proxy_module fetchFinalizedRoot https://lodestar-sepolia.chainsafe.io"
|
||||
code_block: |
|
||||
logoscore call verified_proxy_module fetchFinalizedRoot \
|
||||
https://lodestar-sepolia.chainsafe.io
|
||||
expect_contains:
|
||||
- '"success":true'
|
||||
- "root"
|
||||
|
||||
- title: "Configure with two fields"
|
||||
text: |
|
||||
Everything else comes from the network's defaults, so a working
|
||||
configuration is just the chain and the root:
|
||||
run: |
|
||||
ROOT=$(./logos/bin/logoscore call verified_proxy_module fetchFinalizedRoot https://lodestar-sepolia.chainsafe.io | jq -r '.result.value.root')
|
||||
echo "trusted root: $ROOT"
|
||||
./logos/bin/logoscore call verified_proxy_module configure "json:{\"network\":\"sepolia\",\"trustedBlockRoot\":\"$ROOT\"}"
|
||||
code_block: |
|
||||
ROOT=$(logoscore call verified_proxy_module fetchFinalizedRoot \
|
||||
https://lodestar-sepolia.chainsafe.io | jq -r '.result.value.root')
|
||||
logoscore call verified_proxy_module configure \
|
||||
"json:{\"network\":\"sepolia\",\"trustedBlockRoot\":\"$ROOT\"}"
|
||||
expect_contains:
|
||||
- '"success":true'
|
||||
|
||||
- title: "Confirm the defaults were filled in"
|
||||
text: |
|
||||
The endpoints we never supplied are now set, and the configuration has
|
||||
been persisted — it will be reloaded automatically next time the module
|
||||
starts.
|
||||
run: "./logos/bin/logoscore call verified_proxy_module getConfig"
|
||||
code_block: "logoscore call verified_proxy_module getConfig"
|
||||
expect_contains:
|
||||
- "lodestar-sepolia.chainsafe.io"
|
||||
- "ethereum-sepolia-rpc.publicnode.com"
|
||||
|
||||
- title: "Start the light client and make verified calls"
|
||||
step: true
|
||||
text: |
|
||||
`start` blocks until the light client has bootstrapped from the trusted
|
||||
root, which takes a little while — it is verifying its way to the chain
|
||||
head, not just opening a socket.
|
||||
steps:
|
||||
- title: "Start the proxy"
|
||||
run: "./logos/bin/logoscore call verified_proxy_module start"
|
||||
code_block: "logoscore call verified_proxy_module start"
|
||||
expect_contains:
|
||||
- '"success":true'
|
||||
- "chainId"
|
||||
|
||||
- title: "Check the state"
|
||||
text: |
|
||||
`running`, with the chain id the light client actually verified against
|
||||
— not merely the one we asked for.
|
||||
run: "./logos/bin/logoscore call verified_proxy_module status"
|
||||
code_block: "logoscore call verified_proxy_module status"
|
||||
expect_contains:
|
||||
- '"state":"running"'
|
||||
- "11155111"
|
||||
|
||||
- title: "A verified block number"
|
||||
run: "./logos/bin/logoscore call verified_proxy_module ethBlockNumber"
|
||||
code_block: "logoscore call verified_proxy_module ethBlockNumber"
|
||||
expect_contains:
|
||||
- '"success":true'
|
||||
|
||||
- title: "Compare against the untrusted provider"
|
||||
text: |
|
||||
Ask the execution endpoint directly and put the two side by side. The
|
||||
verified head is normally a couple of blocks **behind**, and that gap
|
||||
is the point: the proxy reports the head it has proof for, not the one
|
||||
the provider asserts.
|
||||
run: |
|
||||
VERIFIED=$(./logos/bin/logoscore call verified_proxy_module ethBlockNumber | jq -r '.result.value')
|
||||
PROVIDER=$(curl -s -X POST -H 'Content-Type: application/json' \
|
||||
--data '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' \
|
||||
https://ethereum-sepolia-rpc.publicnode.com | jq -r '.result')
|
||||
echo "verified: $VERIFIED"
|
||||
echo "provider: $((PROVIDER)) ($PROVIDER)"
|
||||
code_block: |
|
||||
logoscore call verified_proxy_module ethBlockNumber | jq -r '.result.value'
|
||||
curl -s -X POST -H 'Content-Type: application/json' \
|
||||
--data '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' \
|
||||
https://ethereum-sepolia-rpc.publicnode.com | jq -r '.result'
|
||||
expect_contains:
|
||||
- "verified:"
|
||||
- "provider:"
|
||||
|
||||
- title: "Some other verified calls"
|
||||
text: "All of these are proved against the attested state, not taken on trust:"
|
||||
run: |
|
||||
./logos/bin/logoscore call verified_proxy_module ethChainId
|
||||
./logos/bin/logoscore call verified_proxy_module ethGasPrice
|
||||
./logos/bin/logoscore call verified_proxy_module ethGetBlockTransactionCountByNumber latest
|
||||
code_block: |
|
||||
logoscore call verified_proxy_module ethChainId
|
||||
logoscore call verified_proxy_module ethGasPrice
|
||||
logoscore call verified_proxy_module ethGetBlockTransactionCountByNumber latest
|
||||
expect_contains:
|
||||
- '"success":true'
|
||||
|
||||
- title: "Shut down"
|
||||
step: true
|
||||
steps:
|
||||
- title: "Stop the proxy"
|
||||
text: |
|
||||
`stop` drains anything in flight before releasing the library's
|
||||
context, so it is not instantaneous.
|
||||
run: "./logos/bin/logoscore call verified_proxy_module stop"
|
||||
code_block: "logoscore call verified_proxy_module stop"
|
||||
expect_contains:
|
||||
- '"success":true'
|
||||
|
||||
- title: "Stop the daemon"
|
||||
run: "./logos/bin/logoscore stop"
|
||||
code_block: "logoscore stop"
|
||||
|
||||
- run: "sleep 2"
|
||||
|
||||
- title: "Confirm the daemon has stopped"
|
||||
text: |
|
||||
With no daemon running the client reports `not_running` and exits
|
||||
non-zero, so we add `|| true` to let the doc-test assert on the output:
|
||||
run: "./logos/bin/logoscore status || true"
|
||||
code_block: "logoscore status"
|
||||
expect_contains:
|
||||
- '"status":"not_running"'
|
||||
Reference in New Issue
Block a user