mirror of
https://github.com/logos-co/logos-rln-e2e.git
synced 2026-08-27 13:01:13 +00:00
harness W1: lib primitives, target providers, register scenario
- lib/{json,lgx,daemon,wallet,chain}.sh: the proven helpers from the
modules-repo e2e script and the mix orchestrator, re-expressed over the
node_call seam (host daemons now, containers later). daemon env keeps the
env -i isolation; die tails the failing node's log.
- artifacts.sh: env override -> nix build from the flake pins; assembles
E2E_MODULES_DIR; pin-consistency gate (rln-modules' rln-layouts rev must
equal the locked lez-rln rev — decode skew otherwise reads as chain bugs).
- targets/local.sh: dev.sh lifecycle (process-group kill, RPC readiness,
900s first-boot budget) or E2E_DEVNET=external attach; fresh faucet
provisioning via the checkout's tools, verify.sh on every path, staged
fixture -> contract env, per-stage timings. targets/testnet.sh: committed
descriptor + liveness assert, no checkout needed (store-path stage.sh).
- scenarios/register: the full membership lifecycle (claim -> register ->
active -> merkle proof + registry cross-check -> generate/verify proof +
tampered-signal invalid), all budgets from the contract, registry id
logos:${E2E_TARGET}:<config-hex>. R2/R4 diagnostics kept.
- harness/selftest.sh: no-chain gate — artifacts, lgx install, daemon boot,
module load, JSON plumbing. PASSES on darwin-arm64.
- devShell + coreutils (timeout); contract.md gains the target-input table.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# Committed deployments
|
||||
|
||||
Each `deployments/<name>/` is a **testnet** deployment profile: an RLN tree
|
||||
already provisioned on a hosted sequencer, committed so a run spends against a
|
||||
known tree instead of re-provisioning one.
|
||||
|
||||
```
|
||||
deployments/<name>/
|
||||
deployment.json descriptor: name, tree_id, sequencer, registration_program_id,
|
||||
merkle_program_id, config_account, payment_account,
|
||||
supply_holding, funding (faucet|wallet-key)
|
||||
storage.json the wallet holding payment_account (and, on wallet-key
|
||||
deployments, supply_holding)
|
||||
```
|
||||
|
||||
`--target testnet` requires `E2E_DEPLOYMENT=<name>` — there is no default: a
|
||||
testnet run always names the tree it spends against. The target reads
|
||||
`.sequencer` from the descriptor, asserts it answers `getLastBlockId`, and
|
||||
stages the pair through logos-lez-rln's `tools/deployments/stage.sh` into the
|
||||
run's wallet home; every other contract value (tree id, config account,
|
||||
funding) comes out of that staged fixture.
|
||||
|
||||
The descriptor and its wallet are one unit: `stage.sh` fails when the wallet
|
||||
does not hold the descriptor's accounts. Copy both files or neither.
|
||||
|
||||
`--target local` never reads this directory — it provisions a fresh tree per
|
||||
run into `$E2E_RUN_DIR/deployments/local-e2e`.
|
||||
|
||||
## Adding one
|
||||
|
||||
Provision against the hosted sequencer from a logos-lez-rln checkout, then
|
||||
commit the pair:
|
||||
|
||||
```sh
|
||||
cd <logos-lez-rln>
|
||||
bash tools/deployments/provision.sh --name <name> \
|
||||
--sequencer https://testnet.lez.logos.co/ --funding faucet \
|
||||
--outdir <this-repo>/deployments
|
||||
```
|
||||
|
||||
`funding=faucet` deployments are the paid `Register` path (anyone claims
|
||||
tokens up to the deployment's cap). `wallet-key` deployments carry a
|
||||
pre-minted supply in the committed wallet — a scenario that needs the faucet
|
||||
must assert `E2E_FUNDING=faucet`.
|
||||
|
||||
Guest drift invalidates a descriptor: rebuilt guest binaries change the
|
||||
program id, so the same `tree_id` derives a different `config_account`. Run
|
||||
`bash <logos-lez-rln>/tools/deployments/verify.sh deployments/<name>` after a
|
||||
guest bump; a failure means re-provision, not a chain bug.
|
||||
|
||||
The committed wallet is a test wallet on a test chain — treat any key in here
|
||||
as public.
|
||||
@@ -37,6 +37,19 @@ channel.
|
||||
| `E2E_EPOCH_SIZE_SEC` | RLN epoch size passed to `start` (local 60, testnet 600) |
|
||||
| `E2E_ROOT_WINDOW_TIMEOUT_S` | `verify_proof` root-window retry budget (local 60, testnet 120) |
|
||||
|
||||
## Target inputs (caller → target)
|
||||
|
||||
| var | meaning |
|
||||
|---|---|
|
||||
| `E2E_DEVNET` | local only: `host` (default — run lez-rln's `dev.sh`) \| `external` (attach to a running sequencer) |
|
||||
| `E2E_DEVNET_TIMEOUT_S` | local/host: devnet readiness budget (default 900 — first boot cargo-builds the sequencer) |
|
||||
| `LEZ_RLN_CHECKOUT` | lez-rln working tree for dev.sh + provisioning (default `../logos-lez-rln`; must have host bins + guest blobs built) |
|
||||
| `E2E_DEPLOYMENT` | testnet only, required: name of a committed descriptor under `deployments/` |
|
||||
| `E2E_DEPLOYMENT_DIR` | local/external only: reuse an existing provisioned deployment (refused under `E2E_DEVNET=host` — dev.sh wipes the chain) |
|
||||
|
||||
Scenario-specific knobs (e.g. `register`'s `E2E_RATE_LIMIT`) are documented in
|
||||
the scenario's header, never invented in the harness.
|
||||
|
||||
## scenario.env
|
||||
|
||||
Each `scenarios/<id>/scenario.env` declares: `NODES` (daemon count),
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
# . "$(nix build .#pins --no-link --print-out-paths)"
|
||||
pins = pkgs.writeText "e2e-pins.env" ''
|
||||
E2E_LEZ_RLN_SRC=${lez-rln}
|
||||
E2E_RLN_MODULES_SRC=${rln-modules}
|
||||
'';
|
||||
}
|
||||
# The module bundles every scenario loads, re-exported from the
|
||||
@@ -79,6 +80,7 @@
|
||||
rsync
|
||||
gnutar
|
||||
shellcheck
|
||||
coreutils # timeout(1) for the harness call caps; absent on stock macOS
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
+92
-7
@@ -1,17 +1,102 @@
|
||||
# shellcheck shell=bash
|
||||
# harness/artifacts.sh — resolve every binary/bundle a scenario loads.
|
||||
#
|
||||
# Resolution order per artifact (W1-A implements):
|
||||
# Resolution order per artifact:
|
||||
# 1. explicit env override: LOGOSCORE, WALLET_LGX, LEZ_RLN_LGX, RLN_LGX
|
||||
# 2. nix build from the flake pins: .#logoscore, .#wallet-lgx,
|
||||
# 2. RLN_MODULES_CHECKOUT=<dir>: build the three bundles from that working
|
||||
# tree (filtered copy + --override-input rln-modules) — the dev loop for
|
||||
# changing a module and running a scenario against it.
|
||||
# 3. nix build from the flake pins: .#logoscore, .#wallet-lgx,
|
||||
# .#lez-rln-module-lgx, .#rln-module-lgx. Verified 2026-08-10: the module
|
||||
# bundles build from a clean rln-modules fetch — the module-builder
|
||||
# stages the sdk and regenerates the scaffold in-derivation. (The
|
||||
# checkout-side staging scripts are only for bare-cargo dev loops.)
|
||||
#
|
||||
# Also W1-A: the pin-consistency check — the rln-modules tree pins rln-layouts
|
||||
# to a logos-lez-rln rev in logos-lez-rln-module/rust-lib/Cargo.toml; if that
|
||||
# rev differs from the lez-rln source in use, chain-state decode skew shows up
|
||||
# as phantom chain bugs. Assert equality, fail loudly with both revs.
|
||||
# The pin-consistency check: the rln-modules tree pins rln-layouts to a
|
||||
# logos-lez-rln rev in logos-lez-rln-module/rust-lib/Cargo.toml; if that rev
|
||||
# differs from the lez-rln source in use, chain-state decode skew shows up as
|
||||
# phantom chain bugs. Assert equality, fail loudly with both revs.
|
||||
#
|
||||
# Exports beyond docs/contract.md:
|
||||
# E2E_LEZ_RLN_SRC pinned logos-lez-rln source (read-only /nix/store)
|
||||
# E2E_RLN_MODULES_SRC pinned logos-rln-modules source
|
||||
# LEZ_RLN_CHECKOUT passthrough override for the lez-rln SOURCE
|
||||
# E2E_LEZ_RLN the checkout when it exists, else the pinned store
|
||||
# path. tools/deployments/stage.sh runs fine from the
|
||||
# store path; provisioning needs a real checkout —
|
||||
# which of the two a target needs is the target's call.
|
||||
|
||||
resolve_artifacts() { die "artifacts.sh: not implemented yet (W1-A)"; }
|
||||
E2E_ROOT="${E2E_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/lib/lgx.sh"
|
||||
|
||||
_nix_out() {
|
||||
local out
|
||||
out=$(nix build "$E2E_ROOT#$1" --no-link --print-out-paths | tail -1)
|
||||
[ -n "$out" ] || die "nix build .#$1 produced no output path"
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
_bundle_lgx() {
|
||||
if [ -n "${RLN_MODULES_CHECKOUT:-}" ]; then
|
||||
module_lgx "$1" "$RLN_MODULES_CHECKOUT"
|
||||
else
|
||||
lgx_of "$(_nix_out "$1")"
|
||||
fi
|
||||
}
|
||||
|
||||
# rln-layouts is the on-chain wire: the module stack decodes chain state with
|
||||
# it, so its pinned lez-rln rev must be the rev whose programs are deployed.
|
||||
_check_pins() {
|
||||
local pins layouts_rev lock_rev
|
||||
pins=$(_nix_out pins)
|
||||
# shellcheck source=/dev/null
|
||||
. "$pins"
|
||||
export E2E_LEZ_RLN_SRC E2E_RLN_MODULES_SRC
|
||||
layouts_rev=$(grep -E '^rln-layouts' \
|
||||
"$E2E_RLN_MODULES_SRC/logos-lez-rln-module/rust-lib/Cargo.toml" \
|
||||
| grep -oE '[0-9a-f]{40}' | head -1)
|
||||
lock_rev=$(jq -r '.nodes["lez-rln"].locked.rev' "$E2E_ROOT/flake.lock")
|
||||
[ -n "$layouts_rev" ] || die "cannot read the rln-layouts rev from the rln-modules pin"
|
||||
[ -n "$lock_rev" ] || die "cannot read the lez-rln rev from flake.lock"
|
||||
[ "$layouts_rev" = "$lock_rev" ] || die "pin skew: rln-modules pins rln-layouts to lez-rln $layouts_rev, flake.lock pins lez-rln $lock_rev — the module stack would decode chain state with the wrong layouts. Bump one of the two."
|
||||
say "pins consistent: lez-rln ${lock_rev:0:12} (rln-layouts + flake.lock)"
|
||||
}
|
||||
|
||||
resolve_artifacts() {
|
||||
section "artifacts"
|
||||
[ -n "${E2E_RUN_DIR:-}" ] || die "resolve_artifacts: E2E_RUN_DIR unset (run.sh sets it)"
|
||||
local tool out
|
||||
for tool in nix jq python3 tar curl rsync; do
|
||||
command -v "$tool" >/dev/null || die "missing tool: $tool"
|
||||
done
|
||||
|
||||
_check_pins
|
||||
if [ -n "${LEZ_RLN_CHECKOUT:-}" ] && [ -d "$LEZ_RLN_CHECKOUT" ]; then
|
||||
E2E_LEZ_RLN="$LEZ_RLN_CHECKOUT"
|
||||
else
|
||||
E2E_LEZ_RLN="$E2E_LEZ_RLN_SRC"
|
||||
fi
|
||||
export E2E_LEZ_RLN LEZ_RLN_CHECKOUT="${LEZ_RLN_CHECKOUT:-}"
|
||||
say "lez-rln source: $E2E_LEZ_RLN"
|
||||
|
||||
if [ -z "${LOGOSCORE:-}" ]; then
|
||||
out=$(_nix_out logoscore)
|
||||
LOGOSCORE="$out/bin/logoscore"
|
||||
fi
|
||||
[ -x "$LOGOSCORE" ] || die "logoscore not executable: $LOGOSCORE"
|
||||
export LOGOSCORE
|
||||
|
||||
[ -n "${WALLET_LGX:-}" ] || WALLET_LGX=$(_bundle_lgx wallet-lgx)
|
||||
[ -n "${LEZ_RLN_LGX:-}" ] || LEZ_RLN_LGX=$(_bundle_lgx lez-rln-module-lgx)
|
||||
[ -n "${RLN_LGX:-}" ] || RLN_LGX=$(_bundle_lgx rln-module-lgx)
|
||||
export WALLET_LGX LEZ_RLN_LGX RLN_LGX
|
||||
say "bundles: $(basename "$WALLET_LGX"), $(basename "$LEZ_RLN_LGX"), $(basename "$RLN_LGX")"
|
||||
|
||||
E2E_MODULES_DIR="$E2E_RUN_DIR/modules"
|
||||
export E2E_MODULES_DIR
|
||||
mkdir -p "$E2E_MODULES_DIR"
|
||||
install_lgx "$WALLET_LGX"
|
||||
install_lgx "$LEZ_RLN_LGX"
|
||||
install_lgx "$RLN_LGX"
|
||||
say "modules dir: $E2E_MODULES_DIR ($(lgx_platform))"
|
||||
}
|
||||
|
||||
+95
-7
@@ -1,9 +1,97 @@
|
||||
# shellcheck shell=bash
|
||||
# harness/lib/chain.sh — chain-level helpers shared by scenarios.
|
||||
# W1-A ports chain_head, confirm_and_ready (the leaf-confirmation barrier,
|
||||
# including the optimistic-vs-actual-leaf race fix) and diagnose_reg (log-scan
|
||||
# auto-diagnosis with copy-pasteable fixes) from the mix orchestrator.
|
||||
# harness/lib/chain.sh — chain-level helpers shared by scenarios, over the
|
||||
# node_call/node_logs seam (host daemon now, container later).
|
||||
#
|
||||
# Env beyond docs/contract.md:
|
||||
# E2E_REGISTRY_MOD registry-provider module id (default
|
||||
# liblogos_lez_rln_module)
|
||||
# E2E_ACTUAL_LEAF set by confirm_and_ready to the on-chain leaf index
|
||||
|
||||
chain_head() { die "chain.sh: not implemented yet (W1-A)"; }
|
||||
confirm_and_ready() { die "chain.sh: not implemented yet (W1-A)"; }
|
||||
diagnose_reg() { die "chain.sh: not implemented yet (W1-A)"; }
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/daemon.sh"
|
||||
|
||||
E2E_REGISTRY_MOD="${E2E_REGISTRY_MOD:-liblogos_lez_rln_module}"
|
||||
|
||||
# Usage: chain_head [endpoint] (default $E2E_SEQUENCER)
|
||||
chain_head() {
|
||||
local ep="${1:-${E2E_SEQUENCER:-}}" head
|
||||
[ -n "$ep" ] || return 1
|
||||
head=$(curl -sS -m 15 -X POST -H 'Content-Type: application/json' \
|
||||
--data '{"jsonrpc":"2.0","method":"getLastBlockId","params":[],"id":1}' "$ep" \
|
||||
| python3 -c 'import json,sys
|
||||
try:
|
||||
print(json.load(sys.stdin)["result"])
|
||||
except Exception:
|
||||
pass')
|
||||
case "$head" in ''|*[!0-9]*) return 1 ;; esac
|
||||
printf '%s' "$head"
|
||||
}
|
||||
|
||||
# On-chain confirmation barrier for a membership: wait until the registry
|
||||
# provider reports registered:true for our commitment on the CANONICAL tree
|
||||
# BEFORE the caller proceeds, so a following registration lands on a DISTINCT
|
||||
# leaf (a merkle proof alone was unreliable: it is served for the OPTIMISTIC
|
||||
# leaf before the tree advances, so leaves collided). Reads the ACTUAL leaf
|
||||
# into E2E_ACTUAL_LEAF and flags any mismatch with the optimistic one.
|
||||
# Usage: confirm_and_ready <node> <id_commitment_hex> [optimistic_leaf] [label]
|
||||
confirm_and_ready() {
|
||||
local node="$1" idc="$2" lopt="${3:-}"
|
||||
local label="${4:-$node}"
|
||||
local iv="${E2E_POLL_INTERVAL_S:-10}" res conf="" flag="" _w tries
|
||||
[ -n "${E2E_CONFIG_ACCOUNT:-}" ] || die "confirm_and_ready: E2E_CONFIG_ACCOUNT unset (the target sets it)"
|
||||
tries=$(( ${E2E_CONFIRM_TIMEOUT_S:-600} / iv ))
|
||||
[ "$tries" -lt 1 ] && tries=1
|
||||
E2E_ACTUAL_LEAF=""
|
||||
for _w in $(seq 1 "$tries"); do
|
||||
res=$(node_call "$node" "$E2E_REGISTRY_MOD" get_membership \
|
||||
"$(argfile confirm_cfg "$E2E_CONFIG_ACCOUNT")" "$(argfile confirm_idc "$idc")" | jres)
|
||||
case "$res" in
|
||||
*'"registered":true'*)
|
||||
conf=true
|
||||
E2E_ACTUAL_LEAF=$(printf '%s' "$res" | jfield leaf_index)
|
||||
break ;;
|
||||
esac
|
||||
sleep "$iv"
|
||||
done
|
||||
if [ "$conf" != "true" ]; then
|
||||
diagnose_reg "$node"
|
||||
return 1
|
||||
fi
|
||||
[ -n "$lopt" ] && [ "$lopt" != "$E2E_ACTUAL_LEAF" ] \
|
||||
&& flag=" !! LEAF MISMATCH (proof for $lopt, actual $E2E_ACTUAL_LEAF)"
|
||||
say "$label confirmed leaf_opt=${lopt:-n/a} leaf_actual=$E2E_ACTUAL_LEAF$flag"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Diagnose a registration that never confirmed by scanning the node's log for
|
||||
# the rln program's assert strings, then print the remediation.
|
||||
# Usage: diagnose_reg <node>
|
||||
diagnose_reg() {
|
||||
local node="$1" logs
|
||||
logs=$(node_logs "$node" "${E2E_DIAG_LINES:-2000}" 2>/dev/null)
|
||||
printf '%s\n' " !! RLN registration on node '$node' did not confirm on-chain." >&2
|
||||
if printf '%s' "$logs" | grep -qiE "Insufficient balance|may be out of funds|range end index 49"; then
|
||||
cat >&2 <<EOF
|
||||
CAUSE: the run's funding account ran out of RLNTOK mid-run (each
|
||||
registration costs price_per_unit x rate_limit, read live from
|
||||
get_registry_bounds).
|
||||
FIX: claim a bigger budget — raise the scenario's rate limit budget
|
||||
(E2E_RATE_LIMIT / the scenario's claim multiplier) and re-run; nothing
|
||||
needs re-provisioning:
|
||||
E2E_RATE_LIMIT=<smaller rate> ./run.sh ${E2E_SCENARIO:-<scenario>} --target ${E2E_TARGET:-local}
|
||||
EOF
|
||||
elif printf '%s' "$logs" | grep -qiE "Would exceed max total rate limit|max_total_rate_limit"; then
|
||||
cat >&2 <<EOF
|
||||
CAUSE: the RLN rate-limit pool is exhausted — this tree is effectively full.
|
||||
FIX: run against a fresh tree. --target local provisions one per run:
|
||||
./run.sh ${E2E_SCENARIO:-<scenario>} --target local
|
||||
On testnet, provision a new deployment from the lez-rln checkout
|
||||
(tools/deployments/provision.sh, tree_id is the single knob) and commit its
|
||||
descriptor under deployments/.
|
||||
EOF
|
||||
else
|
||||
cat >&2 <<EOF
|
||||
CAUSE: unknown. Inspect the node log:
|
||||
grep -iE 'register|balance|rate limit|payment|tree' $(node_log_path "$node")
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
+137
-8
@@ -1,11 +1,140 @@
|
||||
# shellcheck shell=bash
|
||||
# harness/lib/daemon.sh — logoscore daemon lifecycle + the node_call seam.
|
||||
# W1-A ports daemon boot/poll/load-module from the modules-repo e2e script.
|
||||
# node_call has the same signature whether the node is a host process (now) or
|
||||
# a container (compose topology, P3) — scenarios never know the difference.
|
||||
#
|
||||
# node_call/node_logs have the same signature whether the node is a host
|
||||
# process (now) or a container (compose topology, P3) — scenarios never know
|
||||
# the difference. A node id is a name; its state lives in
|
||||
# $E2E_RUN_DIR/nodes/<node>/{config,daemon.log}, so NODES>1 is a matter of
|
||||
# calling daemon_start twice.
|
||||
#
|
||||
# Sourcing this file REPLACES compat.sh's die with one that first prints the
|
||||
# tail of $E2E_DIE_NODE's daemon log (set by daemon_start to the first node);
|
||||
# die_node picks a different node for one call.
|
||||
#
|
||||
# Env beyond docs/contract.md:
|
||||
# E2E_DAEMON_ENV extra KEY=VALUE pairs (space-separated) for every daemon
|
||||
# E2E_DIE_NODE node whose log tail die prints
|
||||
# E2E_NODES node ids started so far (daemon_stop_all's list)
|
||||
|
||||
daemon_start() { die "daemon.sh: not implemented yet (W1-A)"; }
|
||||
daemon_load_modules() { die "daemon.sh: not implemented yet (W1-A)"; }
|
||||
daemon_stop() { die "daemon.sh: not implemented yet (W1-A)"; }
|
||||
node_call() { die "daemon.sh: not implemented yet (W1-A)"; } # <node> <module> <method> [args…]
|
||||
node_logs() { die "daemon.sh: not implemented yet (W1-A)"; }
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/json.sh"
|
||||
|
||||
E2E_NODES="${E2E_NODES:-}"
|
||||
|
||||
node_cfg_dir() { gv NODECFG "$1"; }
|
||||
node_log_path() { gv NODELOG "$1"; }
|
||||
|
||||
die() {
|
||||
printf '%s\n' "e2e: FAIL: $*" >&2
|
||||
local log
|
||||
log=$(node_log_path "${E2E_DIE_NODE:-}")
|
||||
if [ -n "$log" ] && [ -f "$log" ]; then
|
||||
printf '%s\n' "---- daemon log tail (${E2E_DIE_NODE}) ----" >&2
|
||||
tail -40 "$log" >&2 || true
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
die_node() { local node="$1"; shift; E2E_DIE_NODE="$node"; die "$@"; }
|
||||
|
||||
# Usage: daemon_start <node>
|
||||
daemon_start() {
|
||||
local node="${1:?daemon_start <node>}" dir cfg log pid _t
|
||||
[ -n "${LOGOSCORE:-}" ] || die "daemon_start: LOGOSCORE unset (resolve_artifacts first)"
|
||||
[ -n "${E2E_MODULES_DIR:-}" ] || die "daemon_start: E2E_MODULES_DIR unset (resolve_artifacts first)"
|
||||
dir="${E2E_RUN_DIR:?daemon_start: E2E_RUN_DIR unset}/nodes/$node"
|
||||
cfg="$dir/config"
|
||||
log="$dir/daemon.log"
|
||||
mkdir -p "$cfg"
|
||||
sv NODECFG "$node" "$cfg"
|
||||
sv NODELOG "$node" "$log"
|
||||
E2E_NODES="$E2E_NODES $node"
|
||||
[ -n "${E2E_DIE_NODE:-}" ] || E2E_DIE_NODE="$node"
|
||||
|
||||
# env -i: Qt strips DYLD_* otherwise, and daemon+client must agree on the
|
||||
# effective TMPDIR (QLocalSocket path). LEZ_RLN_TREE_ID_HEX must survive
|
||||
# into the daemon: rln_core derives PDAs from it.
|
||||
local -a envv
|
||||
envv=(HOME="$HOME" PATH="$PATH" LOGOSCORE_CONFIG_DIR="$cfg"
|
||||
RUST_BACKTRACE=full QT_QPA_PLATFORM=offscreen)
|
||||
if [ -n "${E2E_WALLET_HOME:-}" ]; then
|
||||
envv=("${envv[@]}" "NSSA_WALLET_HOME_DIR=$E2E_WALLET_HOME" "LEE_WALLET_HOME_DIR=$E2E_WALLET_HOME")
|
||||
fi
|
||||
if [ -n "${E2E_TREE_ID:-}" ]; then
|
||||
envv=("${envv[@]}" "LEZ_RLN_TREE_ID_HEX=$E2E_TREE_ID")
|
||||
fi
|
||||
local kv
|
||||
for kv in ${E2E_DAEMON_ENV:-}; do envv=("${envv[@]}" "$kv"); done
|
||||
|
||||
say "$node: starting logoscore daemon"
|
||||
# exec: $! must be the daemon itself so daemon_stop can kill just this node.
|
||||
(cd "$dir" && exec env -i "${envv[@]}" "$LOGOSCORE" -m "$E2E_MODULES_DIR" -D </dev/null >>"$log" 2>&1) &
|
||||
pid=$!
|
||||
sv NODEPID "$node" "$pid"
|
||||
disown "$pid" 2>/dev/null || true
|
||||
|
||||
for _t in $(seq 1 60); do
|
||||
[ -f "$cfg/client/config.json" ] && break
|
||||
sleep 1
|
||||
done
|
||||
[ -f "$cfg/client/config.json" ] || die_node "$node" "daemon produced no client config"
|
||||
for _t in $(seq 1 60); do
|
||||
_with_timeout 5 env -u TMPDIR LOGOSCORE_CONFIG_DIR="$cfg" "$LOGOSCORE" --quiet --json list-modules 2>/dev/null \
|
||||
| grep -q '"capability_module".*"loaded"' && break
|
||||
sleep 1
|
||||
done
|
||||
sleep 5
|
||||
}
|
||||
|
||||
# Usage: daemon_load_modules <node> <module>…
|
||||
daemon_load_modules() {
|
||||
local node="${1:?daemon_load_modules <node> <module>…}"; shift
|
||||
[ $# -gt 0 ] || die "daemon_load_modules: no modules given"
|
||||
local cfg log mod
|
||||
cfg=$(node_cfg_dir "$node")
|
||||
log=$(node_log_path "$node")
|
||||
[ -n "$cfg" ] || die "daemon_load_modules: unknown node '$node'"
|
||||
for mod in "$@"; do
|
||||
say "$node: load-module $mod"
|
||||
_with_timeout 30 env -u TMPDIR LOGOSCORE_CONFIG_DIR="$cfg" "$LOGOSCORE" --json load-module "$mod" \
|
||||
>>"$log" 2>&1 || die_node "$node" "load-module $mod failed"
|
||||
done
|
||||
}
|
||||
|
||||
# Usage: node_call <node> <module> <method> [args…]
|
||||
node_call() {
|
||||
local node="$1"; shift
|
||||
local cfg
|
||||
cfg=$(node_cfg_dir "$node")
|
||||
[ -n "$cfg" ] || die "node_call: unknown node '$node'"
|
||||
export E2E_CFG_DIR="$cfg"
|
||||
call_json "$@"
|
||||
}
|
||||
|
||||
# Usage: node_logs <node> [lines] (whole log when lines is omitted)
|
||||
node_logs() {
|
||||
local node="$1" lines="${2:-}" log
|
||||
log=$(node_log_path "$node")
|
||||
[ -n "$log" ] && [ -f "$log" ] || return 1
|
||||
if [ -n "$lines" ]; then tail -n "$lines" "$log"; else cat "$log"; fi
|
||||
}
|
||||
|
||||
daemon_stop() {
|
||||
local node="$1" pid
|
||||
pid=$(gv NODEPID "$node")
|
||||
[ -n "$pid" ] || return 0
|
||||
kill "$pid" 2>/dev/null || true
|
||||
sv NODEPID "$node" ""
|
||||
}
|
||||
|
||||
daemon_stop_all() {
|
||||
local node
|
||||
if [ "${E2E_KEEP:-0}" = "1" ]; then
|
||||
say "E2E_KEEP=1: leaving daemons up, state in ${E2E_RUN_DIR:-<none>}"
|
||||
return
|
||||
fi
|
||||
for node in $E2E_NODES; do daemon_stop "$node"; done
|
||||
# The run's modules dir is unique per run, so this cannot reach another
|
||||
# run's daemons.
|
||||
[ -n "${E2E_MODULES_DIR:-}" ] && pkill -f "logoscore -m $E2E_MODULES_DIR" 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
+112
-8
@@ -1,11 +1,115 @@
|
||||
# shellcheck shell=bash
|
||||
# harness/lib/json.sh — JSON plumbing for logoscore --json calls.
|
||||
# W1-A ports these from logos-rln-modules/logos-rln-module/tests/
|
||||
# e2e_register_testnet.sh (argfile / call_json / jres / jfield / jval / to_hex).
|
||||
#
|
||||
# call_json drives the daemon whose LOGOSCORE_CONFIG_DIR is $E2E_CFG_DIR;
|
||||
# daemon.sh's node_call sets that per node, so a scenario that talks to one
|
||||
# node can call call_json directly.
|
||||
#
|
||||
# Env beyond docs/contract.md:
|
||||
# LOGOSCORE the CLI (harness/artifacts.sh exports it)
|
||||
# E2E_CFG_DIR config dir of the daemon a bare call_json talks to
|
||||
# E2E_ARGS_DIR where argfile materialises @file args (default
|
||||
# $E2E_RUN_DIR/args)
|
||||
# CALL_TIMEOUT per-call timeout in seconds (default 180)
|
||||
|
||||
argfile() { die "json.sh: not implemented yet (W1-A)"; }
|
||||
call_json() { die "json.sh: not implemented yet (W1-A)"; }
|
||||
jres() { die "json.sh: not implemented yet (W1-A)"; }
|
||||
jfield() { die "json.sh: not implemented yet (W1-A)"; }
|
||||
jval() { die "json.sh: not implemented yet (W1-A)"; }
|
||||
to_hex() { die "json.sh: not implemented yet (W1-A)"; }
|
||||
# Digit-leading strings (base58 accounts, hex) must go via @file or the CLI
|
||||
# coerces them to numbers.
|
||||
argfile() {
|
||||
local dir="${E2E_ARGS_DIR:-${E2E_RUN_DIR:-.}/args}"
|
||||
mkdir -p "$dir" || die "argfile: cannot create $dir"
|
||||
printf '%s' "$2" > "$dir/$1.arg"
|
||||
printf '@%s' "$dir/$1.arg"
|
||||
}
|
||||
|
||||
# timeout(1) is coreutils; stock macOS has neither it nor gtimeout. A missing
|
||||
# timeout must not fail every call — run uncapped instead.
|
||||
_with_timeout() {
|
||||
local secs="$1"; shift
|
||||
if command -v timeout >/dev/null 2>&1; then timeout "$secs" "$@"
|
||||
elif command -v gtimeout >/dev/null 2>&1; then gtimeout "$secs" "$@"
|
||||
else "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
call_json() {
|
||||
local mod="$1" meth="$2"; shift 2
|
||||
[ -n "${LOGOSCORE:-}" ] || die "call_json: LOGOSCORE unset (resolve_artifacts first)"
|
||||
[ -n "${E2E_CFG_DIR:-}" ] || die "call_json: no daemon selected (use node_call)"
|
||||
# env -u TMPDIR: daemon and client must agree on the effective TMPDIR
|
||||
# (QLocalSocket path); the daemon runs under env -i, i.e. without one.
|
||||
_with_timeout "${CALL_TIMEOUT:-180}" env -u TMPDIR LOGOSCORE_CONFIG_DIR="$E2E_CFG_DIR" \
|
||||
"$LOGOSCORE" --json call "$mod" "$meth" "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
jres() {
|
||||
python3 -c '
|
||||
import json, sys
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line.startswith("{"):
|
||||
continue
|
||||
try:
|
||||
d = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if d.get("status") == "ok" and "result" in d:
|
||||
r = d["result"]
|
||||
# Compact separators: callers case-glob against the modules own
|
||||
# compact JSON, so re-serialized values must not add spaces.
|
||||
print(r if isinstance(r, str) else json.dumps(r, separators=(",", ":")))
|
||||
break
|
||||
'
|
||||
}
|
||||
|
||||
# The CLI envelope status ("ok"/"error"), empty when the call emitted no JSON.
|
||||
jstatus() {
|
||||
python3 -c '
|
||||
import json, sys
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line.startswith("{"):
|
||||
continue
|
||||
try:
|
||||
d = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if "status" in d:
|
||||
print(d["status"])
|
||||
break
|
||||
'
|
||||
}
|
||||
|
||||
jfield() { python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
print(json.load(sys.stdin).get('$1', ''))
|
||||
except Exception:
|
||||
print('')
|
||||
"; }
|
||||
|
||||
to_hex() { python3 -c 'import sys; print(sys.stdin.buffer.read().hex())'; }
|
||||
|
||||
# Unwrap a LogosResult envelope {success,value,error} (the -> result methods:
|
||||
# start/stop/generate_proof/verify_proof/get_epoch_quota/
|
||||
# get_registry_parameters) to its value on success or its error string on
|
||||
# failure; passes anything else through unchanged (tolerates a double-encoded
|
||||
# envelope, like the lp clients do).
|
||||
jval() { python3 -c '
|
||||
import json, sys
|
||||
raw = sys.stdin.read().strip()
|
||||
try:
|
||||
d = json.loads(raw)
|
||||
except Exception:
|
||||
print(raw); sys.exit()
|
||||
if isinstance(d, str):
|
||||
try:
|
||||
d = json.loads(d)
|
||||
except Exception:
|
||||
print(d); sys.exit()
|
||||
if isinstance(d, dict) and "success" in d and ("value" in d or "error" in d):
|
||||
out = d.get("value") if d.get("success") else d.get("error")
|
||||
# Compact separators — see jres.
|
||||
print(out if isinstance(out, str) else json.dumps(out, separators=(",", ":")))
|
||||
else:
|
||||
print(raw)
|
||||
'; }
|
||||
|
||||
+74
-5
@@ -1,8 +1,77 @@
|
||||
# shellcheck shell=bash
|
||||
# harness/lib/lgx.sh — build/unpack/install .lgx module bundles.
|
||||
# W1-A ports module_lgx + install_lgx from the modules-repo e2e script and the
|
||||
# flatten loop from harness/container/entrypoint.sh — one implementation for
|
||||
# host runs and image builds.
|
||||
#
|
||||
# One implementation for host runs and image builds: install_lgx flattens a
|
||||
# bundle into <dest>/<name>/{manifest.json,<variant files>,variant}, picking
|
||||
# this host's variant and falling back to the bundle's only variant (the
|
||||
# single-variant case the container entrypoint has).
|
||||
#
|
||||
# Env beyond docs/contract.md:
|
||||
# E2E_PLATFORM variant name override (default: derived from uname)
|
||||
# E2E_ROOT repo root holding flake.nix (default: two dirs up)
|
||||
|
||||
module_lgx() { die "lgx.sh: not implemented yet (W1-A)"; }
|
||||
install_lgx() { die "lgx.sh: not implemented yet (W1-A)"; }
|
||||
E2E_ROOT="${E2E_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||
|
||||
lgx_platform() {
|
||||
if [ -n "${E2E_PLATFORM:-}" ]; then printf '%s' "$E2E_PLATFORM"; return; fi
|
||||
case "$(uname -s)-$(uname -m)" in
|
||||
Darwin-arm64) printf 'darwin-arm64-dev' ;;
|
||||
Linux-x86_64) printf 'linux-x86_64-dev' ;;
|
||||
Linux-aarch64) printf 'linux-aarch64-dev' ;;
|
||||
*) die "unsupported platform $(uname -s)-$(uname -m)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
lgx_of() {
|
||||
local out; out=$(find "$1/" -maxdepth 1 -name '*.lgx' | head -1)
|
||||
[ -f "$out" ] || die "no .lgx under $1"
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
# Build a module bundle from a WORKING TREE instead of the flake pin: copy the
|
||||
# tree (filtered) and override the flake input with it. The copy carries the
|
||||
# tree verbatim — uncommitted changes and any gitignored staged sources —
|
||||
# minus target/result/.git: a raw `path:` override copies rust-lib/target
|
||||
# (~1 GB of local cargo artifacts) into /nix/store on every eval and fills the
|
||||
# disk. Nix content-addresses the copy, so unchanged trees rebuild for free.
|
||||
# Usage: module_lgx <flake-attr> <src-tree> [input-name]
|
||||
module_lgx() {
|
||||
local attr="$1" tree="$2" input="${3:-rln-modules}"
|
||||
[ -d "$tree" ] || die "module_lgx: no source tree at $tree"
|
||||
local src="${E2E_RUN_DIR:-.}/src-$input"
|
||||
say "$attr: building from a filtered copy of $tree" >&2
|
||||
rsync -a --delete --exclude '*/rust-lib/target' --exclude 'result' \
|
||||
--exclude 'result-*' --exclude '.git' "$tree/" "$src/" \
|
||||
|| die "rsync $tree failed"
|
||||
local out
|
||||
out=$(cd "$E2E_ROOT" && nix build --no-link --print-out-paths ".#$attr" \
|
||||
--override-input "$input" "path:$src") || die "nix build .#$attr failed"
|
||||
lgx_of "$out"
|
||||
}
|
||||
|
||||
# Usage: install_lgx <bundle.lgx> [dest-modules-dir]
|
||||
install_lgx() {
|
||||
local lgx="$1" dest="${2:-${E2E_MODULES_DIR:-}}" name tmp plat variant nvar
|
||||
[ -f "$lgx" ] || die "install_lgx: no bundle at $lgx"
|
||||
[ -n "$dest" ] || die "install_lgx: no destination (set E2E_MODULES_DIR)"
|
||||
name=$(tar xzOf "$lgx" manifest.json | python3 -c 'import json,sys; print(json.load(sys.stdin)["name"])')
|
||||
[ -n "$name" ] || die "install_lgx: cannot read name from $lgx"
|
||||
tmp=$(mktemp -d)
|
||||
tar xzf "$lgx" -C "$tmp" || die "install_lgx: cannot unpack $lgx"
|
||||
plat=$(lgx_platform)
|
||||
if [ -d "$tmp/variants/$plat" ]; then
|
||||
variant="$plat"
|
||||
else
|
||||
# Single-variant bundles (image builds) carry whatever the builder
|
||||
# produced; anything else is a genuine platform mismatch.
|
||||
nvar=$(find "$tmp/variants" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')
|
||||
[ "$nvar" = "1" ] || die "install_lgx: $lgx has no variants/$plat"
|
||||
variant=$(basename "$(find "$tmp/variants" -mindepth 1 -maxdepth 1 -type d | head -1)")
|
||||
fi
|
||||
rm -rf "${dest:?}/$name"
|
||||
mkdir -p "$dest/$name"
|
||||
cp "$tmp/manifest.json" "$dest/$name/"
|
||||
cp -L "$tmp/variants/$variant/"* "$dest/$name/"
|
||||
printf '%s' "$variant" > "$dest/$name/variant"
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
+88
-8
@@ -1,10 +1,90 @@
|
||||
# shellcheck shell=bash
|
||||
# harness/lib/wallet.sh — wallet open/sync/funding helpers.
|
||||
# W1-A ports open + sync_to_block + fresh-unused-holding from the modules-repo
|
||||
# e2e script, and sync_wallet (chunked) + wait_balance from the mix
|
||||
# orchestrator.
|
||||
# harness/lib/wallet.sh — wallet open/sync/funding helpers, over the node_call
|
||||
# seam (host daemon now, container later).
|
||||
#
|
||||
# Env beyond docs/contract.md:
|
||||
# E2E_WALLET_MOD wallet module id (default logos_execution_zone)
|
||||
# E2E_REGISTRY_MOD registry-provider module id (default
|
||||
# liblogos_lez_rln_module)
|
||||
# SYNC_STEP blocks per sync_to_block call (default 3000)
|
||||
|
||||
wallet_open() { die "wallet.sh: not implemented yet (W1-A)"; }
|
||||
wallet_sync() { die "wallet.sh: not implemented yet (W1-A)"; }
|
||||
wallet_fresh_holding() { die "wallet.sh: not implemented yet (W1-A)"; }
|
||||
wait_balance() { die "wallet.sh: not implemented yet (W1-A)"; }
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/daemon.sh"
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/chain.sh"
|
||||
|
||||
E2E_WALLET_MOD="${E2E_WALLET_MOD:-logos_execution_zone}"
|
||||
E2E_REGISTRY_MOD="${E2E_REGISTRY_MOD:-liblogos_lez_rln_module}"
|
||||
SYNC_STEP="${SYNC_STEP:-3000}"
|
||||
|
||||
# Usage: wallet_open <node> [wallet_home]
|
||||
# storage.json is the mutable wallet; the staged fixture ships it as
|
||||
# storage.json.seed so a re-run starts from the deployment's own accounts.
|
||||
wallet_open() {
|
||||
local node="$1" home="${2:-${E2E_WALLET_HOME:-}}"
|
||||
[ -n "$home" ] || die "wallet_open: no wallet home (the target sets E2E_WALLET_HOME)"
|
||||
[ -f "$home/wallet_config.json" ] || die "wallet_open: no wallet_config.json in $home"
|
||||
if [ ! -f "$home/storage.json" ]; then
|
||||
cp "$home/storage.json.seed" "$home/storage.json" \
|
||||
|| die "wallet_open: cannot seed $home/storage.json"
|
||||
fi
|
||||
node_call "$node" "$E2E_WALLET_MOD" open "$home/wallet_config.json" "$home/storage.json" >/dev/null \
|
||||
|| die_node "$node" "wallet open failed"
|
||||
}
|
||||
|
||||
# Sync to the chain head in SYNC_STEP chunks (a single jump over a long chain
|
||||
# times the sequencer poll out). Prints the block actually reached; stops early
|
||||
# when a chunk makes no progress.
|
||||
# Usage: wallet_sync <node>
|
||||
wallet_sync() {
|
||||
local node="$1" head cur tgt next
|
||||
head=$(chain_head) || die "wallet_sync: cannot probe chain head"
|
||||
cur=$(node_call "$node" "$E2E_WALLET_MOD" get_last_synced_block | jres | jval)
|
||||
case "$cur" in ''|*[!0-9]*) cur=0 ;; esac
|
||||
while [ "$cur" -lt "$head" ]; do
|
||||
tgt=$((cur + SYNC_STEP))
|
||||
[ "$tgt" -gt "$head" ] && tgt="$head"
|
||||
node_call "$node" "$E2E_WALLET_MOD" sync_to_block "$tgt" >/dev/null 2>&1
|
||||
next=$(node_call "$node" "$E2E_WALLET_MOD" get_last_synced_block | jres | jval)
|
||||
case "$next" in ''|*[!0-9]*) break ;; esac
|
||||
[ "$next" = "$cur" ] && break
|
||||
cur="$next"
|
||||
done
|
||||
printf '%s' "$cur"
|
||||
}
|
||||
|
||||
# create_account_public derives accounts DETERMINISTICALLY from the wallet's
|
||||
# key chain, so early derivations collide with accounts earlier runs already
|
||||
# created on-chain — walk the chain until an account with no on-chain token
|
||||
# data. Prints the account id; 1 when the walk is exhausted.
|
||||
# Usage: wallet_fresh_holding <node>
|
||||
wallet_fresh_holding() {
|
||||
local node="$1" acc bal_json _d
|
||||
for _d in $(seq 1 "${E2E_DERIVE_TRIES:-30}"); do
|
||||
acc=$(node_call "$node" "$E2E_WALLET_MOD" create_account_public | jres) || acc=""
|
||||
case "$acc" in ''|ERR|None) sleep 2; continue ;; esac
|
||||
bal_json=$(node_call "$node" "$E2E_REGISTRY_MOD" get_token_balance \
|
||||
"$(argfile fresh_holding "$acc")" | jres) || bal_json=""
|
||||
case "$bal_json" in
|
||||
*'"exists":false'*) printf '%s' "$acc"; return 0 ;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Poll until <account> holds at least <want> RLNTOK (credit lands async).
|
||||
# Prints the last seen balance; 1 when the budget runs out.
|
||||
# Usage: wait_balance <node> <account> <want>
|
||||
wait_balance() {
|
||||
local node="$1" acct="$2" want="$3" bal=0 _w tries
|
||||
local iv="${E2E_POLL_INTERVAL_S:-5}"
|
||||
tries=$(( ${E2E_CONFIRM_TIMEOUT_S:-180} / iv ))
|
||||
[ "$tries" -lt 1 ] && tries=1
|
||||
for _w in $(seq 1 "$tries"); do
|
||||
bal=$(node_call "$node" "$E2E_REGISTRY_MOD" get_token_balance \
|
||||
"$(argfile wait_balance "$acct")" | jres | jfield balance)
|
||||
case "$bal" in ''|*[!0-9]*) bal=0 ;; esac
|
||||
if [ "$bal" -ge "$want" ]; then printf '%s' "$bal"; return 0; fi
|
||||
sleep "$iv"
|
||||
done
|
||||
printf '%s' "$bal"
|
||||
return 1
|
||||
}
|
||||
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# harness/selftest.sh — the no-chain proof of the harness layers: resolve the
|
||||
# artifacts, boot one daemon with the three modules loaded, and call into both
|
||||
# RLN modules with NO sequencer anywhere. The assertion is that the call comes
|
||||
# back as well-formed JSON (a value OR a module error envelope) — a chain
|
||||
# failure is a PASS here; what is under test is artifacts -> lgx -> daemon ->
|
||||
# json, not the chain.
|
||||
#
|
||||
# Safe to run on a laptop with nothing else up: no chain, no docker, no ports.
|
||||
#
|
||||
# bash harness/selftest.sh E2E_KEEP=1 keeps the daemon + run dir
|
||||
set -uo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd "$HERE/.." && pwd)"
|
||||
. "$HERE/lib/compat.sh"
|
||||
|
||||
export E2E_KEEP="${E2E_KEEP:-0}"
|
||||
export CALL_TIMEOUT="${CALL_TIMEOUT:-60}"
|
||||
E2E_RUN_DIR="$ROOT/runs/$(date +%Y%m%d-%H%M%S)-selftest"
|
||||
export E2E_RUN_DIR
|
||||
mkdir -p "$E2E_RUN_DIR"
|
||||
say "run dir: $E2E_RUN_DIR"
|
||||
|
||||
. "$HERE/artifacts.sh"
|
||||
. "$HERE/lib/daemon.sh"
|
||||
|
||||
NODE=selftest
|
||||
cleanup() {
|
||||
daemon_stop_all
|
||||
[ "${E2E_KEEP:-0}" = "1" ] || rm -rf "$E2E_RUN_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
resolve_artifacts
|
||||
daemon_start "$NODE"
|
||||
daemon_load_modules "$NODE" logos_execution_zone liblogos_lez_rln_module liblogos_rln_module
|
||||
|
||||
section "no-chain probes"
|
||||
FAIL=0
|
||||
# A dummy scope: the registry id shape the membership module parses, and a
|
||||
# config account no deployment owns. Neither can succeed without a chain — the
|
||||
# point is that the failure arrives as JSON.
|
||||
DUMMY_CFG=11111111111111111111111111111111
|
||||
DUMMY_ID=$(printf '%064d' 0)
|
||||
REGISTRY_ID="logos:selftest:$(printf '%064d' 1)"
|
||||
|
||||
RAW=$(node_call "$NODE" liblogos_lez_rln_module get_registry_bounds "$(argfile st_cfg "$DUMMY_CFG")")
|
||||
STATUS=$(printf '%s' "$RAW" | jstatus)
|
||||
if [ "$STATUS" = "ok" ] || [ "$STATUS" = "error" ]; then
|
||||
say "liblogos_lez_rln_module.get_registry_bounds -> status=$STATUS result='$(printf '%s' "$RAW" | jres)'"
|
||||
else
|
||||
say "FAIL liblogos_lez_rln_module.get_registry_bounds returned no JSON: ${RAW:-<empty>}"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
RAW=$(node_call "$NODE" liblogos_rln_module get_registry_parameters \
|
||||
"$REGISTRY_ID" "$(argfile st_rlnid "$DUMMY_ID")")
|
||||
STATUS=$(printf '%s' "$RAW" | jstatus)
|
||||
RES=$(printf '%s' "$RAW" | jres)
|
||||
if [ "$STATUS" = "ok" ] && [ -n "$RES" ]; then
|
||||
# -> result: the envelope carries either the parameters or a typed error.
|
||||
say "liblogos_rln_module.get_registry_parameters -> $(printf '%s' "$RES" | jval)"
|
||||
else
|
||||
say "FAIL liblogos_rln_module.get_registry_parameters: status='${STATUS:-<none>}' raw=${RAW:-<empty>}"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
section "selftest"
|
||||
if [ "$FAIL" = "0" ]; then
|
||||
say "PASS — artifacts, lgx install, daemon boot, module load and JSON plumbing all work"
|
||||
else
|
||||
die_node "$NODE" "selftest: $FAIL probe(s) failed"
|
||||
fi
|
||||
+203
-18
@@ -1,22 +1,207 @@
|
||||
# shellcheck shell=bash
|
||||
# harness/targets/local.sh — the local-sequencer target (W1-B implements).
|
||||
# harness/targets/local.sh — the local-sequencer target.
|
||||
#
|
||||
# target_up:
|
||||
# E2E_DEVNET=host (default) run logos-lez-rln's root dev.sh (from
|
||||
# $LEZ_RLN_CHECKOUT, default ../logos-lez-rln) in
|
||||
# the background; it clones + runs the standalone
|
||||
# sequencer_service on port 3040 and wipes its
|
||||
# rocksdb each start.
|
||||
# E2E_DEVNET=external attach to an already-running sequencer
|
||||
# (inner dev loop; skips re-provisioning).
|
||||
# Readiness = JSON-RPC getLastBlockId >= 1, not a port probe.
|
||||
# Then provision a fresh deployment (lez-rln tools/deployments/provision.sh
|
||||
# --funding faucet) into $E2E_RUN_DIR and stage it (stage.sh) into
|
||||
# $E2E_WALLET_HOME; export the contract env (docs/contract.md).
|
||||
# Provisioning needs prebuilt run_setup/derive_accounts + the risc0 guest
|
||||
# blobs in the checkout — when missing, print the exact build recipe and die.
|
||||
# target_up boots (or attaches to) a sequencer on 127.0.0.1:3040, provisions a
|
||||
# fresh faucet-funded RLN deployment on it, stages that into a wallet home and
|
||||
# exports the contract env (docs/contract.md) with the local poll budgets.
|
||||
#
|
||||
# target_down: kill the sequencer we started unless E2E_KEEP=1.
|
||||
# Inputs beyond the contract:
|
||||
# E2E_DEVNET=host|external host (default) runs logos-lez-rln's root dev.sh
|
||||
# in the background: it clones the pinned
|
||||
# sequencer source, wipes its rocksdb and starts
|
||||
# sequencer_service on port 3040 (first boot
|
||||
# cargo-builds it). external attaches to a
|
||||
# sequencer already listening at E2E_SEQUENCER.
|
||||
# LEZ_RLN_CHECKOUT=<dir> logos-lez-rln checkout carrying dev.sh and
|
||||
# tools/deployments (default ../logos-lez-rln).
|
||||
# E2E_SEQUENCER=<url> endpoint (default http://127.0.0.1:3040/).
|
||||
# E2E_DEPLOYMENT_DIR=<dir> external mode only: reuse this deployment
|
||||
# instead of provisioning a fresh one.
|
||||
# E2E_DEVNET_TIMEOUT_S=900 readiness budget for the sequencer.
|
||||
# The contract's poll budgets honour a pre-set env; the defaults below are
|
||||
# the contract's local values.
|
||||
#
|
||||
# Readiness is JSON-RPC getLastBlockId >= 1, never a port probe: the listener
|
||||
# accepts connections before the chain produces its first block.
|
||||
#
|
||||
# target_down kills the dev.sh process tree unless E2E_KEEP=1.
|
||||
|
||||
target_up() { die "targets/local.sh: not implemented yet (W1-B)"; }
|
||||
target_down() { :; }
|
||||
_LOCAL_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
_LOCAL_DEVNET_PID=""
|
||||
|
||||
# Chain head, or non-zero when the endpoint does not answer.
|
||||
_local_chain_head() {
|
||||
local head
|
||||
head=$(curl -sS -m 10 -X POST -H 'Content-Type: application/json' \
|
||||
--data '{"jsonrpc":"2.0","method":"getLastBlockId","params":[],"id":1}' \
|
||||
"$1" 2>/dev/null | jq -re '.result // empty' 2>/dev/null) || return 1
|
||||
case "$head" in
|
||||
''|*[!0-9]*) return 1 ;;
|
||||
esac
|
||||
printf '%s' "$head"
|
||||
}
|
||||
|
||||
# _local_wait_chain <url> <budget_s> — block until the chain has a block.
|
||||
_local_wait_chain() {
|
||||
local url="$1" budget="$2" waited=0 head
|
||||
while [ "$waited" -lt "$budget" ]; do
|
||||
head=$(_local_chain_head "$url") || head=""
|
||||
if [ -n "$head" ] && [ "$head" -ge 1 ] 2>/dev/null; then
|
||||
say "sequencer ready at $url (block $head)"
|
||||
return 0
|
||||
fi
|
||||
sleep 5
|
||||
waited=$((waited + 5))
|
||||
if [ $((waited % 60)) -eq 0 ]; then
|
||||
say " waiting for $url (${waited}s of ${budget}s)"
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
_local_start_devnet() {
|
||||
local lez="$1" log="$E2E_RUN_DIR/devnet.log"
|
||||
[ -f "$lez/dev.sh" ] || die "no logos-lez-rln checkout at $lez (set LEZ_RLN_CHECKOUT)"
|
||||
command -v cargo >/dev/null || die "dev.sh needs cargo — install Rust (https://rustup.rs)"
|
||||
say "starting devnet: $lez/dev.sh (log: $log)"
|
||||
# Monitor mode puts the job in its own process group, so target_down can
|
||||
# signal cargo and the sequencer it spawns as one tree.
|
||||
set -m
|
||||
(cd "$lez" && exec bash ./dev.sh) </dev/null >>"$log" 2>&1 &
|
||||
_LOCAL_DEVNET_PID=$!
|
||||
set +m
|
||||
# Off the job table: the group id stays valid for target_down, and the
|
||||
# shell stops reporting the job's death on the harness's own stdout.
|
||||
disown "$_LOCAL_DEVNET_PID" 2>/dev/null || true
|
||||
}
|
||||
|
||||
_local_kill_devnet() {
|
||||
local pid="$1" waited=0 port stale p
|
||||
kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
|
||||
while [ "$waited" -lt 10 ]; do
|
||||
_local_chain_head "$E2E_SEQUENCER" >/dev/null || return 0
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
kill -KILL "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
|
||||
# The sequencer binary can outlive its cargo parent; dev.sh frees the port
|
||||
# the same way on its next start.
|
||||
port=$(printf '%s' "$E2E_SEQUENCER" | sed -n 's|.*://[^:/]*:\([0-9][0-9]*\).*|\1|p')
|
||||
stale=$(lsof -ti "tcp:${port:-3040}" 2>/dev/null || true)
|
||||
for p in $stale; do
|
||||
kill "$p" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
# Provisioning runs run_setup + derive_accounts out of the checkout and deploys
|
||||
# the guest blobs, so both must be built before a fresh local deployment.
|
||||
_local_require_build() {
|
||||
local lez="$1" missing=""
|
||||
[ -x "$lez/lez-rln/target/release/run_setup" ] || missing="$missing lez-rln/target/release/run_setup"
|
||||
[ -x "$lez/lez-rln/target/release/derive_accounts" ] || missing="$missing lez-rln/target/release/derive_accounts"
|
||||
ls "$lez"/lez-rln/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/*.bin >/dev/null 2>&1 \
|
||||
|| missing="$missing lez-rln/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/*.bin"
|
||||
[ -z "$missing" ] && return 0
|
||||
die "logos-lez-rln is not built for provisioning — missing:$missing
|
||||
|
||||
build it (order matters — the host build strips the deploy blobs):
|
||||
cd $lez/lez-rln
|
||||
cargo risczero build --manifest-path methods/guest/Cargo.toml
|
||||
PYO3_PYTHON=\$(command -v python3) cargo build --release --bin run_setup --bin derive_accounts"
|
||||
}
|
||||
|
||||
_local_provision() {
|
||||
local lez="$1" outroot="$2" log="$E2E_RUN_DIR/provision.log"
|
||||
_local_require_build "$lez"
|
||||
mkdir -p "$outroot"
|
||||
say "provisioning a fresh faucet deployment on $E2E_SEQUENCER (run_setup — several minutes; log: $log)"
|
||||
(cd "$lez" && bash tools/deployments/provision.sh \
|
||||
--name local-e2e --sequencer "$E2E_SEQUENCER" --outdir "$outroot" \
|
||||
--funding faucet) >>"$log" 2>&1 || {
|
||||
[ -f "$log" ] && tail -40 "$log" >&2
|
||||
die "provision.sh failed — see $log"
|
||||
}
|
||||
}
|
||||
|
||||
target_up() {
|
||||
section "target: local"
|
||||
for tool in curl jq python3; do
|
||||
command -v "$tool" >/dev/null || die "missing tool: $tool"
|
||||
done
|
||||
|
||||
local lez devnet t0 dep_dir
|
||||
lez="${LEZ_RLN_CHECKOUT:-$_LOCAL_HERE/../logos-lez-rln}"
|
||||
[ -d "$lez" ] && lez="$(cd "$lez" && pwd)"
|
||||
devnet="${E2E_DEVNET:-host}"
|
||||
E2E_SEQUENCER="${E2E_SEQUENCER:-http://127.0.0.1:3040/}"
|
||||
|
||||
t0=$(date +%s)
|
||||
case "$devnet" in
|
||||
host)
|
||||
_local_start_devnet "$lez"
|
||||
_local_wait_chain "$E2E_SEQUENCER" "${E2E_DEVNET_TIMEOUT_S:-900}" \
|
||||
|| die "devnet never produced a block within ${E2E_DEVNET_TIMEOUT_S:-900}s — see $E2E_RUN_DIR/devnet.log"
|
||||
;;
|
||||
external)
|
||||
_local_wait_chain "$E2E_SEQUENCER" 30 \
|
||||
|| die "E2E_DEVNET=external but no sequencer answers getLastBlockId at $E2E_SEQUENCER"
|
||||
;;
|
||||
*) die "E2E_DEVNET must be host|external, got '$devnet'" ;;
|
||||
esac
|
||||
say "timing: devnet-up $(( $(date +%s) - t0 ))s"
|
||||
|
||||
t0=$(date +%s)
|
||||
dep_dir="${E2E_DEPLOYMENT_DIR:-}"
|
||||
if [ "$devnet" = "external" ] && [ -n "$dep_dir" ]; then
|
||||
[ -f "$dep_dir/deployment.json" ] && [ -f "$dep_dir/storage.json" ] \
|
||||
|| die "E2E_DEPLOYMENT_DIR=$dep_dir has no deployment.json + storage.json"
|
||||
say "reusing deployment $dep_dir"
|
||||
else
|
||||
[ -z "$dep_dir" ] || die "E2E_DEPLOYMENT_DIR is only honoured with E2E_DEVNET=external (a fresh devnet knows no earlier tree)"
|
||||
_local_provision "$lez" "$E2E_RUN_DIR/deployments"
|
||||
dep_dir="$E2E_RUN_DIR/deployments/local-e2e"
|
||||
fi
|
||||
# The guest-drift guard: a rebuilt guest re-derives a different config for
|
||||
# the same tree_id, which otherwise surfaces as a chain bug.
|
||||
bash "$lez/tools/deployments/verify.sh" "$dep_dir" || die "verify.sh failed for $dep_dir"
|
||||
say "timing: provision $(( $(date +%s) - t0 ))s"
|
||||
|
||||
t0=$(date +%s)
|
||||
E2E_DEPLOYMENT_DIR="$dep_dir"
|
||||
E2E_WALLET_HOME="$E2E_RUN_DIR/wallet-home"
|
||||
bash "$lez/tools/deployments/stage.sh" "$dep_dir" "$E2E_WALLET_HOME" \
|
||||
|| die "stage.sh failed for $dep_dir"
|
||||
# stage.sh emits the wallet as a seed; the run mutates its own copy.
|
||||
cp "$E2E_WALLET_HOME/storage.json.seed" "$E2E_WALLET_HOME/storage.json" \
|
||||
|| die "no storage.json.seed in $E2E_WALLET_HOME"
|
||||
|
||||
E2E_TREE_ID=$(grep -oE 'LEZ_RLN_TREE_ID_HEX=[0-9a-f]{64}' "$E2E_WALLET_HOME/env.sh" | cut -d= -f2)
|
||||
E2E_CONFIG_ACCOUNT=$(tr -d '\n\r' < "$E2E_WALLET_HOME/config_account.txt")
|
||||
E2E_FUNDING=$(tr -d '\n\r' < "$E2E_WALLET_HOME/funding.txt")
|
||||
[ -n "$E2E_TREE_ID" ] && [ -n "$E2E_CONFIG_ACCOUNT" ] && [ -n "$E2E_FUNDING" ] \
|
||||
|| die "staged fixtures incomplete in $E2E_WALLET_HOME"
|
||||
say "timing: stage $(( $(date +%s) - t0 ))s"
|
||||
|
||||
E2E_CONFIRM_TIMEOUT_S="${E2E_CONFIRM_TIMEOUT_S:-120}"
|
||||
E2E_POLL_INTERVAL_S="${E2E_POLL_INTERVAL_S:-5}"
|
||||
E2E_EPOCH_SIZE_SEC="${E2E_EPOCH_SIZE_SEC:-60}"
|
||||
E2E_ROOT_WINDOW_TIMEOUT_S="${E2E_ROOT_WINDOW_TIMEOUT_S:-60}"
|
||||
export E2E_SEQUENCER E2E_DEPLOYMENT_DIR E2E_WALLET_HOME E2E_TREE_ID \
|
||||
E2E_CONFIG_ACCOUNT E2E_FUNDING E2E_CONFIRM_TIMEOUT_S E2E_POLL_INTERVAL_S \
|
||||
E2E_EPOCH_SIZE_SEC E2E_ROOT_WINDOW_TIMEOUT_S
|
||||
say "deployment: tree ${E2E_TREE_ID:0:8}… config $E2E_CONFIG_ACCOUNT funding $E2E_FUNDING"
|
||||
}
|
||||
|
||||
target_down() {
|
||||
if [ "${E2E_KEEP:-0}" = "1" ]; then
|
||||
[ -n "$_LOCAL_DEVNET_PID" ] \
|
||||
&& say "E2E_KEEP=1: devnet pid $_LOCAL_DEVNET_PID up at $E2E_SEQUENCER (log $E2E_RUN_DIR/devnet.log)"
|
||||
say "E2E_KEEP=1: deployment ${E2E_DEPLOYMENT_DIR:-<none>}, wallet home ${E2E_WALLET_HOME:-<none>}"
|
||||
say "E2E_KEEP=1: reattach with E2E_DEVNET=external E2E_DEPLOYMENT_DIR=${E2E_DEPLOYMENT_DIR:-<none>}"
|
||||
return 0
|
||||
fi
|
||||
[ -n "$_LOCAL_DEVNET_PID" ] || return 0
|
||||
say "stopping devnet (pid $_LOCAL_DEVNET_PID)"
|
||||
_local_kill_devnet "$_LOCAL_DEVNET_PID"
|
||||
_LOCAL_DEVNET_PID=""
|
||||
}
|
||||
|
||||
+116
-8
@@ -1,12 +1,120 @@
|
||||
# shellcheck shell=bash
|
||||
# harness/targets/testnet.sh — the hosted-testnet target (W1-B implements).
|
||||
# harness/targets/testnet.sh — the hosted-testnet target.
|
||||
#
|
||||
# target_up: no chain lifecycle — read a committed descriptor from
|
||||
# deployments/<name> (default via E2E_DEPLOYMENT), assert the sequencer
|
||||
# answers getLastBlockId, stage into $E2E_WALLET_HOME, export the contract
|
||||
# env with testnet poll budgets (docs/contract.md).
|
||||
# No chain lifecycle: the descriptor is committed under this repo's
|
||||
# deployments/<name> (see deployments/README.md). target_up asserts the
|
||||
# descriptor's sequencer answers getLastBlockId, stages the deployment into
|
||||
# E2E_WALLET_HOME and exports the contract env (docs/contract.md) with the
|
||||
# testnet poll budgets.
|
||||
#
|
||||
# target_down: nothing to tear down.
|
||||
# Inputs beyond the contract:
|
||||
# E2E_DEPLOYMENT=<name> descriptor dir under deployments/ — required, no
|
||||
# default: a testnet run always names the tree it
|
||||
# spends against.
|
||||
# LEZ_RLN_CHECKOUT=<dir> logos-lez-rln source providing
|
||||
# tools/deployments/stage.sh. Resolution: this var,
|
||||
# then E2E_LEZ_RLN_SRC (harness/artifacts.sh), then
|
||||
# the flake pin (nix build .#pins), then
|
||||
# ../logos-lez-rln.
|
||||
# The contract's poll budgets honour a pre-set env; the defaults below are
|
||||
# the contract's testnet values.
|
||||
#
|
||||
# Descriptors are staged, never verified here: verify.sh re-derives program ids
|
||||
# from built guest binaries, which a testnet run must not require.
|
||||
|
||||
target_up() { die "targets/testnet.sh: not implemented yet (W1-B)"; }
|
||||
target_down() { :; }
|
||||
_TESTNET_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
_testnet_chain_head() {
|
||||
local head
|
||||
head=$(curl -sS -m 15 -X POST -H 'Content-Type: application/json' \
|
||||
--data '{"jsonrpc":"2.0","method":"getLastBlockId","params":[],"id":1}' \
|
||||
"$1" 2>/dev/null | jq -re '.result // empty' 2>/dev/null) || return 1
|
||||
case "$head" in
|
||||
''|*[!0-9]*) return 1 ;;
|
||||
esac
|
||||
printf '%s' "$head"
|
||||
}
|
||||
|
||||
# The lez-rln source carrying tools/deployments/stage.sh.
|
||||
_testnet_lez_src() {
|
||||
local pins
|
||||
if [ -n "${LEZ_RLN_CHECKOUT:-}" ]; then
|
||||
printf '%s' "$LEZ_RLN_CHECKOUT"
|
||||
return 0
|
||||
fi
|
||||
if [ -n "${E2E_LEZ_RLN_SRC:-}" ]; then
|
||||
printf '%s' "$E2E_LEZ_RLN_SRC"
|
||||
return 0
|
||||
fi
|
||||
if pins=$(nix build "$_TESTNET_HERE#pins" --no-link --print-out-paths 2>/dev/null) \
|
||||
&& [ -f "$pins" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$pins"
|
||||
if [ -n "${E2E_LEZ_RLN_SRC:-}" ]; then
|
||||
printf '%s' "$E2E_LEZ_RLN_SRC"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
[ -d "$_TESTNET_HERE/../logos-lez-rln" ] || return 1
|
||||
(cd "$_TESTNET_HERE/../logos-lez-rln" && pwd)
|
||||
}
|
||||
|
||||
_testnet_deployments() {
|
||||
local d names=""
|
||||
for d in "$_TESTNET_HERE"/deployments/*/; do
|
||||
[ -f "$d/deployment.json" ] || continue
|
||||
names="$names $(basename "$d")"
|
||||
done
|
||||
printf '%s' "${names# }"
|
||||
}
|
||||
|
||||
target_up() {
|
||||
section "target: testnet"
|
||||
for tool in curl jq python3; do
|
||||
command -v "$tool" >/dev/null || die "missing tool: $tool"
|
||||
done
|
||||
|
||||
local name dep_dir lez head available
|
||||
available=$(_testnet_deployments)
|
||||
name="${E2E_DEPLOYMENT:-}"
|
||||
[ -n "$name" ] \
|
||||
|| die "--target testnet needs E2E_DEPLOYMENT=<name> (deployments/: ${available:-<none committed>})"
|
||||
dep_dir="$_TESTNET_HERE/deployments/$name"
|
||||
[ -f "$dep_dir/deployment.json" ] && [ -f "$dep_dir/storage.json" ] \
|
||||
|| die "no deployment 'deployments/$name' with deployment.json + storage.json (available: ${available:-<none committed>})"
|
||||
|
||||
lez=$(_testnet_lez_src) \
|
||||
|| die "no logos-lez-rln source for tools/deployments/stage.sh (set LEZ_RLN_CHECKOUT)"
|
||||
[ -f "$lez/tools/deployments/stage.sh" ] || die "no tools/deployments/stage.sh under $lez"
|
||||
|
||||
E2E_SEQUENCER=$(jq -re '.sequencer' "$dep_dir/deployment.json") \
|
||||
|| die "deployments/$name/deployment.json has no sequencer"
|
||||
head=$(_testnet_chain_head "$E2E_SEQUENCER") \
|
||||
|| die "sequencer $E2E_SEQUENCER does not answer getLastBlockId (deployment '$name')"
|
||||
say "sequencer $E2E_SEQUENCER at block $head"
|
||||
|
||||
E2E_DEPLOYMENT_DIR="$dep_dir"
|
||||
E2E_WALLET_HOME="$E2E_RUN_DIR/wallet-home"
|
||||
bash "$lez/tools/deployments/stage.sh" "$dep_dir" "$E2E_WALLET_HOME" \
|
||||
|| die "stage.sh failed for deployments/$name"
|
||||
# stage.sh emits the wallet as a seed; the run mutates its own copy.
|
||||
cp "$E2E_WALLET_HOME/storage.json.seed" "$E2E_WALLET_HOME/storage.json" \
|
||||
|| die "no storage.json.seed in $E2E_WALLET_HOME"
|
||||
|
||||
E2E_TREE_ID=$(grep -oE 'LEZ_RLN_TREE_ID_HEX=[0-9a-f]{64}' "$E2E_WALLET_HOME/env.sh" | cut -d= -f2)
|
||||
E2E_CONFIG_ACCOUNT=$(tr -d '\n\r' < "$E2E_WALLET_HOME/config_account.txt")
|
||||
E2E_FUNDING=$(tr -d '\n\r' < "$E2E_WALLET_HOME/funding.txt")
|
||||
[ -n "$E2E_TREE_ID" ] && [ -n "$E2E_CONFIG_ACCOUNT" ] && [ -n "$E2E_FUNDING" ] \
|
||||
|| die "staged fixtures incomplete in $E2E_WALLET_HOME"
|
||||
|
||||
E2E_CONFIRM_TIMEOUT_S="${E2E_CONFIRM_TIMEOUT_S:-600}"
|
||||
E2E_POLL_INTERVAL_S="${E2E_POLL_INTERVAL_S:-10}"
|
||||
E2E_EPOCH_SIZE_SEC="${E2E_EPOCH_SIZE_SEC:-600}"
|
||||
E2E_ROOT_WINDOW_TIMEOUT_S="${E2E_ROOT_WINDOW_TIMEOUT_S:-120}"
|
||||
export E2E_SEQUENCER E2E_DEPLOYMENT_DIR E2E_WALLET_HOME E2E_TREE_ID \
|
||||
E2E_CONFIG_ACCOUNT E2E_FUNDING E2E_CONFIRM_TIMEOUT_S E2E_POLL_INTERVAL_S \
|
||||
E2E_EPOCH_SIZE_SEC E2E_ROOT_WINDOW_TIMEOUT_S
|
||||
say "deployment: $name tree ${E2E_TREE_ID:0:8}… config $E2E_CONFIG_ACCOUNT funding $E2E_FUNDING"
|
||||
}
|
||||
|
||||
target_down() { :; }
|
||||
|
||||
@@ -23,6 +23,7 @@ list_scenarios() {
|
||||
for env in "$HERE"/scenarios/*/scenario.env; do
|
||||
[ -f "$env" ] || continue
|
||||
id=$(basename "$(dirname "$env")")
|
||||
# shellcheck source=/dev/null
|
||||
status=$(. "$env"; printf '%s' "${STATUS:-active}")
|
||||
printf ' %-14s %s\n' "$id" "$status"
|
||||
done
|
||||
@@ -66,6 +67,7 @@ say "run dir: $E2E_RUN_DIR"
|
||||
. "$HERE/harness/artifacts.sh"
|
||||
resolve_artifacts
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
. "$TARGET_SH"
|
||||
trap 'target_down' EXIT
|
||||
target_up
|
||||
|
||||
+288
-6
@@ -1,8 +1,290 @@
|
||||
#!/usr/bin/env bash
|
||||
# W1-C ports logos-rln-modules/logos-rln-module/tests/e2e_register_testnet.sh
|
||||
# here, minus its harness stages (staging/daemon/wallet move to harness/lib),
|
||||
# with poll budgets from the contract env instead of testnet constants and the
|
||||
# registry id derived as logos:${E2E_TARGET}:<config-account-hex>.
|
||||
# scenarios/register — the single-node membership lifecycle over the real
|
||||
# module stack (logos_execution_zone -> liblogos_lez_rln_module ->
|
||||
# liblogos_rln_module). Drives a PAID registration through the membership
|
||||
# module's spec surface — the faucet-funded Register instruction, NOT the
|
||||
# gifter's RegisterFree path:
|
||||
#
|
||||
# open wallet -> sync -> derive fresh holding -> claim_tokens (faucet)
|
||||
# -> unlock_keystore -> register (membership module GENERATES the credential
|
||||
# in-module) -> poll get_membership_state to "active" -> select_membership
|
||||
# -> get_merkle_proof -> cross-check via liblogos_lez_rln_module.get_membership
|
||||
# -> start (warm root window) -> generate_proof -> get_epoch_quota
|
||||
# -> verify_proof (verdict valid) -> verify_proof with a tampered signal
|
||||
# (verdict invalid)
|
||||
#
|
||||
# Besides the registration itself this is the acceptance for two open
|
||||
# architecture risks:
|
||||
# R2 — lp_* calls INTO a Rust module (membership -> rln over the raw lp
|
||||
# client). A provider_failure on every membership call while direct
|
||||
# liblogos_lez_rln_module calls succeed means the lp transport to Rust
|
||||
# modules is broken -> fall back to the generated typed client.
|
||||
# R4 — the host stamping instance_persistence_path. unlock_keystore
|
||||
# failing with kind "internal" (no persistence path) means logoscore
|
||||
# does not provide one -> the module needs an explicit override.
|
||||
#
|
||||
# Target-agnostic: chain, deployment, funding mode and every poll budget
|
||||
# arrive through the harness contract (docs/contract.md) — the target is a
|
||||
# flag, and nothing here is derived locally.
|
||||
#
|
||||
# Cost: one registration at rate_limit=100 burns 100 x price_per_unit, taken
|
||||
# from a fresh faucet claim; the run takes as long as the target's
|
||||
# confirmation budget allows.
|
||||
#
|
||||
# Env beyond docs/contract.md:
|
||||
# E2E_RATE_LIMIT=100 registration rate limit
|
||||
set -uo pipefail
|
||||
. "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/harness/lib/compat.sh"
|
||||
die "scenarios/register: not implemented yet (W1-C)"
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd "$HERE/../.." && pwd)"
|
||||
for _lib in compat json lgx daemon wallet chain; do
|
||||
# shellcheck source=/dev/null
|
||||
. "$ROOT/harness/lib/$_lib.sh"
|
||||
done
|
||||
|
||||
RATE_LIMIT="${E2E_RATE_LIMIT:-100}"
|
||||
NODE=n1
|
||||
|
||||
for _v in LOGOSCORE E2E_MODULES_DIR E2E_SEQUENCER E2E_WALLET_HOME E2E_CONFIG_ACCOUNT \
|
||||
E2E_TREE_ID E2E_FUNDING E2E_CONFIRM_TIMEOUT_S E2E_POLL_INTERVAL_S \
|
||||
E2E_EPOCH_SIZE_SEC E2E_ROOT_WINDOW_TIMEOUT_S; do
|
||||
eval "[ -n \"\${$_v:-}\" ]" || die "contract env missing: $_v (see docs/contract.md)"
|
||||
done
|
||||
[ "$E2E_POLL_INTERVAL_S" -ge 1 ] 2>/dev/null || die "E2E_POLL_INTERVAL_S must be a positive integer"
|
||||
[ "$E2E_FUNDING" = "faucet" ] \
|
||||
|| die "target '$E2E_TARGET' provides funding=$E2E_FUNDING — this scenario exercises the faucet-paid Register path (no gifter); pick a faucet deployment"
|
||||
|
||||
# Poll count for a contract budget, floor 1.
|
||||
polls() {
|
||||
local n=$(( $1 / $2 ))
|
||||
[ "$n" -ge 1 ] || n=1
|
||||
printf '%s' "$n"
|
||||
}
|
||||
|
||||
NODE_UP=0
|
||||
DYING=0
|
||||
# The daemon's reply is usually the whole story on failure.
|
||||
die() {
|
||||
printf '%s\n' "e2e: FAIL: $*" >&2
|
||||
if [ "$DYING" = 0 ] && [ "$NODE_UP" = 1 ]; then
|
||||
DYING=1
|
||||
echo "---- node log tail ----" >&2
|
||||
node_logs "$NODE" 40 >&2 || true
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
cleanup() {
|
||||
if [ "${E2E_KEEP:-0}" = "1" ]; then
|
||||
say "E2E_KEEP=1: leaving node $NODE up, state in $E2E_RUN_DIR"
|
||||
return
|
||||
fi
|
||||
[ "$NODE_UP" = 1 ] && daemon_stop "$NODE"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
CONFIG_HEX=$(python3 - "$E2E_CONFIG_ACCOUNT" <<'EOF'
|
||||
import sys
|
||||
A = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||
n = 0
|
||||
for c in sys.argv[1]:
|
||||
n = n * 58 + A.index(c)
|
||||
print(n.to_bytes(32, "big").hex())
|
||||
EOF
|
||||
) || die "cannot decode config account '$E2E_CONFIG_ACCOUNT'"
|
||||
REGISTRY_ID="logos:${E2E_TARGET}:$CONFIG_HEX"
|
||||
say "registry: $REGISTRY_ID (tree ${E2E_TREE_ID:0:8}…, sequencer $E2E_SEQUENCER)"
|
||||
|
||||
# ---------- node ------------------------------------------------------------
|
||||
section "node"
|
||||
daemon_start "$NODE" || die "daemon_start $NODE failed"
|
||||
NODE_UP=1
|
||||
daemon_load_modules "$NODE" logos_execution_zone liblogos_lez_rln_module liblogos_rln_module \
|
||||
|| die "load-module failed"
|
||||
|
||||
# ---------- wallet: open + sync ---------------------------------------------
|
||||
section "wallet"
|
||||
wallet_open "$NODE" || die "wallet open failed"
|
||||
CHAIN_HEAD=$(chain_head) || die "cannot probe chain head at $E2E_SEQUENCER"
|
||||
say "syncing wallet to chain head $CHAIN_HEAD"
|
||||
wallet_sync "$NODE" "$CHAIN_HEAD"
|
||||
|
||||
# ---------- faucet funding (Register-instruction path, no gifter) -----------
|
||||
say "deriving a fresh holding account"
|
||||
HOLDING=$(wallet_fresh_holding "$NODE") || HOLDING=""
|
||||
[ -n "$HOLDING" ] || die "no unused holding account"
|
||||
say "holding: $HOLDING"
|
||||
|
||||
# rate_limit x price_per_unit, doubled for slack — read the live price from
|
||||
# the v1.1 bounds method rather than hardcoding the deployment's tariff.
|
||||
BOUNDS=$(node_call "$NODE" liblogos_lez_rln_module get_registry_bounds \
|
||||
"$(argfile cfg "$E2E_CONFIG_ACCOUNT")" | jres) || BOUNDS=""
|
||||
[ -n "$BOUNDS" ] || die "get_registry_bounds failed (rln module up?)"
|
||||
PRICE=$(printf '%s' "$BOUNDS" | jfield price_per_unit)
|
||||
[ -n "$PRICE" ] || die "no price_per_unit in bounds: $BOUNDS"
|
||||
CLAIM=$(( RATE_LIMIT * PRICE * 2 ))
|
||||
say "claiming $CLAIM RLNTOK from the faucet (rate $RATE_LIMIT x price $PRICE x2)"
|
||||
CLAIM_RES=$(node_call "$NODE" liblogos_lez_rln_module claim_tokens \
|
||||
"$(argfile cfg2 "$E2E_CONFIG_ACCOUNT")" "$(argfile hold "$HOLDING")" "$CLAIM" | jres) || CLAIM_RES=""
|
||||
[ -n "$CLAIM_RES" ] || die "claim_tokens failed"
|
||||
wait_balance "$NODE" "$HOLDING" "$CLAIM" || die "faucet credit never landed (want $CLAIM)"
|
||||
|
||||
# ---------- scope (the identity is generated INSIDE the module) --------------
|
||||
# The consumer supplies only the scope (registry_id + rln_identifier) and the
|
||||
# rate limit; register mints and persists the credential in-module and the
|
||||
# secret never crosses the wire.
|
||||
RLN_ID=$(openssl rand -hex 32)
|
||||
|
||||
# ---------- the registration, through the membership module -----------------
|
||||
section "registration"
|
||||
UNLOCK=$(node_call "$NODE" liblogos_rln_module unlock_keystore e2e-test-password | jres) || UNLOCK=""
|
||||
case "$UNLOCK" in
|
||||
*'"unlocked":true'*) say "keystore unlocked" ;;
|
||||
*'no persistence path'*|*'not initialized'*)
|
||||
die "R4 CONFIRMED: host provides no instance_persistence_path — unlock said: $UNLOCK" ;;
|
||||
*) die "unlock_keystore failed: ${UNLOCK:-<empty>}" ;;
|
||||
esac
|
||||
|
||||
OPTIONS_JSON="{\"funding_holding_account_id\":\"$HOLDING\"}"
|
||||
say "register($REGISTRY_ID, rate $RATE_LIMIT) via membership module"
|
||||
REG=$(node_call "$NODE" liblogos_rln_module register \
|
||||
"$REGISTRY_ID" "$(argfile rlnid "$RLN_ID")" "$RATE_LIMIT" "$OPTIONS_JSON" | jres) || REG=""
|
||||
case "$REG" in
|
||||
*'"state":"pending"'*) ;;
|
||||
*'provider_failure'*)
|
||||
die "R2 CONFIRMED?: membership->rln lp transport failed. Reply: $REG — check whether direct liblogos_lez_rln_module calls above succeeded (they did if you see this), which isolates the fault to lp calls INTO a Rust module. Fallback: generated typed client." ;;
|
||||
*) die "register failed: ${REG:-<empty>}" ;;
|
||||
esac
|
||||
MEMBERSHIP_HASH=$(printf '%s' "$REG" | jfield membership_hash)
|
||||
# The commitment is public — the module surfaces it in the Membership view (no
|
||||
# secret). Used only for the sibling cross-check below.
|
||||
COMMITMENT=$(printf '%s' "$REG" | python3 -c \
|
||||
'import json,sys; print(json.load(sys.stdin).get("credential",{}).get("identity_commitment",""))' 2>/dev/null || true)
|
||||
say "pending membership: $MEMBERSHIP_HASH (commitment ${COMMITMENT:0:16}…)"
|
||||
|
||||
say "polling get_membership_state to active (budget ${E2E_CONFIRM_TIMEOUT_S}s)…"
|
||||
STATE=""
|
||||
STATE_JSON=""
|
||||
for _t in $(seq 1 "$(polls "$E2E_CONFIRM_TIMEOUT_S" "$E2E_POLL_INTERVAL_S")"); do
|
||||
STATE_JSON=$(node_call "$NODE" liblogos_rln_module get_membership_state \
|
||||
"$REGISTRY_ID" "$(argfile rlnid "$RLN_ID")" | jres) || STATE_JSON=""
|
||||
STATE=$(printf '%s' "$STATE_JSON" | jfield state)
|
||||
say " state poll $_t: ${STATE:-<none>}"
|
||||
case "$STATE" in
|
||||
active|grace_period) break ;;
|
||||
failed) die "registration FAILED: $(node_call "$NODE" liblogos_rln_module get_memberships "$REGISTRY_ID" | jres)" ;;
|
||||
esac
|
||||
sleep "$E2E_POLL_INTERVAL_S"
|
||||
done
|
||||
[ "$STATE" = "active" ] || [ "$STATE" = "grace_period" ] \
|
||||
|| die "membership never became active (last state: ${STATE:-<none>})"
|
||||
LEAF=$(printf '%s' "$STATE_JSON" | jfield leaf_index)
|
||||
say "ACTIVE at leaf $LEAF"
|
||||
|
||||
# ---------- post-registration surface ----------------------------------------
|
||||
# select_membership returns the PUBLIC view only (the secret never leaves the
|
||||
# module); assert the membership_hash rather than a released credential.
|
||||
SELECTED=$(node_call "$NODE" liblogos_rln_module select_membership \
|
||||
"$REGISTRY_ID" "$(argfile rlnid "$RLN_ID")" "" | jres) || SELECTED=""
|
||||
case "$SELECTED" in
|
||||
*"$MEMBERSHIP_HASH"*) say "select_membership returned the public membership" ;;
|
||||
*) die "select_membership did not return the membership: ${SELECTED:-<empty>}" ;;
|
||||
esac
|
||||
|
||||
PROOF=$(node_call "$NODE" liblogos_rln_module get_merkle_proof "$REGISTRY_ID" "$LEAF" | jres) || PROOF=""
|
||||
case "$PROOF" in
|
||||
*'"valid_roots"'*) say "get_merkle_proof returned a rooted proof" ;;
|
||||
*) die "get_merkle_proof failed: ${PROOF:-<empty>}" ;;
|
||||
esac
|
||||
|
||||
CROSS=$(node_call "$NODE" liblogos_lez_rln_module get_membership \
|
||||
"$(argfile cfg3 "$E2E_CONFIG_ACCOUNT")" "$(argfile commit2 "$COMMITMENT")" | jres) || CROSS=""
|
||||
case "$CROSS" in
|
||||
*'"registered":true'*) say "cross-check: rln module sees the membership ($(printf '%s' "$CROSS" | jfield state))" ;;
|
||||
*) die "cross-check get_membership failed: ${CROSS:-<empty>}" ;;
|
||||
esac
|
||||
|
||||
# ---------- rate-limit proofs (the spec's rate-limiting portion) --------------
|
||||
# start() warms the registry's valid-root window; generate_proof spends a
|
||||
# message_id slot and proves in-module (the secret never crosses the wire);
|
||||
# verify_proof serves from the local window only — it is expected to answer
|
||||
# not_ready until the warm-up read lands, so poll that away first.
|
||||
section "rate-limit proofs"
|
||||
say "start(registries=[$REGISTRY_ID]) to warm the root window"
|
||||
# epoch_size: verify_proof binds proofs to the current epoch (±1), and the
|
||||
# window warm-up polling below can span tens of seconds — a 1s default epoch
|
||||
# would expire the proof before verification. The target sizes it to its own
|
||||
# confirmation speed.
|
||||
START=$(node_call "$NODE" liblogos_rln_module start \
|
||||
"{\"epoch_size_sec\":$E2E_EPOCH_SIZE_SEC,\"registries\":[\"$REGISTRY_ID\"]}" | jres | jval) || START=""
|
||||
case "$START" in
|
||||
*'"started":true'*) ;;
|
||||
*) die "start failed: ${START:-<empty>}" ;;
|
||||
esac
|
||||
|
||||
SIGNAL_HEX=$(printf 'logos e2e signal' | to_hex)
|
||||
say "generate_proof over the registered membership"
|
||||
# timestamp: the consumer's Unix-seconds clock — the module derives the proof's
|
||||
# epoch from it (not its own clock). `date +%s` == now, so the epoch lands in
|
||||
# the start()'d window.
|
||||
# str: forces a literal string — a bare or @file numeric arg is coerced to a
|
||||
# JSON number by the CLI, which the tstr dispatch then reads as "".
|
||||
PROOF_JSON=$(node_call "$NODE" liblogos_rln_module generate_proof \
|
||||
"$REGISTRY_ID" "$(argfile rlnid2 "$RLN_ID")" "$(argfile sig "$SIGNAL_HEX")" "str:$(date +%s)" | jres | jval) || PROOF_JSON=""
|
||||
case "$PROOF_JSON" in
|
||||
*'"proof"'*'"nullifier"'*|*'"nullifier"'*'"proof"'*) ;;
|
||||
*) die "generate_proof failed: ${PROOF_JSON:-<empty>}" ;;
|
||||
esac
|
||||
MESSAGE_ID=$(printf '%s' "$PROOF_JSON" | jfield message_id)
|
||||
say "proof issued (message_id ${MESSAGE_ID:-?}, epoch $(printf '%s' "$PROOF_JSON" | jfield epoch))"
|
||||
|
||||
# The quota snapshot (logos-delivery's QuotaProvider shape): numeric
|
||||
# epoch_index + rate_limit + remaining, decremented by the proof above —
|
||||
# asserted strictly only when the epoch didn't roll in between.
|
||||
QUOTA=$(node_call "$NODE" liblogos_rln_module get_epoch_quota \
|
||||
"$REGISTRY_ID" "$(argfile rlnid5 "$RLN_ID")" | jres | jval) || QUOTA=""
|
||||
case "$QUOTA" in
|
||||
*'"epoch_index"'*'"remaining"'*) ;;
|
||||
*) die "get_epoch_quota failed: ${QUOTA:-<empty>}" ;;
|
||||
esac
|
||||
REMAINING=$(printf '%s' "$QUOTA" | jfield remaining)
|
||||
Q_EPOCH=$(printf '%s' "$QUOTA" | jfield epoch_index)
|
||||
PROOF_EPOCH=$(printf '%s' "$PROOF_JSON" | jfield epoch)
|
||||
if [ "$Q_EPOCH" = "$PROOF_EPOCH" ]; then
|
||||
[ "$REMAINING" = "$((RATE_LIMIT - 1))" ] \
|
||||
|| die "quota remaining $REMAINING != $((RATE_LIMIT - 1)) after one proof"
|
||||
say "epoch quota: remaining $REMAINING/$RATE_LIMIT in epoch $Q_EPOCH"
|
||||
else
|
||||
say "epoch rolled between proof and quota (proof $PROOF_EPOCH, quota $Q_EPOCH) — remaining $REMAINING"
|
||||
fi
|
||||
|
||||
say "verify_proof from the local root window (polling not_ready away)…"
|
||||
VALID=""
|
||||
for _t in $(seq 1 "$(polls "$E2E_ROOT_WINDOW_TIMEOUT_S" "$E2E_POLL_INTERVAL_S")"); do
|
||||
VERIFY=$(node_call "$NODE" liblogos_rln_module verify_proof \
|
||||
"$REGISTRY_ID" "$(argfile rlnid3 "$RLN_ID")" "$(argfile sig2 "$SIGNAL_HEX")" \
|
||||
"$(argfile proof "$PROOF_JSON")" | jres | jval) || VERIFY=""
|
||||
case "$VERIFY" in
|
||||
*'"verdict":"valid"'*) VALID=yes; break ;;
|
||||
*'"verdict":"invalid"'*) die "verify_proof rejected our own fresh proof: $VERIFY" ;;
|
||||
*'not_ready'*) say " root window still cold ($_t)"; sleep "$E2E_POLL_INTERVAL_S" ;;
|
||||
*) die "verify_proof failed: ${VERIFY:-<empty>}" ;;
|
||||
esac
|
||||
done
|
||||
[ "$VALID" = "yes" ] || die "verify_proof never left not_ready (root window warm-up)"
|
||||
say "verify_proof: valid"
|
||||
|
||||
# A different signal against the same proof MUST be invalid — not an error.
|
||||
TAMPER_HEX=$(printf 'tampered signal' | to_hex)
|
||||
TVERIFY=$(node_call "$NODE" liblogos_rln_module verify_proof \
|
||||
"$REGISTRY_ID" "$(argfile rlnid4 "$RLN_ID")" "$(argfile sig3 "$TAMPER_HEX")" \
|
||||
"$(argfile proof2 "$PROOF_JSON")" | jres | jval) || TVERIFY=""
|
||||
case "$TVERIFY" in
|
||||
*'"verdict":"invalid"'*) say "tampered signal correctly invalid" ;;
|
||||
*) die "tampered signal was not rejected: ${TVERIFY:-<empty>}" ;;
|
||||
esac
|
||||
|
||||
echo
|
||||
echo "e2e: PASS — registered on $REGISTRY_ID"
|
||||
echo "e2e: membership_hash $MEMBERSHIP_HASH"
|
||||
echo "e2e: leaf_index $LEAF"
|
||||
echo "e2e: funded by $HOLDING (faucet claim, no gifter)"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# register — the module-stack e2e: faucet claim, RLN membership registration,
|
||||
# merkle proof + registry cross-check, proof generation/verification (valid +
|
||||
# tampered). Ported from logos-rln-modules in W1-C.
|
||||
# tampered). Needs a faucet-funded deployment (E2E_FUNDING=faucet).
|
||||
NODES=1
|
||||
NEEDS_MODULES="logos_execution_zone liblogos_lez_rln_module liblogos_rln_module"
|
||||
TARGETS="local testnet"
|
||||
|
||||
Reference in New Issue
Block a user