diff --git a/.env b/.env deleted file mode 100644 index f0413c4f..00000000 --- a/.env +++ /dev/null @@ -1,133 +0,0 @@ -# Local Network Configuration -# -# This file is read by: -# - docker-compose (YAML variable substitution, plain key=value only) -# - host scripts (source .env) -# - containers (volume-mounted at /opt/config/.env, sourced by run.sh) -# -# Local overrides: create .env.local (gitignored) to override values for host -# scripts. Host scripts source .env.local after .env. Note: .env.local does NOT -# affect containers or docker-compose — those always use .env directly. -# -# Host scripts use ${VAR_HOST:-localhost} for service hostnames, allowing -# devcontainer environments to set *_HOST env vars (e.g. CHAIN_HOST=chain) -# to reach services on the Docker network instead of localhost. - -# --- Service profiles --- -# Controls which optional service groups are started. -# Available profiles: -# block-oracle epoch block oracle -# explorer block explorer UI -# rewards-eligibility REO eligibility oracle node -# indexing-payments dipper + iisa (requires GHCR auth — see README) -# Default: profiles that work out of the box. -COMPOSE_PROFILES=block-oracle,explorer -# All profiles (indexing-payments requires GHCR auth — see README): -#COMPOSE_PROFILES=rewards-eligibility,block-oracle,explorer,indexing-payments - -# --- Dev overrides --- -# Uncomment and extend to build services from local source. -# See compose/dev/README.md for available overrides. -#COMPOSE_FILE=docker-compose.yaml:compose/dev/graph-node.yaml - -# indexer components versions -GRAPH_NODE_VERSION=v0.42.1 -INDEXER_AGENT_VERSION=v0.25.10 -INDEXER_SERVICE_RS_VERSION=v2.1.0 -INDEXER_TAP_AGENT_VERSION=v2.1.0 - -# indexing-payments image versions (requires GHCR auth — see README) -# Set real tags in .env.local when enabling the indexing-payments profile. -DIPPER_VERSION=sha-24d10d4 -IISA_VERSION= - -# gateway components versions -GATEWAY_COMMIT=29fa2968439723548ff67926575a6cfb73876e7c -GRAPH_TALLY_AGGREGATOR_VERSION=v0.7.1 -GRAPH_TALLY_ESCROW_MANAGER_VERSION=v2.0.0 - -# eligibility oracle (clone-and-build — requires published repo) -ELIGIBILITY_ORACLE_COMMIT=84710857394d3419f83dcbf6687a91f415cc1625 - -# network components versions -BLOCK_ORACLE_COMMIT=3a3a425ff96130c3842cee7e43d06bbe3d729aed -CONTRACTS_COMMIT=511cd70563593122f556c7b35469ec185574769a -NETWORK_SUBGRAPH_COMMIT=5b6c22089a2e55db16586a19cbf6e1d73a93c7b9 - -# service ports -CHAIN_RPC_PORT=8545 -IPFS_RPC_PORT=5001 -POSTGRES_PORT=5432 -GRAPH_NODE_GRAPHQL_PORT=8000 -GRAPH_NODE_ADMIN_PORT=8020 -GRAPH_NODE_STATUS_PORT=8030 -GRAPH_NODE_METRICS_PORT=8040 -INDEXER_MANAGEMENT_PORT=7600 -INDEXER_SERVICE_PORT=7601 -GATEWAY_PORT=7700 -REDPANDA_KAFKA_EXTERNAL_PORT=29092 -REDPANDA_ADMIN_PORT=19644 -REDPANDA_PANDAPROXY_PORT=18082 -REDPANDA_SCHEMA_REGISTRY_PORT=18081 -GRAPH_TALLY_AGGREGATOR_PORT=7610 -BLOCK_EXPLORER_PORT=3000 - -# backward compat: old names without _PORT suffix (shell-only, uses ${} expansion) -# docker-compose sees these as literal strings — use _PORT names in docker-compose.yaml -# TODO: remove once all consumers (other repos) are migrated to _PORT names -CHAIN_RPC=${CHAIN_RPC_PORT} -IPFS_RPC=${IPFS_RPC_PORT} -POSTGRES=${POSTGRES_PORT} -GRAPH_NODE_GRAPHQL=${GRAPH_NODE_GRAPHQL_PORT} -GRAPH_NODE_ADMIN=${GRAPH_NODE_ADMIN_PORT} -GRAPH_NODE_STATUS=${GRAPH_NODE_STATUS_PORT} -GRAPH_NODE_METRICS=${GRAPH_NODE_METRICS_PORT} -INDEXER_MANAGEMENT=${INDEXER_MANAGEMENT_PORT} -INDEXER_SERVICE=${INDEXER_SERVICE_PORT} -GATEWAY=${GATEWAY_PORT} -REDPANDA_KAFKA_EXTERNAL=${REDPANDA_KAFKA_EXTERNAL_PORT} -REDPANDA_ADMIN=${REDPANDA_ADMIN_PORT} -REDPANDA_PANDAPROXY=${REDPANDA_PANDAPROXY_PORT} -REDPANDA_SCHEMA_REGISTRY=${REDPANDA_SCHEMA_REGISTRY_PORT} -GRAPH_TALLY_AGGREGATOR=${GRAPH_TALLY_AGGREGATOR_PORT} -BLOCK_EXPLORER=${BLOCK_EXPLORER_PORT} - -# Indexing Payments (used with indexing-payments override) -DIPPER_ADMIN_RPC_PORT=9000 -DIPPER_INDEXER_RPC_PORT=9001 - -## Chain config -CHAIN_ID=1337 -CHAIN_NAME="hardhat" - -## Wallet -## - Account 0 used by: EBO, admin actions (deploy contracts, transfer ETH/GRT), gateway payer for PaymentsEscrow -## - Account 1 used by: Gateway signer for PaymentsEscrow -## - Account x402 used by: Gateway x402 receiver wallet -MNEMONIC="test test test test test test test test test test test junk" -ACCOUNT0_ADDRESS="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" -ACCOUNT0_SECRET="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" -ACCOUNT1_ADDRESS="0x70997970C51812dc3A010C7d01b50e0d17dc79C8" -ACCOUNT1_SECRET="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" -ACCOUNT_X402_ADDRESS="0xE19f949A060934e19239a4730D86D3a4a0D43F33" -ACCOUNT_X402_SECRET="0x48fef45dc52e43363cc31dde814c5cb9d17ecd5221bed71c8bed0ce83de37215" - -# receiver of Scalar payments (receiver is index 0 of mnemonic) -INDEXER_MNEMONIC="test test test test test test test test test test test zero" -RECEIVER_ADDRESS="0xf4EF6650E48d099a4972ea5B414daB86e1998Bd3" -RECEIVER_SECRET="0x2ee789a68207020b45607f5adb71933de0946baebbaaab74af7cbd69c8a90573" - -# subgraphs -SUBGRAPH="BFr2mx7FgkJ36Y6pE5BiXs1KmNUmVDCnL82KUSdcLW1g" -SUBGRAPH_2="9p1TRzaccKzWBN4P6YEwEUxYwJn6HwPxf5dKXK2NYxgS" - -# REO (Rewards Eligibility Oracle) -# Set to 1 to deploy and configure the REO contract (Phase 4). Unset or 0 to skip. -REO_ENABLED=0 -# eligibilityPeriod: how long an indexer stays eligible after renewal (seconds) -REO_ELIGIBILITY_PERIOD=300 -# oracleUpdateTimeout: fail-safe — if no oracle update for this long, all indexers eligible (seconds) -REO_ORACLE_UPDATE_TIMEOUT=86400 - -# Gateway -GATEWAY_API_KEY="deadbeefdeadbeefdeadbeefdeadbeef" diff --git a/.gitignore b/.gitignore index 0a484e30..86b93993 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,20 @@ .claude .idea -# Environment overrides +# Environment files +# .env: auto-generated by scripts/resolve-recipe.sh from a recipe; picked up +# automatically by `docker compose`. Regenerate with `just resolve` or `just up`. +# .env.local: optional per-checkout overrides (highest precedence in the resolver). +# .env.secrets: optional secrets fragment (legacy; new code uses .env.local). +.env .env.local .env.secrets +# Recipe selection: .recipe (optional, committed per-branch default — branches +# pin their target scenario by committing one); .recipe.local (per-checkout +# override, never committed). +.recipe.local + # OS .DS_Store Thumbs.db @@ -20,6 +30,12 @@ Thumbs.db # Rust build artifacts tests/target/ +# State dumps from scripts/dump-state.sh (captured for offline debugging) +_dumps/ + +# Volume snapshots from scripts/bake-snapshot.sh +_snapshots/ + # Legacy local config directory (now uses config-local Docker volume) config/local/ diff --git a/CHEATSHEET.md b/CHEATSHEET.md index e1ae4e14..066c0036 100644 --- a/CHEATSHEET.md +++ b/CHEATSHEET.md @@ -129,4 +129,3 @@ curl "http://localhost:7700/api/subgraphs/id/BFr2mx7FgkJ36Y6pE5BiXs1KmNUmVDCnL82 ```bash docker exec -it redpanda rpk topic consume gateway_client_query_results --brokers="localhost:9092" ``` - diff --git a/README.md b/README.md index e5478a8e..8b4dffce 100644 --- a/README.md +++ b/README.md @@ -2,120 +2,137 @@ A local Graph network for debugging & integration tests. -## Usage +Requires Docker & Docker Compose v2.24+, plus [`just`](https://github.com/casey/just) +for the entry-point commands. -Requires Docker & Docker Compose v2.24+. +## Quick start ```bash -# Start (or resume) the network — skips already-completed setup steps -docker compose up -d - -# Re-initialise from scratch (removes all persisted state) -docker compose down -v && docker compose up -d +just up # resolve active recipe → .env, then docker compose up -d --build +just down # docker compose down +just logs gateway # docker compose logs -f gateway ``` -State (chain, postgres, ipfs) is persisted in named volumes, so the network -restarts where it left off. Use `down -v` only when you want a clean slate. - -Add `--build` to rebuild after changes to Docker build context, including modifying `run.sh` or `Dockerfile`, or changed source code. - -## Useful commands +The first `just up` materialises a recipe (see below) into a gitignored `.env` +file. After that, bare `docker compose` commands work directly — `just up` just +chains recipe resolution + a build-aware compose up. `docker compose` halts +with a clear error if `.env` is missing. -- `docker compose up -d --build ${service}` — rebuild a single service after code changes -- `docker compose logs -f ${service}` -- `source .env` +## Recipes -Useful commands for each component can be found at [CHEATSHEET.md](CHEATSHEET.md) +A **recipe** selects which env fragments compose, which compose profiles enable, +and which image versions pin. Recipes live in [recipes/](recipes/) and reference +fragments in [config/](config/). -## Local Overrides - -Create `.env.local` (gitignored) to override defaults without touching `.env`: +| Recipe | Profile set | Includes | +| ------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| `baseline` | `block-oracle,explorer,rewards-eligibility` | Full GIP-0088 contract deployment (REO + IA + RAM) on stable image versions | +| `indexing-payments` | `explorer,rewards-eligibility,indexing-payments` | Baseline + WIP DIPs services (dipper, IISA, indexing-payments subgraph, dips-fork indexer-rs) | ```bash -# .env.local — your local settings -COMPOSE_PROFILES=rewards-eligibility,block-oracle,explorer,indexing-payments -GRAPH_NODE_VERSION=v0.38.0-rc1 +just recipes # list available recipes +just recipe-active # show which one is currently selected +just up indexing-payments # one-shot recipe override +echo indexing-payments > .recipe.local # per-checkout sticky default (gitignored) ``` -Host scripts source `.env.local` automatically after `.env`. +Recipe selection precedence: CLI arg → `RECIPE` env → `.recipe.local` → +`.recipe` (committed per-branch default) → `baseline`. + +To add a recipe, create `recipes/my-recipe.json` with the fragments + override +`env` to apply. See existing JSON files for the shape. -## Service Profiles +## Local overrides -Optional services are controlled via `COMPOSE_PROFILES` in `.env`. -By default, profiles that work out of the box are enabled: +Create `.env.local` (gitignored) for machine-specific values that layer on top +of the resolved recipe — image-tag overrides, compose-profile additions, etc: ```bash -COMPOSE_PROFILES=rewards-eligibility,block-oracle,explorer +# .env.local +GRAPH_NODE_VERSION=v0.43.0 +COMPOSE_PROFILES=block-oracle,explorer,rewards-eligibility,indexing-payments,my-extra ``` -Available profiles: +`.env.local` is sourced last during recipe resolution, so its values win. -| Profile | Services | Prerequisites | -| --------------------- | --------------------------------- | -------------------------- | -| `block-oracle` | block-oracle | none | -| `explorer` | block-explorer UI | none | -| `rewards-eligibility` | eligibility-oracle-node | none (clones from GitHub) | -| `indexing-payments` | dipper, iisa, iisa-scoring | GHCR auth (below) | +## Iterating on upstream source -To enable all profiles, uncomment the full line in `.env`: +Most services run from prebuilt images pinned by `${SERVICE_VERSION}` vars +in [config/services.env](config/services.env) and +[config/indexing-payments.env](config/indexing-payments.env). To iterate on +upstream source, build a locally-tagged image in the upstream repo and +override the version pin in `.env.local`: ```bash -COMPOSE_PROFILES=rewards-eligibility,block-oracle,explorer,indexing-payments +# .env.local +INDEXING_PAYMENTS_SUBGRAPH_VERSION=local ``` -### GHCR authentication (indexing-payments) +Then `just rebuild ` here to pick up the change. -The `indexing-payments` profile pulls private images from `ghcr.io/edgeandnode`. -Create a GitHub **classic** Personal Access Token with `read:packages` scope -(https://github.com/settings/tokens — fine-grained tokens do not support packages) and log in once: +How each upstream produces a `:local` tag is repo-specific. Convention is a +`just build-image` recipe that tags `:local` — e.g. +graphprotocol/indexing-payments-subgraph builds +`ghcr.io/graphprotocol/indexing-payments-subgraph:local` via its `justfile`. + +## Rebuilding after edits ```bash -echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_USERNAME --password-stdin +just rebuild indexer-agent # rebuild + restart one service +just rebuild # rebuild + restart all +just up # equivalent if recipe hasn't changed (defaults to --build) ``` -Then set the image versions in `.env` or `.env.local`: +`run.sh` and `Dockerfile` changes only take effect after a rebuild. + +## State persistence + +Volumes (`chain-data`, `postgres-data`, `ipfs-data`, `redpanda-data`, +`iisa-scores`, `config-local`) survive `just down`. To start clean: ```bash -DIPPER_VERSION= -IISA_VERSION= +just reset # docker compose down -v +just up ``` -## Building from source - Dev overrides (compose/dev/) +## GHCR authentication (indexing-payments) -For local development, mount locally-built binaries into running containers. -Set `COMPOSE_FILE` in `.env` to include dev override files: +The `indexing-payments` profile pulls private images from `ghcr.io/edgeandnode`. +Create a GitHub **classic** Personal Access Token with `read:packages` scope +([fine-grained tokens don't support packages](https://github.com/settings/tokens)) +and log in once: ```bash -# Mount local indexer-service binary -INDEXER_SERVICE_BINARY=/path/to/indexer-rs/target/release/indexer-service-rs -COMPOSE_FILE=docker-compose.yaml:compose/dev/indexer-service.yaml - -# Multiple overrides -COMPOSE_FILE=docker-compose.yaml:compose/dev/indexer-service.yaml:compose/dev/tap-agent.yaml +echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_USERNAME --password-stdin ``` -Each override requires a binary path env var. Source repos own their own build; -local-network just wraps the published image with `run.sh` and utilities. -See [compose/dev/README.md](compose/dev/README.md) for details. - ## Devcontainer usage -When running inside a devcontainer, service names (gateway, redpanda, etc.) won't resolve by default because the devcontainer is on a different Docker network. Connect it to the compose network once per session: +Inside a devcontainer, service names won't resolve by default because the +devcontainer is on a different Docker network. Connect once per session: ```bash scripts/connect-network.sh ``` -The script auto-detects the compose project network. You can also pass a network name explicitly: `scripts/connect-network.sh my-network_default`. +The script auto-detects the compose project network. Pass a name explicitly with +`scripts/connect-network.sh my-network_default`. + +## Component cheatsheet + +See [CHEATSHEET.md](CHEATSHEET.md) for per-component commands. ## Common issues ### `too far behind` -Gateway error: - ``` ERROR graph_gateway::network::subgraph_client: network_subgraph_query_err="response too far behind" ``` -This happens when subgraphs fall behind the chain head. With automine (default), this is a harmless warning during startup. Run `scripts/mine-block.sh 10` to advance blocks manually if needed. +Subgraphs fell behind the chain head. With automine (default), this is harmless +during startup. `scripts/mine-block.sh 10` to advance blocks manually. + +### `LOCAL_NETWORK_RECIPE is missing a value` + +`.env` hasn't been generated yet. Run `just resolve` (or any `just up`). diff --git a/compose/dev/indexer-agent.yaml b/compose/dev/indexer-agent.yaml index c3135c0e..f8c526ff 100644 --- a/compose/dev/indexer-agent.yaml +++ b/compose/dev/indexer-agent.yaml @@ -11,7 +11,7 @@ services: indexer-agent: entrypoint: bash -cl /opt/run-override.sh ports: - - "${INDEXER_MANAGEMENT}:7600" + - "${INDEXER_MANAGEMENT_PORT}:7600" # Nodejs debugger - 9230:9230 volumes: diff --git a/compose/test-indexer.yaml b/compose/test-indexer.yaml new file mode 100644 index 00000000..22a7006f --- /dev/null +++ b/compose/test-indexer.yaml @@ -0,0 +1,105 @@ +# Per-test indexer stack — instantiated by IndexerHandle as its own compose project. +# +# Usage from Rust (IndexerHandle::new): +# docker compose \ +# -f compose/test-indexer.yaml \ +# --project-name local-network-test- \ +# --env-file .env \ +# up -d +# +# Each per-test compose project gets: +# - its own postgres + graph-node + indexer-agent (independent of the main stack) +# - shared access to main's chain + ipfs via the cross-stack network +# - shared read-only access to main's config-local volume (contract address books) +# +# Service names (postgres / graph-node / indexer-agent) are SAME as the main stack +# but are isolated to the project's default network — main and per-test instances +# don't collide because only chain + ipfs are exposed on cross-stack. +# +# Per-test indexer identity is passed via env vars: TEST_INDEXER_ADDRESS, +# TEST_INDEXER_SECRET, TEST_INDEXER_MNEMONIC. The agent's run.sh prefers these +# over the .env defaults (see indexer-agent/run.sh). + +networks: + default: + cross-stack: + name: cross-stack + external: true + +volumes: + config-local: + name: local-network_config-local + external: true + +services: + postgres: + image: postgres:17-alpine + command: postgres -c 'max_connections=200' + volumes: + - ../containers/core/postgres/setup.sql:/docker-entrypoint-initdb.d/setup.sql:ro + environment: + POSTGRES_INITDB_ARGS: "--encoding UTF8 --locale=C" + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_USER: postgres + healthcheck: { interval: 1s, retries: 30, test: pg_isready -U postgres } + restart: on-failure:3 + + graph-node: + build: + context: ../containers/indexer/graph-node + args: + GRAPH_NODE_VERSION: ${GRAPH_NODE_VERSION} + depends_on: + postgres: { condition: service_healthy } + stop_signal: SIGKILL + volumes: + - ../shared:/opt/shared:ro + - ../.env:/opt/config/.env:ro + - config-local:/opt/config:ro + healthcheck: + { interval: 1s, retries: 60, test: curl -f http://127.0.0.1:8030 } + restart: on-failure:3 + networks: [default, cross-stack] + + subgraph-deploy: + # Reuse the main-project's prebuilt image instead of building per-project + # copies. The main `just up` builds local-network-subgraph-deploy:latest; + # per-test stacks consume that same image so source/run.sh changes only + # need a rebuild in the main project, not in every test stack. + image: local-network-subgraph-deploy:latest + pull_policy: never + depends_on: + graph-node: { condition: service_healthy } + volumes: + - ../shared:/opt/shared:ro + - ../.env:/opt/config/.env:ro + - config-local:/opt/config:ro + networks: [default, cross-stack] + + indexer-agent: + # Reuse the main-project's prebuilt image (see subgraph-deploy comment). + image: local-network-indexer-agent:latest + pull_policy: never + platform: linux/amd64 + depends_on: + subgraph-deploy: { condition: service_completed_successfully } + stop_signal: SIGKILL + volumes: + - ../shared:/opt/shared:ro + - ../.env:/opt/config/.env:ro + - config-local:/opt/config:ro + healthcheck: + { interval: 5s, retries: 120, test: curl -f http://127.0.0.1:7600/ } + restart: on-failure:3 + environment: + # Per-test indexer identity — see indexer-agent/run.sh override block. + TEST_INDEXER_ADDRESS: ${TEST_INDEXER_ADDRESS} + TEST_INDEXER_SECRET: ${TEST_INDEXER_SECRET} + TEST_INDEXER_MNEMONIC: ${TEST_INDEXER_MNEMONIC} + # Manual allocation mode is appropriate for per-test agents: tests + # explicitly drive create/close cycles, and we don't want the + # auto-reconciler racing them. Unlike the main stack's removed + # manual-allocation.yaml workaround (which broke start-indexing's + # initial-allocation loop), per-test stacks don't run start-indexing. + INDEXER_AGENT_ALLOCATION_MANAGEMENT: manual + networks: [default, cross-stack] diff --git a/config/accounts-role-named.env b/config/accounts-role-named.env new file mode 100644 index 00000000..6162a968 --- /dev/null +++ b/config/accounts-role-named.env @@ -0,0 +1,31 @@ +# Per-role wallet addresses + secrets, derived from the test mnemonic. +# Each admin role gets its own account so concurrent tests don't share a +# nonce queue. +# +# - DEPLOYER (mnemonic index 0): deploys contracts; not signed against +# at test runtime (was previously bundled with all admin roles). +# - GOVERNOR (mnemonic index 1): RewardsManager governor, gateway tally signer, +# graph-tally-aggregator key, admin of REO PAUSE_ROLE. +# - OPERATOR (mnemonic index 2): REO OPERATOR_ROLE — signs setEligibilityPeriod, +# setEligibilityValidation, setOracleUpdateTimeout, manages ORACLE_ROLE +# admin; also the permissionless rewards_on_subgraph_*_update calls. +# - ORACLE (mnemonic index 3): REO ORACLE_ROLE — signs renewIndexerEligibility. +# - SUBGRAPH_AVAILABILITY_ORACLE (mnemonic index 4): setDenied() on +# RewardsManager (denial tests). +# - PAUSE_ADMIN (mnemonic index 7): REO PAUSE_ROLE — signs pause/unpause. +# (#5 and #6 are reserved as test reclaim addresses; #4 is the SAO.) +# - GATEWAY_X402 (separate key): gateway x402 receiver wallet. + +DEPLOYER_ADDRESS="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +DEPLOYER_SECRET="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +GOVERNOR_ADDRESS="0x70997970C51812dc3A010C7d01b50e0d17dc79C8" +GOVERNOR_SECRET="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" +OPERATOR_ADDRESS="0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" +OPERATOR_SECRET="0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" +ORACLE_ADDRESS="0x90F79bf6EB2c4f870365E785982E1f101E93b906" +ORACLE_SECRET="0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6" +SUBGRAPH_AVAILABILITY_ORACLE_SECRET="0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a" +PAUSE_ADMIN_ADDRESS="0x14dC79964da2C08b23698B3D3cc7Ca32193d9955" +PAUSE_ADMIN_SECRET="0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356" +GATEWAY_X402_ADDRESS="0xE19f949A060934e19239a4730D86D3a4a0D43F33" +GATEWAY_X402_SECRET="0x48fef45dc52e43363cc31dde814c5cb9d17ecd5221bed71c8bed0ce83de37215" diff --git a/config/base.env b/config/base.env new file mode 100644 index 00000000..43e0a166 --- /dev/null +++ b/config/base.env @@ -0,0 +1,57 @@ +# Base fragment — shared infrastructure across all recipes. +# Chain, ports, mnemonic, gateway API key, subgraph IDs. +# Image versions and accounts live in separate fragments. + +# --- Ports --- +CHAIN_RPC_PORT=8545 +IPFS_RPC_PORT=5001 +POSTGRES_PORT=5432 +GRAPH_NODE_GRAPHQL_PORT=8000 +GRAPH_NODE_ADMIN_PORT=8020 +GRAPH_NODE_STATUS_PORT=8030 +GRAPH_NODE_METRICS_PORT=8040 +INDEXER_MANAGEMENT_PORT=7600 +INDEXER_SERVICE_PORT=7601 +# Indexer-service exposes a separate port for DIPs queries. Defined here +# (rather than in indexing-payments.env) so the baseline recipe can render +# compose without a missing-var error on indexer-service's port mapping. +# The DIPs query endpoint only actually binds when INDEXING_PAYMENTS_ENABLED=1 +# (set by the indexing-payments overlay). To tighten further: move the DIPS +# port mapping to a profile-overlay compose file. +INDEXER_SERVICE_DIPS_PORT=7602 +GATEWAY_PORT=7700 +REDPANDA_KAFKA_EXTERNAL_PORT=29092 +REDPANDA_ADMIN_PORT=19644 +REDPANDA_PANDAPROXY_PORT=18082 +REDPANDA_SCHEMA_REGISTRY_PORT=18081 +GRAPH_TALLY_AGGREGATOR_PORT=7610 +BLOCK_EXPLORER_PORT=3000 + +# Dipper RPC ports — defined here (rather than indexing-payments.env) so the +# baseline recipe can render compose. The dipper service is profile-gated, so +# these are never actually bound under baseline — but compose substitutes +# vars at parse time regardless. Same caveat as INDEXER_SERVICE_DIPS_PORT above. +DIPPER_ADMIN_RPC_PORT=9000 +DIPPER_INDEXER_RPC_PORT=9001 + +# --- Chain config --- +CHAIN_ID=1337 +CHAIN_NAME="hardhat" + +# --- Test mnemonic (anvil default) --- +MNEMONIC="test test test test test test test test test test test junk" + +# --- Indexer wallet (mnemonic index 0 of the indexer mnemonic) --- +INDEXER_MNEMONIC="test test test test test test test test test test test zero" +INDEXER_ADDRESS="0xf4EF6650E48d099a4972ea5B414daB86e1998Bd3" +INDEXER_SECRET="0x2ee789a68207020b45607f5adb71933de0946baebbaaab74af7cbd69c8a90573" + +# --- Subgraph IDs (deterministic IPFS hashes) --- +SUBGRAPH="BFr2mx7FgkJ36Y6pE5BiXs1KmNUmVDCnL82KUSdcLW1g" +SUBGRAPH_2="9p1TRzaccKzWBN4P6YEwEUxYwJn6HwPxf5dKXK2NYxgS" + +# --- Gateway --- +GATEWAY_API_KEY="deadbeefdeadbeefdeadbeefdeadbeef" +# Optional: appended to Kafka topic names (e.g. "local" → gateway_queries_local). +# Leave empty for default topic names. All consumers must agree on this value. +#KAFKA_TOPIC_ENVIRONMENT=local diff --git a/config/indexing-payments.env b/config/indexing-payments.env new file mode 100644 index 00000000..ae2a419d --- /dev/null +++ b/config/indexing-payments.env @@ -0,0 +1,25 @@ +# Indexing-payments overlay — adds the WIP DIPs component versions on top +# of services.env. Compose this fragment AFTER services.env so the values +# here win. +# +# What this overlay turns on: +# - The dips-fork indexer-rs (indexer-service + tap-agent) that +# understands the [dips] config schema in indexer-service/run.sh. +# - The indexing-payments profile services: dipper, IISA, and the +# indexing-payments subgraph. + +# Gate the [dips] section in indexer-service config.toml. Off by default +# in services.env (upstream v2.1.0 indexer-rs would fail to parse it). +INDEXING_PAYMENTS_ENABLED=1 + +# --- Indexer-rs (dips-fork) --- +# Pinned to the dips-fork branch head (mb9/dips-switch-to-offer-authorization, +# graphprotocol/indexer-rs#1009). Required for the [dips] config schema +# (`recurring_collector`, `supported_networks`, `min_grt_per_*`). +INDEXER_SERVICE_RS_VERSION=sha-faa26d4 +INDEXER_TAP_AGENT_VERSION=sha-faa26d4 + +# --- Indexing-payments images (require GHCR auth — see README) --- +DIPPER_VERSION=sha-e803916 +IISA_VERSION=v2.3.1 +INDEXING_PAYMENTS_SUBGRAPH_VERSION=sha-a58543a diff --git a/config/mock-reo.env b/config/mock-reo.env new file mode 100644 index 00000000..af60391f --- /dev/null +++ b/config/mock-reo.env @@ -0,0 +1,11 @@ +# Mock REO fragment — wires MockRewardsEligibilityOracle as RewardsManager's +# providerEligibilityOracle. Indexers self-toggle eligibility via the mock's +# setEligible(bool) call (signed from the indexer's own key); no off-chain +# oracle, no period mechanics. Tests that need eligibility as a binary +# precondition use this path. +# +# Set REO_MOCK=0 (or omit this fragment) to keep RewardsEligibilityOracleA +# wired (production-like). REO-A governance tests in tests/reo_governance.rs +# assume that mode. + +REO_MOCK=1 diff --git a/config/services.env b/config/services.env new file mode 100644 index 00000000..c487d0e1 --- /dev/null +++ b/config/services.env @@ -0,0 +1,62 @@ +# Baseline services fragment — image versions, contract pin, and deployment +# toggles for the default local-network shape. Includes the full GIP-0088 +# contract deployment (REO + IssuanceAllocator + RecurringAgreementManager +# on the audit-fix-3 ABI) but NOT the indexing-payments services. The +# `baseline` recipe consumes this fragment alone; the `indexing-payments` +# recipe layers `indexing-payments.env` on top to add the WIP DIPs +# components (dipper, IISA, indexing-payments-subgraph, dips-fork +# indexer-rs). + +# --- Indexer components --- +GRAPH_NODE_VERSION=v0.43.0 +INDEXER_AGENT_VERSION=v0.25.10 +# Upstream `main` releases. The indexing-payments overlay overrides these +# with a dips-fork sha that adds the DIPs config schema +# (`recurring_collector`, `supported_networks`, `min_grt_per_*`) consumed +# by the indexer-service config.toml when INDEXING_PAYMENTS_ENABLED=1. +INDEXER_SERVICE_RS_VERSION=v2.1.0 +INDEXER_TAP_AGENT_VERSION=v2.1.0 + +# --- Indexing-payments images --- +# dipper/IISA only run under the `indexing-payments` profile and their FROM +# lines are never resolved on baseline — `unused` keeps compose's ${VAR} +# substitution happy without naming a real tag. The subgraph image is +# different: subgraph-deploy is profile-less, so its Dockerfile resolves the +# indexing-payments-subgraph image even on baseline. Pin to the real tag so +# baseline builds work (requires GHCR auth — see README); the indexing-payments +# subgraph gets deployed but sits idle since dipper isn't running to consume it. +DIPPER_VERSION=unused +IISA_VERSION=unused +INDEXING_PAYMENTS_SUBGRAPH_VERSION=sha-a58543a + +# --- Gateway components --- +GATEWAY_VERSION=sha-a1c56d0 +GRAPH_TALLY_AGGREGATOR_VERSION=v0.7.1 +GRAPH_TALLY_ESCROW_MANAGER_VERSION=v2.0.0 + +# --- Eligibility oracle node --- +ELIGIBILITY_ORACLE_NODE_VERSION=main + +# --- Network / contracts commits --- +BLOCK_ORACLE_COMMIT=3a3a425ff96130c3842cee7e43d06bbe3d729aed +# deployment/testnet/2024-05-09/gip-0088 — audit-fix-3 dips publish + +# GIP-0088 deployment infra. Required by GIP0088_ENABLED=1 below. +CONTRACTS_COMMIT=8eff3867bd83fbc6aeedd06ce5c2747be4b91d42 +NETWORK_SUBGRAPH_COMMIT=5b6c22089a2e55db16586a19cbf6e1d73a93c7b9 + +# --- GIP-0088 contract deployment --- +# Triggers Phase 3 in containers/core/graph-contracts/run.sh: deploys REO, +# IssuanceAllocator, RecurringAgreementManager; runs activation goals; +# grants ORACLE_ROLE; sets eligibility validation + period + timeout. +GIP0088_ENABLED=1 +# eligibilityPeriod: how long an indexer stays eligible after renewal (seconds). +REO_ELIGIBILITY_PERIOD=300 +# oracleUpdateTimeout: fail-safe — if no oracle update for this long, all +# indexers eligible (seconds). +REO_ORACLE_UPDATE_TIMEOUT=86400 + +# --- Indexing-payments toggle --- +# Gates the [dips] section in indexer-service config.toml. Off in baseline +# (upstream v2.1.0 indexer-rs doesn't recognise the DIPs schema). Set to 1 +# in indexing-payments.env, where the dips-fork indexer-rs is also pinned. +INDEXING_PAYMENTS_ENABLED=0 diff --git a/containers/core/gateway/Dockerfile b/containers/core/gateway/Dockerfile index 47d4a631..b29200c5 100644 --- a/containers/core/gateway/Dockerfile +++ b/containers/core/gateway/Dockerfile @@ -1,15 +1,16 @@ -FROM debian:bookworm-slim -ARG GATEWAY_COMMIT +# check=skip=InvalidDefaultArgInFrom +ARG GATEWAY_VERSION +FROM ghcr.io/edgeandnode/graph-gateway:${GATEWAY_VERSION} +# Tools needed by run.sh (config generation, wait_for_gql) RUN apt-get update \ - && apt-get install -y clang cmake curl git jq libsasl2-dev libssl-dev pkg-config protobuf-compiler \ + && apt-get install -y --no-install-recommends curl jq ca-certificates \ && rm -rf /var/lib/apt/lists/* -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal + +# Upstream ENTRYPOINT is target/release/graph-gateway relative to /opt/gateway; +# expose on PATH so run.sh can invoke `graph-gateway` directly. +RUN ln -sf /opt/gateway/target/release/graph-gateway /usr/local/bin/graph-gateway WORKDIR /opt -RUN git clone https://github.com/edgeandnode/gateway && \ - cd gateway && git checkout ${GATEWAY_COMMIT} && \ - . /root/.cargo/env && cargo build -p graph-gateway && \ - cp target/debug/graph-gateway /usr/local/bin/graph-gateway && cd .. && rm -rf gateway COPY ./run.sh /opt/run.sh ENTRYPOINT ["bash", "/opt/run.sh"] diff --git a/containers/core/gateway/run.sh b/containers/core/gateway/run.sh index 6b299f2e..069db2d5 100755 --- a/containers/core/gateway/run.sh +++ b/containers/core/gateway/run.sh @@ -28,6 +28,7 @@ cat >config.json <<-EOF ], "exchange_rate_provider": 1.0, "graph_env_id": "local", + "kafka_topic_environment": "${KAFKA_TOPIC_ENVIRONMENT:-}", "indexer_selection_retry_limit": 2, "kafka": { "bootstrap.servers": "redpanda:9092" @@ -48,17 +49,11 @@ cat >config.json <<-EOF "query_fees_target": 40e-6, "receipts": { "chain_id": "1337", - "payer": "${ACCOUNT0_ADDRESS}", - "signer": "${ACCOUNT1_SECRET}", + "payer": "${DEPLOYER_ADDRESS}", + "signer": "${GOVERNOR_SECRET}", "verifier": "${graph_tally_verifier}" }, - "subgraph_service": "${subgraph_service}", - "x402": { - "facilitator_url": "https://x402.org/facilitator", - "receiver_address": "${ACCOUNT_X402_ADDRESS}", - "chain": "base_sepolia", - "price": 42e-6 - } + "subgraph_service": "${subgraph_service}" } EOF cat config.json diff --git a/containers/core/graph-contracts/Dockerfile b/containers/core/graph-contracts/Dockerfile index e051901f..617cd139 100644 --- a/containers/core/graph-contracts/Dockerfile +++ b/containers/core/graph-contracts/Dockerfile @@ -21,19 +21,15 @@ COPY --from=ghcr.io/foundry-rs/foundry:stable \ WORKDIR /opt -# 1. Graph protocol contracts (Horizon) -# Install/build commands mirror upstream CI (see contracts repo's -# .github/actions/setup/action.yml and .github/workflows/build-test.yml). +# Graph protocol contracts workspace (Horizon + DataEdge in one workspace). +# Install/build commands mirror upstream CI (see contracts repo's +# .github/actions/setup/action.yml and .github/workflows/build-test.yml). +# The workspace `pnpm build` produces both the Horizon and DataEdge artifacts; +# run.sh deploys DataEdge from /opt/contracts/packages/data-edge at runtime +# (so the localhost→chain + mnemonic patches stay out of the build cache). RUN git clone https://github.com/graphprotocol/contracts && \ cd contracts && git checkout ${CONTRACTS_COMMIT} && \ pnpm install --frozen-lockfile && pnpm build -# 2. DataEdge contracts (fixed commit, for block-oracle setup) -RUN git clone https://github.com/graphprotocol/contracts contracts-data-edge && \ - cd contracts-data-edge && git checkout bdc66135e7700e9a4dcd6a4beac585337fdb9c21 && \ - cd packages/data-edge && pnpm install && \ - sed -i "s/localhost/chain/g" hardhat.config.ts && \ - pnpm build - COPY --chmod=755 ./run.sh /opt/run.sh ENTRYPOINT ["bash", "/opt/run.sh"] diff --git a/containers/core/graph-contracts/run.sh b/containers/core/graph-contracts/run.sh index 541c356d..b52cc6d9 100644 --- a/containers/core/graph-contracts/run.sh +++ b/containers/core/graph-contracts/run.sh @@ -44,7 +44,7 @@ ensure_dispute_manager_registered() { echo " Controller: ${controller_address}" echo " DisputeManager: ${dispute_manager_address}" echo " Current proxy: ${current_proxy}" - cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" --confirmations=0 --private-key="${ACCOUNT1_SECRET}" \ + cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" --confirmations=0 --private-key="${GOVERNOR_SECRET}" \ "${controller_address}" "setContractProxy(bytes32,address)" "${dispute_manager_id}" "${dispute_manager_address}" fi } @@ -90,7 +90,7 @@ if [ -n "$rewards_manager" ]; then else echo " Setting issuancePerBlock to 100 GRT (was ${current_issuance})" cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" --confirmations=0 \ - --private-key="${ACCOUNT1_SECRET}" \ + --private-key="${GOVERNOR_SECRET}" \ "${rewards_manager}" "setIssuancePerBlock(uint256)" "${target_issuance}" fi fi @@ -152,138 +152,238 @@ fi echo "==== Phase 2 complete ====" # ============================================================ -# Phase 3: Rewards Eligibility Oracle (REO) +# Phase 3: GIP-0088 — REO + IssuanceAllocator + RecurringAgreementManager # ============================================================ -if [ "${REO_ENABLED:-0}" != "1" ]; then - echo "==== Phase 3: Rewards Eligibility Oracle (SKIPPED — REO_ENABLED not set) ====" +if [ "${GIP0088_ENABLED:-0}" != "1" ]; then + echo "==== Phase 3: GIP-0088 (SKIPPED — GIP0088_ENABLED not set) ====" else -echo "==== Phase 3: Rewards Eligibility Oracle ====" +echo "==== Phase 3: GIP-0088 ====" -# Ensure NetworkOperator in issuance address book (required by configure step) -TEMP_JSON=$(jq --arg op "${ACCOUNT0_ADDRESS}" \ +# Address-book entries that the GIP-0088 configure step reads: +# NetworkOperator → granted OPERATOR_ROLE on REO. Use OPERATOR_ADDRESS so +# the operator's nonce queue is independent of the deployer's. +TEMP_JSON=$(jq --arg op "${OPERATOR_ADDRESS}" \ '.["1337"].NetworkOperator = {"address": $op}' /opt/config/issuance.json) printf '%s\n' "$TEMP_JSON" > /opt/config/issuance.json -# -- Idempotency check -- -# The hardhat deploy configure step (04_configure.ts) targets REO_DEFAULTS -# (14d eligibility, 7d timeout) using the GOVERNOR account, which lacks -# OPERATOR_ROLE. run.sh below handles all configuration using ACCOUNT0 -# (OPERATOR). So we only run hardhat deploy for initial deployment; on -# re-runs where the REO proxy already exists on-chain, skip straight to -# the idempotent configuration below. -phase3_deploy_skip=false -reo_address=$(jq -r '.["1337"].RewardsEligibilityOracle.address // empty' /opt/config/issuance.json 2>/dev/null || true) -if [ -n "$reo_address" ]; then - code_check=$(cast code --rpc-url="http://chain:${CHAIN_RPC_PORT}" "$reo_address" 2>/dev/null || echo "0x") - if [ "$code_check" != "0x" ]; then - echo "REO already deployed at $reo_address" - echo "SKIP: hardhat deploy (configuration handled below)" - phase3_deploy_skip=true +# Controller.pauseGuardian → granted PAUSE_ROLE on REO by the configure step +# (read from Controller.pauseGuardian() at configure time, not from address +# book). Set it to PAUSE_ADMIN_ADDRESS before Phase 4 runs so PAUSE_ROLE +# lands on a dedicated account. +controller_address=$(jq -r '.["1337"].Controller.address // empty' /opt/config/horizon.json 2>/dev/null || true) +if [ -n "${controller_address}" ]; then + current_guardian=$(cast call --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ + "${controller_address}" "pauseGuardian()(address)" 2>/dev/null || echo "0x") + current_lc=$(echo "$current_guardian" | tr '[:upper:]' '[:lower:]') + target_lc=$(echo "$PAUSE_ADMIN_ADDRESS" | tr '[:upper:]' '[:lower:]') + if [ "$current_lc" = "$target_lc" ]; then + echo "Controller pauseGuardian already ${PAUSE_ADMIN_ADDRESS}" else - echo "REO address stale (no code at $reo_address), redeploying..." + echo "Setting Controller pauseGuardian to ${PAUSE_ADMIN_ADDRESS} (was ${current_guardian})" + cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" --confirmations=0 \ + --private-key="${GOVERNOR_SECRET}" \ + "${controller_address}" "setPauseGuardian(address)" "${PAUSE_ADMIN_ADDRESS}" fi fi -if [ "$phase3_deploy_skip" = "false" ]; then +# -- Idempotency check -- +# Skip the whole deployment when REO + IA + RAM are all on-chain and IA is +# wired as a minter on GraphToken (proves the activation goals ran). +phase3_skip=false +ram_address=$(jq -r '.["1337"].RecurringAgreementManager.address // empty' /opt/config/issuance.json 2>/dev/null || true) +ia_address=$(jq -r '.["1337"].IssuanceAllocator.address // empty' /opt/config/issuance.json 2>/dev/null || true) +reo_address=$(jq -r '.["1337"].RewardsEligibilityOracleA.address // empty' /opt/config/issuance.json 2>/dev/null || true) +if [ -n "$ram_address" ] && [ -n "$ia_address" ] && [ -n "$reo_address" ]; then + ram_code=$(cast code --rpc-url="http://chain:${CHAIN_RPC_PORT}" "$ram_address" 2>/dev/null || echo "0x") + ia_code=$(cast code --rpc-url="http://chain:${CHAIN_RPC_PORT}" "$ia_address" 2>/dev/null || echo "0x") + reo_code=$(cast code --rpc-url="http://chain:${CHAIN_RPC_PORT}" "$reo_address" 2>/dev/null || echo "0x") + if [ "$ram_code" != "0x" ] && [ "$ia_code" != "0x" ] && [ "$reo_code" != "0x" ]; then + graph_token=$(contract_addr L2GraphToken.address horizon) + ia_is_minter=$(cast call --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ + "${graph_token}" "isMinter(address)(bool)" "${ia_address}" 2>/dev/null || echo "false") + if [ "$ia_is_minter" = "true" ]; then + echo "GIP-0088 contracts already deployed and activated" + echo " REO: $reo_address" + echo " IA: $ia_address" + echo " RAM: $ram_address" + phase3_skip=true + fi + fi +fi + +if [ "$phase3_skip" = "false" ]; then cd /opt/contracts/packages/deployment - # Clean any stale governance TX batches from partial runs + # Clean stale deployment state from previous localNetwork runs rm -rf /opt/contracts/packages/deployment/txs/localNetwork - - # Full REO lifecycle via deployment package tags: - # sync → deploy → configure → transfer → integrate → verify - # Deploy scripts are idempotent (skip if already deployed/configured). - # The mnemonic provides both deployer (ACCOUNT0) and governor (ACCOUNT1), - # so all steps including RM integration execute directly. - # - # Some steps (upgrade) exit with code 1 after saving governance TX batches. - # On localNetwork, the governor key is available so we auto-execute and retry. - export GOVERNOR_KEY="${ACCOUNT1_SECRET}" - for attempt in 1 2 3; do - echo " Deploy attempt $attempt..." - if npx hardhat deploy --tags rewards-eligibility --network localNetwork --skip-prompts; then - break + rm -rf /opt/contracts/packages/deployment/deployments/localNetwork + + # On localNetwork the governor key is available, so governance TXs + # auto-execute via deploy:execute-governance. + export GOVERNOR_KEY="${GOVERNOR_SECRET}" + + # -- GIP-0088 Upgrade Phase -- + # Deploy → configure → transfer → upgrade. Each step is idempotent and may + # produce governance TXs that need executing before the next attempt. + for step in \ + "GIP-0088:upgrade,deploy" \ + "GIP-0088:upgrade,configure" \ + "GIP-0088:upgrade,transfer" \ + "GIP-0088:upgrade,upgrade"; do + echo " --- Running: --tags ${step} ---" + for attempt in 1 2 3; do + if pnpm exec hardhat deploy --tags "${step}" --network localNetwork --skip-prompts; then + break + fi + if ls /opt/contracts/packages/deployment/txs/localNetwork/*.json 2>/dev/null | grep -qv executed; then + echo " Executing pending governance TXs..." + pnpm exec hardhat deploy:execute-governance --network localNetwork || true + else + echo " Deploy step failed (no governance TXs pending)" + exit 1 + fi + done + if ls /opt/contracts/packages/deployment/txs/localNetwork/*.json 2>/dev/null | grep -qv executed; then + echo " Executing governance TXs..." + pnpm exec hardhat deploy:execute-governance --network localNetwork || true fi - # Check for pending governance TXs and execute them + done + + # -- GIP-0088 Activation Goals -- + # Each goal generates governance TXs independently; execute after each. + for goal in \ + "GIP-0088:eligibility-integrate" \ + "GIP-0088:issuance-connect" \ + "GIP-0088:issuance-allocate"; do + echo " --- Running: --tags ${goal} ---" + for attempt in 1 2 3; do + if pnpm exec hardhat deploy --tags "${goal}" --network localNetwork --skip-prompts; then + break + fi + if ls /opt/contracts/packages/deployment/txs/localNetwork/*.json 2>/dev/null | grep -qv executed; then + echo " Executing pending governance TXs..." + pnpm exec hardhat deploy:execute-governance --network localNetwork || true + else + echo " Activation goal failed (no governance TXs pending)" + exit 1 + fi + done if ls /opt/contracts/packages/deployment/txs/localNetwork/*.json 2>/dev/null | grep -qv executed; then - echo " Executing pending governance TXs..." - npx hardhat deploy:execute-governance --network localNetwork || true - else - echo " No governance TXs to execute, deployment failed for another reason" - exit 1 + echo " Executing governance TXs..." + pnpm exec hardhat deploy:execute-governance --network localNetwork || true fi done - # Read deployed REO address from issuance address book - reo_address=$(jq -r '.["1337"].RewardsEligibilityOracle.address' /opt/config/issuance.json) + # Read deployed addresses + reo_address=$(jq -r '.["1337"].RewardsEligibilityOracleA.address' /opt/config/issuance.json) + ia_address=$(jq -r '.["1337"].IssuanceAllocator.address' /opt/config/issuance.json) + ram_address=$(jq -r '.["1337"].RecurringAgreementManager.address' /opt/config/issuance.json) fi -echo " REO deployed at: $reo_address" - -# Grant ORACLE_ROLE to the REO node signing key (ACCOUNT0). -# OPERATOR_ROLE is the admin for ORACLE_ROLE, and ACCOUNT0 has OPERATOR_ROLE. -# Idempotent: only grants if not already granted. -oracle_role=$(cast call --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ - "${reo_address}" "ORACLE_ROLE()(bytes32)") -has_role=$(cast call --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ - "${reo_address}" "hasRole(bytes32,address)(bool)" "${oracle_role}" "${ACCOUNT0_ADDRESS}" 2>/dev/null || echo "false") -if [ "$has_role" = "true" ]; then - echo " ORACLE_ROLE already granted to ${ACCOUNT0_ADDRESS}" -else - echo " Granting ORACLE_ROLE to ${ACCOUNT0_ADDRESS} (via OPERATOR_ROLE)" - cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" --confirmations=0 \ - --private-key="${ACCOUNT0_SECRET}" \ - "${reo_address}" "grantRole(bytes32,address)" "${oracle_role}" "${ACCOUNT0_ADDRESS}" -fi +echo " REO deployed at: ${reo_address:-}" +echo " IA deployed at: ${ia_address:-}" +echo " RAM deployed at: ${ram_address:-}" + +# -- REO local-network ORACLE_ROLE grant (deployment-package gap) -- +# The GIP-0088 configure step grants GOVERNOR_ROLE / OPERATOR_ROLE (via +# NetworkOperator address-book entry, set above) and PAUSE_ROLE (via +# Controller.pauseGuardian, set above). It does NOT grant ORACLE_ROLE — +# `createREORoleConditions` in @graphprotocol/deployment omits it. Until that +# upstream gap is fixed, grant it here, signed by OPERATOR_SECRET (which now +# holds OPERATOR_ROLE, the admin of ORACLE_ROLE per RewardsEligibilityOracle.sol:125). +# Also keep DEPLOYER's existing ORACLE_ROLE grant for transitional compatibility +# with services (eligibility-oracle-node, dipper) that may still sign with it. +if [ -n "${reo_address:-}" ]; then + oracle_role=$(cast call --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ + "${reo_address}" "ORACLE_ROLE()(bytes32)") + + for grantee in "${ORACLE_ADDRESS}" "${DEPLOYER_ADDRESS}"; do + has=$(cast call --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ + "${reo_address}" "hasRole(bytes32,address)(bool)" "${oracle_role}" "${grantee}" 2>/dev/null || echo "false") + if [ "$has" = "true" ]; then + echo " ORACLE_ROLE already granted to ${grantee}" + else + echo " Granting ORACLE_ROLE to ${grantee} (signed by OPERATOR)" + cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" --confirmations=0 \ + --private-key="${OPERATOR_SECRET}" \ + "${reo_address}" "grantRole(bytes32,address)" "${oracle_role}" "${grantee}" + fi + done -# Enable eligibility validation (deny-by-default). -# The contract defaults to validation disabled (everyone eligible). For local -# testing we want the realistic deny-by-default behaviour. Idempotent. -# Requires OPERATOR_ROLE (ACCOUNT0). -validation_enabled=$(cast call --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ - "${reo_address}" "getEligibilityValidation()(bool)" 2>/dev/null || echo "false") -if [ "$validation_enabled" = "true" ]; then - echo " Eligibility validation already enabled" -else - echo " Enabling eligibility validation (deny-by-default)" - cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" --confirmations=0 \ - --private-key="${ACCOUNT0_SECRET}" \ - "${reo_address}" "setEligibilityValidation(bool)" true -fi + # Enable eligibility validation (deny-by-default). + validation_enabled=$(cast call --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ + "${reo_address}" "getEligibilityValidation()(bool)" 2>/dev/null || echo "false") + if [ "$validation_enabled" = "true" ]; then + echo " Eligibility validation already enabled" + else + echo " Enabling eligibility validation (deny-by-default)" + cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" --confirmations=0 \ + --private-key="${OPERATOR_SECRET}" \ + "${reo_address}" "setEligibilityValidation(bool)" true + fi -# Set eligibility period (how long an indexer stays eligible after renewal). -# Contract default is 14 days; local network uses a short value for fast iteration. -# Requires OPERATOR_ROLE (ACCOUNT0). -current_period=$(cast call --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ - "${reo_address}" "getEligibilityPeriod()(uint256)" 2>/dev/null | awk '{print $1}') -if [ "$current_period" = "${REO_ELIGIBILITY_PERIOD}" ]; then - echo " Eligibility period already set to ${REO_ELIGIBILITY_PERIOD}s" -else - echo " Setting eligibility period to ${REO_ELIGIBILITY_PERIOD}s (was ${current_period}s)" - cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" --confirmations=0 \ - --private-key="${ACCOUNT0_SECRET}" \ - "${reo_address}" "setEligibilityPeriod(uint256)" "${REO_ELIGIBILITY_PERIOD}" + # Set eligibility period (short value for fast iteration). + current_period=$(cast call --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ + "${reo_address}" "getEligibilityPeriod()(uint256)" 2>/dev/null | awk '{print $1}') + if [ "$current_period" = "${REO_ELIGIBILITY_PERIOD}" ]; then + echo " Eligibility period already set to ${REO_ELIGIBILITY_PERIOD}s" + else + echo " Setting eligibility period to ${REO_ELIGIBILITY_PERIOD}s (was ${current_period}s)" + cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" --confirmations=0 \ + --private-key="${OPERATOR_SECRET}" \ + "${reo_address}" "setEligibilityPeriod(uint256)" "${REO_ELIGIBILITY_PERIOD}" + fi + + # Set oracle update timeout (long value to avoid accidental fail-safe). + current_timeout=$(cast call --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ + "${reo_address}" "getOracleUpdateTimeout()(uint256)" 2>/dev/null | awk '{print $1}') + if [ "$current_timeout" = "${REO_ORACLE_UPDATE_TIMEOUT}" ]; then + echo " Oracle update timeout already set to ${REO_ORACLE_UPDATE_TIMEOUT}s" + else + echo " Setting oracle update timeout to ${REO_ORACLE_UPDATE_TIMEOUT}s (was ${current_timeout}s)" + cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" --confirmations=0 \ + --private-key="${OPERATOR_SECRET}" \ + "${reo_address}" "setOracleUpdateTimeout(uint256)" "${REO_ORACLE_UPDATE_TIMEOUT}" + fi fi -# Set oracle update timeout (fail-safe: all indexers eligible if no oracle update for this long). -# Contract default is 7 days; local network uses a longer value to avoid accidental fail-safe. -# Requires OPERATOR_ROLE (ACCOUNT0). -current_timeout=$(cast call --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ - "${reo_address}" "getOracleUpdateTimeout()(uint256)" 2>/dev/null | awk '{print $1}') -if [ "$current_timeout" = "${REO_ORACLE_UPDATE_TIMEOUT}" ]; then - echo " Oracle update timeout already set to ${REO_ORACLE_UPDATE_TIMEOUT}s" +# -- Optional: wire MockRewardsEligibilityOracle as RM's providerEligibilityOracle -- +# REO_MOCK=1 (default in .env) replaces REO-A with the mock so tests can +# self-toggle eligibility (mock.setEligible(bool) signed by indexer's own key). +# REO_MOCK=0 keeps REO-A wired (production-like). +# +# Direct cast send rather than the deployment package's +# `RewardsEligibilityOracleMock,integrate` task because that task is missing +# the syncComponentsFromRegistry call its REO-A counterpart has, so it fails +# with "RewardsManager not deployed". See task notes for the upstream fix. +if [ "${REO_MOCK:-1}" = "1" ]; then + rm_address=$(jq -r '.["1337"].RewardsManager.address // empty' /opt/config/horizon.json 2>/dev/null || true) + mock_address=$(jq -r '.["1337"].RewardsEligibilityOracleMock.address // empty' /opt/config/issuance.json 2>/dev/null || true) + if [ -z "${rm_address}" ] || [ -z "${mock_address}" ]; then + echo " REO_MOCK=1 set but RewardsManager or RewardsEligibilityOracleMock address missing — skipping wire" + else + current_oracle=$(cast call --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ + "${rm_address}" "getProviderEligibilityOracle()(address)" 2>/dev/null || echo "0x") + current_lc=$(echo "$current_oracle" | tr '[:upper:]' '[:lower:]') + target_lc=$(echo "$mock_address" | tr '[:upper:]' '[:lower:]') + if [ "$current_lc" = "$target_lc" ]; then + echo " RewardsManager.providerEligibilityOracle already points at mock (${mock_address})" + else + echo " Wiring RewardsManager.providerEligibilityOracle to mock ${mock_address} (was ${current_oracle})" + cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" --confirmations=0 \ + --private-key="${GOVERNOR_SECRET}" \ + "${rm_address}" "setProviderEligibilityOracle(address)" "${mock_address}" + fi + fi else - echo " Setting oracle update timeout to ${REO_ORACLE_UPDATE_TIMEOUT}s (was ${current_timeout}s)" - cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" --confirmations=0 \ - --private-key="${ACCOUNT0_SECRET}" \ - "${reo_address}" "setOracleUpdateTimeout(uint256)" "${REO_ORACLE_UPDATE_TIMEOUT}" + echo " REO_MOCK=0 — keeping RewardsEligibilityOracleA wired to RewardsManager" fi # Clean deployment metadata from address books. # The deployment package writes fields like implementationDeployment and # proxyDeployment that the indexer-agent doesn't recognise, causing it to # crash with "Address book entry contains invalid fields". -for ab in horizon.json subgraph-service.json; do +for ab in horizon.json subgraph-service.json issuance.json; do if [ -f "/opt/config/$ab" ]; then TEMP_JSON=$(jq 'walk(if type == "object" then del(.implementationDeployment, .proxyDeployment) else . end)' "/opt/config/$ab") printf '%s\n' "$TEMP_JSON" > "/opt/config/$ab" @@ -291,7 +391,7 @@ for ab in horizon.json subgraph-service.json; do done echo "==== Phase 3 complete ====" -fi # REO_ENABLED +fi # GIP0088_ENABLED echo "==== All contract deployments complete ====" # Optional: keep container running for debugging diff --git a/containers/core/subgraph-deploy/Dockerfile b/containers/core/subgraph-deploy/Dockerfile index 611fcafd..7ba63674 100644 --- a/containers/core/subgraph-deploy/Dockerfile +++ b/containers/core/subgraph-deploy/Dockerfile @@ -1,3 +1,7 @@ +# check=skip=InvalidDefaultArgInFrom +ARG INDEXING_PAYMENTS_SUBGRAPH_VERSION +FROM ghcr.io/graphprotocol/indexing-payments-subgraph:${INDEXING_PAYMENTS_SUBGRAPH_VERSION} AS indexing-payments-src + FROM node:23.11-bookworm-slim ARG NETWORK_SUBGRAPH_COMMIT ARG BLOCK_ORACLE_COMMIT @@ -28,5 +32,9 @@ RUN git clone https://github.com/graphprotocol/block-oracle && \ cd block-oracle && git checkout ${BLOCK_ORACLE_COMMIT} && \ cd packages/subgraph && yarn +# 4. Indexing-payments subgraph — source tree + node_modules, copied from +# the prebuilt image pinned by INDEXING_PAYMENTS_SUBGRAPH_VERSION. +COPY --from=indexing-payments-src /opt/indexing-payments-subgraph /opt/indexing-payments-subgraph + COPY --chmod=755 ./run.sh /opt/run.sh ENTRYPOINT ["bash", "/opt/run.sh"] diff --git a/containers/core/subgraph-deploy/run.sh b/containers/core/subgraph-deploy/run.sh index 0d3d08ab..3e8bbdba 100644 --- a/containers/core/subgraph-deploy/run.sh +++ b/containers/core/subgraph-deploy/run.sh @@ -40,6 +40,42 @@ deploy_network() { echo "==== Network subgraph done ====" } +deploy_indexing_payments() { + echo "==== Indexing-payments subgraph ====" + if curl -s "http://graph-node:${GRAPH_NODE_GRAPHQL_PORT}/subgraphs/name/indexing-payments" \ + -H 'content-type: application/json' \ + -d '{"query": "{ _meta { deployment } }" }' | grep -q "_meta" + then + echo "SKIP: Indexing-payments subgraph already deployed" + return + fi + + subgraph_service=$(contract_addr SubgraphService.address subgraph-service) + recurring_collector=$(contract_addr RecurringCollector.address horizon) + + cd /opt/indexing-payments-subgraph + cat > /tmp/indexing-payments-config.json <<-CONF + { + "network": "hardhat", + "subgraphServiceAddress": "${subgraph_service}", + "recurringCollectorAddress": "${recurring_collector}", + "startBlock": 0 + } + CONF + npx mustache /tmp/indexing-payments-config.json subgraph.template.yaml > subgraph.yaml + npx graph codegen + npx graph build + npx graph create indexing-payments --node="http://graph-node:${GRAPH_NODE_ADMIN_PORT}" + npx graph deploy indexing-payments --node="http://graph-node:${GRAPH_NODE_ADMIN_PORT}" --ipfs="http://ipfs:${IPFS_RPC_PORT}" --version-label=v0.1.0 | tee deploy.txt + # Without subgraph_reassign, graph-node leaves the deployment unassigned + # and the subgraph never starts — dipper's chain_listener would stall. + deployment_id="$(grep "Build completed: " deploy.txt | awk '{print $3}' | sed -e 's/\x1b\[[0-9;]*m//g')" + curl -s "http://graph-node:${GRAPH_NODE_ADMIN_PORT}" \ + -H 'content-type: application/json' \ + -d "{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"method\":\"subgraph_reassign\",\"params\":{\"node_id\":\"default\",\"ipfs_hash\":\"${deployment_id}\"}}" + echo "==== Indexing-payments subgraph done ====" +} + deploy_block_oracle() { echo "==== Block-oracle subgraph ====" if curl -s "http://graph-node:${GRAPH_NODE_GRAPHQL_PORT}/subgraphs/name/block-oracle" \ @@ -74,16 +110,19 @@ deploy_block_oracle() { echo "==== Block-oracle subgraph done ====" } -# Launch in parallel +# Launch all in parallel deploy_network & pid_network=$! deploy_block_oracle & pid_oracle=$! +deploy_indexing_payments & +pid_indexing_payments=$! # Wait for all, fail if any fails failed=0 wait $pid_network || { echo "FAILED: Network subgraph"; failed=1; } wait $pid_oracle || { echo "FAILED: Block-oracle subgraph"; failed=1; } +wait $pid_indexing_payments || { echo "FAILED: Indexing-payments subgraph"; failed=1; } if [ "$failed" -ne 0 ]; then echo "One or more subgraph deployments failed" diff --git a/containers/indexer/indexer-agent/dev/run-override.sh b/containers/indexer/indexer-agent/dev/run-override.sh index 07d5cba6..4d376226 100755 --- a/containers/indexer/indexer-agent/dev/run-override.sh +++ b/containers/indexer/indexer-agent/dev/run-override.sh @@ -7,19 +7,19 @@ set -xeu token_address=$(contract_addr L2GraphToken.address horizon) staking_address=$(contract_addr HorizonStaking.address horizon) indexer_staked="$(cast call "--rpc-url=http://chain:${CHAIN_RPC_PORT}" \ - "${staking_address}" 'hasStake(address) (bool)' "${RECEIVER_ADDRESS}")" + "${staking_address}" 'hasStake(address) (bool)' "${INDEXER_ADDRESS}")" echo "indexer_staked=${indexer_staked}" if [ "${indexer_staked}" = "false" ]; then # transfer ETH to receiver cast send "--rpc-url=http://chain:${CHAIN_RPC_PORT}" --confirmations=0 "--mnemonic=${MNEMONIC}" \ - --value=1ether "${RECEIVER_ADDRESS}" + --value=1ether "${INDEXER_ADDRESS}" # transfer 100,000 GRT to receiver cast send "--rpc-url=http://chain:${CHAIN_RPC_PORT}" --confirmations=0 "--mnemonic=${MNEMONIC}" \ - "${token_address}" 'transfer(address,uint256)' "${RECEIVER_ADDRESS}" '100000000000000000000000' + "${token_address}" 'transfer(address,uint256)' "${INDEXER_ADDRESS}" '100000000000000000000000' # stake required GRT for indexer registration - cast send "--rpc-url=http://chain:${CHAIN_RPC_PORT}" --confirmations=0 "--private-key=${RECEIVER_SECRET}" \ + cast send "--rpc-url=http://chain:${CHAIN_RPC_PORT}" --confirmations=0 "--private-key=${INDEXER_SECRET}" \ "${token_address}" 'approve(address,uint256)' "${staking_address}" '100000000000000000000000' - cast send "--rpc-url=http://chain:${CHAIN_RPC_PORT}" --confirmations=0 "--private-key=${RECEIVER_SECRET}" \ + cast send "--rpc-url=http://chain:${CHAIN_RPC_PORT}" --confirmations=0 "--private-key=${INDEXER_SECRET}" \ "${staking_address}" 'stake(uint256)' '100000000000000000000000' fi @@ -31,7 +31,7 @@ export INDEXER_AGENT_GRAPH_NODE_QUERY_ENDPOINT="http://graph-node:${GRAPH_NODE_G export INDEXER_AGENT_GRAPH_NODE_ADMIN_ENDPOINT="http://graph-node:${GRAPH_NODE_ADMIN_PORT}" export INDEXER_AGENT_GRAPH_NODE_STATUS_ENDPOINT="http://graph-node:${GRAPH_NODE_STATUS_PORT}/graphql" export INDEXER_AGENT_IPFS_ENDPOINT="http://ipfs:${IPFS_RPC_PORT}" -export INDEXER_AGENT_INDEXER_ADDRESS="${RECEIVER_ADDRESS}" +export INDEXER_AGENT_INDEXER_ADDRESS="${INDEXER_ADDRESS}" export INDEXER_AGENT_INDEXER_MANAGEMENT_PORT="${INDEXER_MANAGEMENT_PORT}" export INDEXER_AGENT_INDEX_NODE_IDS=default export INDEXER_AGENT_INDEXER_GEO_COORDINATES="1 1" diff --git a/containers/indexer/indexer-agent/run.sh b/containers/indexer/indexer-agent/run.sh index 4bf148e8..159b1d5d 100755 --- a/containers/indexer/indexer-agent/run.sh +++ b/containers/indexer/indexer-agent/run.sh @@ -1,25 +1,37 @@ #!/bin/sh set -eu + +# In test-indexer compose projects, IndexerHandle injects per-test indexer identity +# via TEST_INDEXER_* env vars. Capture them before sourcing .env (which would +# otherwise overwrite them with the production-default INDEXER_* values). +__test_indexer_address="${TEST_INDEXER_ADDRESS:-}" +__test_indexer_secret="${TEST_INDEXER_SECRET:-}" +__test_indexer_mnemonic="${TEST_INDEXER_MNEMONIC:-}" + . /opt/config/.env +[ -n "${__test_indexer_address}" ] && INDEXER_ADDRESS="${__test_indexer_address}" +[ -n "${__test_indexer_secret}" ] && INDEXER_SECRET="${__test_indexer_secret}" +[ -n "${__test_indexer_mnemonic}" ] && INDEXER_MNEMONIC="${__test_indexer_mnemonic}" + . /opt/shared/lib.sh token_address=$(contract_addr L2GraphToken.address horizon) staking_address=$(contract_addr HorizonStaking.address horizon) -indexer_staked="$(cast call "--rpc-url=http://chain:${CHAIN_RPC_PORT}" \ - "${staking_address}" 'hasStake(address) (bool)' "${RECEIVER_ADDRESS}")" -echo "indexer_staked=${indexer_staked}" -if [ "${indexer_staked}" = "false" ]; then +indexer_stake="$(cast call "--rpc-url=http://chain:${CHAIN_RPC_PORT}" \ + "${staking_address}" 'getStake(address)(uint256)' "${INDEXER_ADDRESS}")" +echo "indexer_stake=${indexer_stake}" +if [ "${indexer_stake}" = "0" ]; then # transfer ETH to receiver cast send "--rpc-url=http://chain:${CHAIN_RPC_PORT}" --confirmations=0 "--mnemonic=${MNEMONIC}" \ - --value=1ether "${RECEIVER_ADDRESS}" + --value=1ether "${INDEXER_ADDRESS}" # transfer 100,000 GRT to receiver cast send "--rpc-url=http://chain:${CHAIN_RPC_PORT}" --confirmations=0 "--mnemonic=${MNEMONIC}" \ - "${token_address}" 'transfer(address,uint256)' "${RECEIVER_ADDRESS}" '100000000000000000000000' + "${token_address}" 'transfer(address,uint256)' "${INDEXER_ADDRESS}" '100000000000000000000000' # stake required GRT for indexer registration - cast send "--rpc-url=http://chain:${CHAIN_RPC_PORT}" --confirmations=0 "--private-key=${RECEIVER_SECRET}" \ + cast send "--rpc-url=http://chain:${CHAIN_RPC_PORT}" --confirmations=0 "--private-key=${INDEXER_SECRET}" \ "${token_address}" 'approve(address,uint256)' "${staking_address}" '100000000000000000000000' - cast send "--rpc-url=http://chain:${CHAIN_RPC_PORT}" --confirmations=0 "--private-key=${RECEIVER_SECRET}" \ + cast send "--rpc-url=http://chain:${CHAIN_RPC_PORT}" --confirmations=0 "--private-key=${INDEXER_SECRET}" \ "${staking_address}" 'stake(uint256)' '100000000000000000000000' fi @@ -28,13 +40,13 @@ fi subgraph_service_address=$(contract_addr SubgraphService.address subgraph-service) operator_authorized="$(cast call "--rpc-url=http://chain:${CHAIN_RPC_PORT}" \ "${staking_address}" 'isAuthorized(address,address,address)(bool)' \ - "${RECEIVER_ADDRESS}" "${RECEIVER_ADDRESS}" "${subgraph_service_address}")" + "${INDEXER_ADDRESS}" "${INDEXER_ADDRESS}" "${subgraph_service_address}")" echo "operator_authorized=${operator_authorized}" if [ "${operator_authorized}" = "false" ]; then echo "Authorizing indexer as operator for SubgraphService..." - cast send "--rpc-url=http://chain:${CHAIN_RPC_PORT}" --confirmations=0 "--private-key=${RECEIVER_SECRET}" \ + cast send "--rpc-url=http://chain:${CHAIN_RPC_PORT}" --confirmations=0 "--private-key=${INDEXER_SECRET}" \ "${staking_address}" 'setOperator(address,address,bool)' \ - "${RECEIVER_ADDRESS}" "${subgraph_service_address}" "true" + "${INDEXER_ADDRESS}" "${subgraph_service_address}" "true" fi export INDEXER_AGENT_HORIZON_ADDRESS_BOOK=/opt/config/horizon.json @@ -45,7 +57,7 @@ export INDEXER_AGENT_GRAPH_NODE_QUERY_ENDPOINT="http://graph-node:${GRAPH_NODE_G export INDEXER_AGENT_GRAPH_NODE_ADMIN_ENDPOINT="http://graph-node:${GRAPH_NODE_ADMIN_PORT}" export INDEXER_AGENT_GRAPH_NODE_STATUS_ENDPOINT="http://graph-node:${GRAPH_NODE_STATUS_PORT}/graphql" export INDEXER_AGENT_IPFS_ENDPOINT="http://ipfs:${IPFS_RPC_PORT}" -export INDEXER_AGENT_INDEXER_ADDRESS="${RECEIVER_ADDRESS}" +export INDEXER_AGENT_INDEXER_ADDRESS="${INDEXER_ADDRESS}" export INDEXER_AGENT_INDEXER_MANAGEMENT_PORT="${INDEXER_MANAGEMENT_PORT}" export INDEXER_AGENT_INDEX_NODE_IDS=default export INDEXER_AGENT_INDEXER_GEO_COORDINATES="1 1" @@ -63,4 +75,40 @@ export INDEXER_AGENT_MAX_PROVISION_INITIAL_SIZE=200000 export INDEXER_AGENT_CONFIRMATION_BLOCKS=1 export INDEXER_AGENT_LOG_LEVEL=trace +# Tell the agent to leave protocol-infra subgraphs (indexing-payments, +# block-oracle) alone. Without this the reconciler pauses them (they have +# no allocation), which: +# - stalls dipper's chain_listener waiting for indexing-payments events +# - lags the block-oracle subgraph behind the chain, which makes +# indexer-agent (and tests) time out on epoch sync. +# subgraph-deploy is a compose dependency, so the deployments exist by now. +get_deployment() { + local name=$1 + curl -sf \ + "http://graph-node:${GRAPH_NODE_GRAPHQL_PORT}/subgraphs/name/${name}" \ + -H 'content-type: application/json' \ + -d '{"query":"{ _meta { deployment } }"}' \ + | jq -r '.data._meta.deployment // empty' +} + +indexing_payments_deployment=$(get_deployment indexing-payments) +block_oracle_deployment=$(get_deployment block-oracle) +if [ -z "${indexing_payments_deployment}" ]; then + echo "ERROR: indexing-payments subgraph deployment not found — chain_listener will stall" >&2 + exit 1 +fi +if [ -z "${block_oracle_deployment}" ]; then + echo "ERROR: block-oracle subgraph deployment not found — epoch sync will lag" >&2 + exit 1 +fi +echo "Marking indexing-payments (${indexing_payments_deployment}) and block-oracle (${block_oracle_deployment}) as offchain" +export INDEXER_AGENT_OFFCHAIN_SUBGRAPHS="${indexing_payments_deployment},${block_oracle_deployment}" + +# The agent constructs an indexing-payments SubgraphClient unconditionally +# (Network.create:100). Without an endpoint or deployment-id, it crashes +# with "Cannot read properties of undefined (reading 'status')" before the +# management API can come up. Provide the query endpoint here regardless of +# --enable-dips so the spec is fully populated. +export INDEXER_AGENT_INDEXING_PAYMENTS_SUBGRAPH_ENDPOINT="http://graph-node:${GRAPH_NODE_GRAPHQL_PORT}/subgraphs/name/indexing-payments" + node ./dist/index.js start diff --git a/containers/indexer/indexer-service/run.sh b/containers/indexer/indexer-service/run.sh index 4a937ae1..d75b97f6 100755 --- a/containers/indexer/indexer-service/run.sh +++ b/containers/indexer/indexer-service/run.sh @@ -9,7 +9,7 @@ subgraph_service=$(contract_addr SubgraphService.address subgraph-service) cat >config.toml <<-EOF [indexer] -indexer_address = "${RECEIVER_ADDRESS}" +indexer_address = "${INDEXER_ADDRESS}" operator_mnemonic = "${INDEXER_MNEMONIC}" [database] @@ -24,6 +24,13 @@ query_url = "http://graph-node:${GRAPH_NODE_GRAPHQL_PORT}/subgraphs/name/graph-n recently_closed_allocation_buffer_secs = 60 syncing_interval_secs = 30 +# Schema-required even in Horizon mode (where V2 escrow accounts live in the +# network subgraph itself). Point at the network subgraph as a satisfier; the +# legacy TAP v1 subgraph isn't deployed in this stack. +[subgraphs.escrow] +query_url = "http://graph-node:${GRAPH_NODE_GRAPHQL_PORT}/subgraphs/name/graph-network" +syncing_interval_secs = 30 + [blockchain] chain_id = 1337 receipts_verifier_address_v2 = "${graph_tally_verifier}" @@ -43,9 +50,37 @@ max_amount_willing_to_lose_grt = 1 timestamp_buffer_secs = 15 [tap.sender_aggregator_endpoints] -${ACCOUNT0_ADDRESS} = "http://graph-tally-aggregator:${GRAPH_TALLY_AGGREGATOR_PORT}" +${DEPLOYER_ADDRESS} = "http://graph-tally-aggregator:${GRAPH_TALLY_AGGREGATOR_PORT}" +# Horizon mode. Required by the dips-fork build pinned in the +# indexing-payments overlay; a deprecated no-op on baseline v2.1.0+ +# (upstream made Horizon always-on). +[horizon] +enabled = true EOF + +# DIPs config is only emitted when the indexing-payments overlay is active — +# the upstream indexer-rs build pinned in services.env doesn't recognise the +# [dips] schema. The indexing-payments.env overlay sets INDEXING_PAYMENTS_ENABLED=1 +# and bumps INDEXER_SERVICE_RS_VERSION to a dips-fork sha that does. +if [ "${INDEXING_PAYMENTS_ENABLED:-0}" = "1" ]; then + recurring_collector=$(contract_addr RecurringCollector.address horizon) + cat >>config.toml <<-EOF + + [dips] + host = "0.0.0.0" + port = "${INDEXER_SERVICE_DIPS_PORT}" + recurring_collector = "${recurring_collector}" + supported_networks = ["hardhat"] + min_grt_per_billion_entities_per_30_days = "0" + + [dips.min_grt_per_30_days] + hardhat = "0" + + [dips.additional_networks] + hardhat = "1337" + EOF +fi cat config.toml indexer-service-rs --config=config.toml diff --git a/containers/indexer/start-indexing/run.sh b/containers/indexer/start-indexing/run.sh index 24b5c71d..63926ceb 100755 --- a/containers/indexer/start-indexing/run.sh +++ b/containers/indexer/start-indexing/run.sh @@ -13,7 +13,7 @@ elapsed() { echo "[+$((SECONDS - t0))s] $*"; } if curl -s "http://graph-node:${GRAPH_NODE_GRAPHQL_PORT}/subgraphs/name/graph-network" \ -H 'content-type: application/json' \ -d '{"query": "{ allocations(where:{status:Active}) { indexer { id } } }" }' \ - | grep -qi "${RECEIVER_ADDRESS}" + | grep -qi "${INDEXER_ADDRESS}" then echo "Active allocations found, ensuring curation signal on all deployments..." @@ -151,7 +151,7 @@ elapsed "Waiting for active allocation in network subgraph..." while ! curl -s "http://graph-node:${GRAPH_NODE_GRAPHQL_PORT}/subgraphs/name/graph-network" \ -H 'content-type: application/json' \ -d '{"query": "{ allocations(where:{status:Active}) { indexer { id } } }" }' \ - | grep -qi "${RECEIVER_ADDRESS}" + | grep -qi "${INDEXER_ADDRESS}" do sleep 2 done diff --git a/containers/indexing-payments/dipper/run.sh b/containers/indexing-payments/dipper/run.sh index edd9f9d1..10daaf2f 100755 --- a/containers/indexing-payments/dipper/run.sh +++ b/containers/indexing-payments/dipper/run.sh @@ -11,15 +11,17 @@ network_subgraph_deployment=$(wait_for_gql \ "{ _meta { deployment } }" \ ".data._meta.deployment") -tap_verifier=$(contract_addr TAPVerifier tap-contracts) subgraph_service=$(contract_addr SubgraphService.address subgraph-service) +recurring_collector=$(contract_addr RecurringCollector.address horizon) + +signal_topic=$(kafka_topic indexing-requirements) ## Config cat >config.json <<-EOF { "dips": { "data_service": "${subgraph_service}", - "recurring_collector": "0x0000000000000000000000000000000000000000", + "recurring_collector": "${recurring_collector}", "max_initial_tokens": "1000000000000000000", "max_ongoing_tokens_per_second": "1000000000000000", "max_seconds_per_collection": 86400, @@ -36,13 +38,13 @@ cat >config.json <<-EOF "admin_rpc": { "listen_addr": "0.0.0.0:${DIPPER_ADMIN_RPC_PORT}", "gateway_operator_allowlist": [ - "${RECEIVER_ADDRESS}" + "${INDEXER_ADDRESS}" ] }, "indexer_rpc": { "listen_addr": "0.0.0.0:${DIPPER_INDEXER_RPC_PORT}", "allowlist": [ - "${RECEIVER_ADDRESS}" + "${INDEXER_ADDRESS}" ] }, "db": { @@ -58,19 +60,30 @@ cat >config.json <<-EOF "update_interval": 60 }, "signer": { - "secret_key": "${ACCOUNT0_SECRET}", + "secret_key": "${DEPLOYER_SECRET}", "chain_id": 1337 }, - "tap_signer": { - "secret_key": "${ACCOUNT0_SECRET}", - "chain_id": 1337, - "verifier": "${tap_verifier}" - }, "iisa": { "endpoint": "http://iisa:8080", "request_timeout": 30, "connect_timeout": 10, "max_retries": 3 + }, + "signal": { + "brokers": "redpanda:9092", + "topic": "${signal_topic}", + "consumer_group": "dipper-local" + }, + "chain_listener": { + "enabled": true, + "subgraph_endpoint": "http://graph-node:${GRAPH_NODE_GRAPHQL_PORT}/subgraphs/name/indexing-payments", + "chain_id": ${CHAIN_ID}, + "poll_interval": 5, + "request_timeout": 30, + "max_retries": 3 + }, + "additional_networks": { + "1337": "hardhat" } } EOF diff --git a/containers/oracles/block-oracle/run.sh b/containers/oracles/block-oracle/run.sh index 8b1d8f3b..8692cb39 100755 --- a/containers/oracles/block-oracle/run.sh +++ b/containers/oracles/block-oracle/run.sh @@ -10,13 +10,19 @@ echo "=== Configuring block-oracle service ===" cd /opt/block-oracle cat >config.toml <<-EOF blockmeta_auth_token = "" -owner_address = "${ACCOUNT0_ADDRESS#0x}" -owner_private_key = "${ACCOUNT0_SECRET#0x}" +owner_address = "${DEPLOYER_ADDRESS#0x}" +owner_private_key = "${DEPLOYER_SECRET#0x}" data_edge_address = "${data_edge#0x}" epoch_manager_address = "${graph_epoch_manager#0x}" subgraph_url = "http://graph-node:${GRAPH_NODE_GRAPHQL_PORT}/subgraphs/name/block-oracle" bearer_token = "TODO" log_level = "trace" +# Default is 10 blocks — easy to trip under heavy test load when an +# anvil_mine burst (epoch advance, time travel) puts the chain hundreds of +# blocks ahead of the subgraph briefly, sending the service into a +# 2s-cooldown spiral. 100 keeps the check active for genuine stalls while +# absorbing typical test bursts. +freshness_threshold = 100 [protocol_chain] name = "eip155:1337" diff --git a/containers/oracles/eligibility-oracle-node/Dockerfile b/containers/oracles/eligibility-oracle-node/Dockerfile index 9f064620..052a9e62 100644 --- a/containers/oracles/eligibility-oracle-node/Dockerfile +++ b/containers/oracles/eligibility-oracle-node/Dockerfile @@ -1,34 +1,13 @@ -FROM debian:bookworm-slim -ARG ELIGIBILITY_ORACLE_COMMIT +# check=skip=InvalidDefaultArgInFrom +ARG ELIGIBILITY_ORACLE_NODE_VERSION +FROM ghcr.io/edgeandnode/eligibility-oracle-node:${ELIGIBILITY_ORACLE_NODE_VERSION} -# Build + runtime dependencies -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - build-essential clang cmake lld pkg-config git \ - curl jq unzip ca-certificates \ - libssl-dev librdkafka-dev \ - && rm -rf /var/lib/apt/lists/* +# Upstream image runs as non-root `oracle`; revert for apt-get, run.sh stays as root. +USER root -# Install Rust -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal - -# Clone and build eligibility-oracle binary -WORKDIR /opt -ENV CC=clang CXX=clang++ -ENV RUSTFLAGS="-C link-arg=-fuse-ld=lld" -RUN git clone https://github.com/edgeandnode/eligibility-oracle-node && \ - cd eligibility-oracle-node && git checkout ${ELIGIBILITY_ORACLE_COMMIT} && \ - . /root/.cargo/env && cargo build --release -p eligibility-oracle && \ - cp target/release/eligibility-oracle /usr/local/bin/eligibility-oracle && \ - cd .. && rm -rf eligibility-oracle-node - -# Clean up build-only dependencies -RUN apt-get purge -y build-essential clang cmake lld pkg-config git libssl-dev librdkafka-dev && \ - apt-get autoremove -y && rm -rf /var/lib/apt/lists/* - -# Install runtime libraries +# Tools needed by run.sh (config generation, block-number polling, rpk install) RUN apt-get update \ - && apt-get install -y --no-install-recommends libssl3 librdkafka1 \ + && apt-get install -y --no-install-recommends curl jq unzip ca-certificates \ && rm -rf /var/lib/apt/lists/* # rpk CLI for Redpanda topic management @@ -36,5 +15,6 @@ RUN curl -sLO https://github.com/redpanda-data/redpanda/releases/latest/download && unzip rpk-linux-amd64.zip -d /usr/local/bin/ \ && rm rpk-linux-amd64.zip +WORKDIR /opt COPY --chmod=755 ./run.sh /opt/run.sh ENTRYPOINT ["bash", "/opt/run.sh"] diff --git a/containers/oracles/eligibility-oracle-node/run.sh b/containers/oracles/eligibility-oracle-node/run.sh index cfa74842..e8a1281e 100644 --- a/containers/oracles/eligibility-oracle-node/run.sh +++ b/containers/oracles/eligibility-oracle-node/run.sh @@ -6,12 +6,12 @@ set -eu # Wait for the REO contract address to be available in issuance.json reo_address="" for f in issuance.json; do - reo_address=$(jq -r '.["1337"].RewardsEligibilityOracle.address // empty' "/opt/config/$f" 2>/dev/null || true) + reo_address=$(jq -r '.["1337"].RewardsEligibilityOracleA.address // empty' "/opt/config/$f" 2>/dev/null || true) [ -n "$reo_address" ] && break done if [ -z "$reo_address" ]; then - echo "ERROR: RewardsEligibilityOracle address not found in issuance.json" + echo "ERROR: RewardsEligibilityOracleA address not found in issuance.json" echo "The REO contract must be deployed before starting the oracle node." exit 1 fi @@ -21,8 +21,11 @@ echo " REO contract: ${reo_address}" echo " Chain ID: ${CHAIN_ID}" echo " Redpanda: redpanda:9092" +input_topic=$(kafka_topic gateway_queries) +output_topic=$(kafka_topic eligibility_oracle_state) + # Create compacted output topic (idempotent) -rpk topic create indexer_daily_metrics \ +rpk topic create "$output_topic" \ --brokers="redpanda:9092" \ -c cleanup.policy=compact,delete \ -c retention.ms=7776000000 \ @@ -32,7 +35,7 @@ rpk topic create indexer_daily_metrics \ # survive Redpanda restarts and can cause the oracle to skip new messages # when the topic has been repopulated after a network restart. rpk group seek eligibility-oracle --to start \ - --topics gateway_queries \ + --topics "$input_topic" \ --brokers="redpanda:9092" \ 2>/dev/null || true @@ -40,6 +43,8 @@ rpk group seek eligibility-oracle --to start \ cat >config.toml <config.json <<-EOF { @@ -24,13 +26,13 @@ cat >config.json <<-EOF "config": { "bootstrap.servers": "redpanda:9092" }, - "realtime_topic": "gateway_queries" + "realtime_topic": "${queries_topic}" }, "network_subgraph": "http://graph-node:${GRAPH_NODE_GRAPHQL_PORT}/subgraphs/name/graph-network", "query_auth": "freestuff", "rpc_url": "http://chain:${CHAIN_RPC_PORT}", - "signers": ["${ACCOUNT1_SECRET}"], - "secret_key": "${ACCOUNT0_SECRET}", + "signers": ["${GOVERNOR_SECRET}"], + "secret_key": "${DEPLOYER_SECRET}", "update_interval_seconds": 10 } EOF diff --git a/containers/query-payments/tap-agent/run.sh b/containers/query-payments/tap-agent/run.sh index c68bc347..9d4823e1 100755 --- a/containers/query-payments/tap-agent/run.sh +++ b/containers/query-payments/tap-agent/run.sh @@ -9,12 +9,12 @@ graph_tally_verifier=$(contract_addr GraphTallyCollector.address horizon) subgraph_service=$(contract_addr SubgraphService.address subgraph-service) cat >endpoints.yaml <<-EOF -${ACCOUNT0_ADDRESS}: "http://graph-tally-aggregator:${GRAPH_TALLY_AGGREGATOR_PORT}" +${DEPLOYER_ADDRESS}: "http://graph-tally-aggregator:${GRAPH_TALLY_AGGREGATOR_PORT}" EOF cat >config.toml <<-EOF [indexer] -indexer_address = "${RECEIVER_ADDRESS}" +indexer_address = "${INDEXER_ADDRESS}" operator_mnemonic = "${INDEXER_MNEMONIC}" [database] @@ -29,6 +29,13 @@ query_url = "http://graph-node:${GRAPH_NODE_GRAPHQL_PORT}/subgraphs/name/graph-n recently_closed_allocation_buffer_secs = 60 syncing_interval_secs = 30 +# Schema-required even in Horizon mode (where V2 escrow accounts live in the +# network subgraph itself). Point at the network subgraph as a satisfier; the +# legacy TAP v1 subgraph isn't deployed in this stack. +[subgraphs.escrow] +query_url = "http://graph-node:${GRAPH_NODE_GRAPHQL_PORT}/subgraphs/name/graph-network" +syncing_interval_secs = 30 + [blockchain] chain_id = 1337 receipts_verifier_address_v2 = "${graph_tally_verifier}" @@ -47,8 +54,15 @@ max_amount_willing_to_lose_grt = 1 timestamp_buffer_secs = 15 [tap.sender_aggregator_endpoints] -${ACCOUNT0_ADDRESS} = "http://graph-tally-aggregator:${GRAPH_TALLY_AGGREGATOR_PORT}" +${DEPLOYER_ADDRESS} = "http://graph-tally-aggregator:${GRAPH_TALLY_AGGREGATOR_PORT}" +[horizon] +# Enable Horizon migration support and detection +# When enabled: Check if Horizon contracts are active in the network +# - If Horizon contracts detected: Hybrid migration mode (new V2 receipts only, process existing V1 receipts) +# - If Horizon contracts not detected: Remain in legacy mode (V1 receipts only) +# When disabled: Pure legacy mode, no Horizon detection performed +enabled = true EOF cat config.toml diff --git a/docker-compose.yaml b/docker-compose.yaml index 218ffd8f..c537fc06 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,3 +1,8 @@ +# Recipe-resolution sentinel. `LOCAL_NETWORK_RECIPE` is emitted into .env by +# scripts/resolve-recipe.sh; if .env is absent or hand-edited without it, this +# substitution halts compose with a clear pointer to `just resolve`. +x-recipe-sentinel: ${LOCAL_NETWORK_RECIPE:?Run "just resolve" (or "just up [recipe]") first to generate .env from a recipe in recipes/} + services: chain: container_name: chain @@ -10,6 +15,7 @@ services: restart: on-failure:3 environment: - FORK_RPC_URL=${FORK_RPC_URL:-} + networks: [default, cross-stack] block-explorer: container_name: block-explorer @@ -47,6 +53,7 @@ services: ipfs daemon healthcheck: { interval: 1s, retries: 50, test: ipfs id } restart: on-failure:3 + networks: [default, cross-stack] postgres: container_name: postgres @@ -135,6 +142,7 @@ services: platform: linux/amd64 depends_on: graph-contracts: { condition: service_completed_successfully } + subgraph-deploy: { condition: service_completed_successfully } ports: ["${INDEXER_MANAGEMENT_PORT}:7600"] stop_signal: SIGKILL volumes: @@ -152,6 +160,7 @@ services: args: NETWORK_SUBGRAPH_COMMIT: ${NETWORK_SUBGRAPH_COMMIT} BLOCK_ORACLE_COMMIT: ${BLOCK_ORACLE_COMMIT} + INDEXING_PAYMENTS_SUBGRAPH_VERSION: ${INDEXING_PAYMENTS_SUBGRAPH_VERSION} depends_on: graph-contracts: { condition: service_completed_successfully } graph-node: { condition: service_healthy } @@ -174,6 +183,7 @@ services: redpanda: container_name: redpanda image: docker.redpanda.com/redpandadata/redpanda:v23.3.5 + user: root ports: - ${REDPANDA_KAFKA_EXTERNAL_PORT}:29092 - ${REDPANDA_ADMIN_PORT}:9644 @@ -242,7 +252,7 @@ services: build: context: containers/core/gateway args: - GATEWAY_COMMIT: ${GATEWAY_COMMIT} + GATEWAY_VERSION: ${GATEWAY_VERSION} depends_on: indexer-service: { condition: service_healthy } redpanda: { condition: service_healthy } @@ -270,6 +280,7 @@ services: subgraph-deploy: { condition: service_completed_successfully } ports: - "${INDEXER_SERVICE_PORT}:7601" + - "${INDEXER_SERVICE_DIPS_PORT}:${INDEXER_SERVICE_DIPS_PORT}" stop_signal: SIGKILL volumes: - ./shared:/opt/shared:ro @@ -309,7 +320,7 @@ services: build: context: containers/oracles/eligibility-oracle-node args: - ELIGIBILITY_ORACLE_COMMIT: ${ELIGIBILITY_ORACLE_COMMIT} + ELIGIBILITY_ORACLE_NODE_VERSION: ${ELIGIBILITY_ORACLE_NODE_VERSION} depends_on: redpanda: { condition: service_healthy } gateway: { condition: service_healthy } @@ -319,7 +330,7 @@ services: - config-local:/opt/config:ro environment: RUST_LOG: eligibility_oracle=debug - BLOCKCHAIN_PRIVATE_KEY: ${ACCOUNT0_SECRET} + BLOCKCHAIN_PRIVATE_KEY: ${DEPLOYER_SECRET} restart: on-failure:3 iisa-scoring: @@ -332,7 +343,7 @@ services: redpanda: { condition: service_healthy } environment: REDPANDA_BOOTSTRAP_SERVERS: "redpanda:9092" - REDPANDA_TOPIC: gateway_queries + REDPANDA_TOPIC: gateway_queries${KAFKA_TOPIC_ENVIRONMENT:+_${KAFKA_TOPIC_ENVIRONMENT}} SCORES_FILE_PATH: /app/scores/indexer_scores.json IISA_SCORING_INTERVAL: "600" volumes: @@ -414,3 +425,12 @@ volumes: redpanda-data: iisa-scores: config-local: + +# Network shared with per-test indexer compose projects (compose/test-indexer.yaml). +# Only services that test stacks need to reach (chain, ipfs) attach to this. Services +# the test stack runs its OWN copy of (graph-node, postgres, indexer-agent, etc.) stay +# off cross-stack so the test container's DNS lookup resolves to its own instance. +networks: + default: + cross-stack: + name: cross-stack diff --git a/docs/README.md b/docs/README.md index 2f34abac..a3e9caa5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,76 +1,33 @@ # Documentation -This directory contains detailed documentation for the local-network project. +This directory contains supplementary documentation for the local-network +project. The README at the repository root covers the recipe flow and is the +right starting point. -## Indexing Payments +## Testing flows -**[Start Here: indexing-payments/safe-based/README.md](./indexing-payments/safe-based/README.md)** +Step-by-step guides in [flows/](./flows/): -**Implementation Documentation:** +- [IndexerAgentTesting.md](./flows/IndexerAgentTesting.md) — running the + indexer-agent test suite against a local-network-hosted Postgres. -- [Architecture.md](./indexing-payments/safe-based/Architecture.md) - Technical architecture -- [DipperServicePlan.md](./indexing-payments/safe-based/DipperServicePlan.md) - Dipper service implementation -- [IndexerAgentPlan.md](./indexing-payments/safe-based/IndexerAgentPlan.md) - Agent modifications -- [IndexerServicePlan.md](./indexing-payments/safe-based/IndexerServicePlan.md) - Service updates - -**Planning Summaries:** [archive/](./indexing-payments/archive/) - -- [IntegrationSummary.md](./indexing-payments/archive/IntegrationSummary.md) - Implementation status & quick start -- [UserExperience.md](./indexing-payments/archive/UserExperience.md) - What changes with override -- [TestingStatus.md](./indexing-payments/archive/TestingStatus.md) - Current testing status - -## Eligibility Oracle - -**[Start Here: eligibility-oracle/Goal.md](./eligibility-oracle/Goal.md)** - -- [Goal.md](./eligibility-oracle/Goal.md) - Objective and scope -- [Status.md](./eligibility-oracle/Status.md) - Implementation progress and log - -## Test Plan Automation - -**[Start Here: testing/reo/Goal.md](./testing/reo/Goal.md)** - -- [Goal.md](./testing/reo/Goal.md) - Layered automation approach and workflow sequence -- [Status.md](./testing/reo/Status.md) - Progress, bugs found, and gaps -- [CurationSignal.md](./testing/reo/CurationSignal.md) - Task: add curation signal to local network setup -- [TestFramework.md](./testing/TestFramework.md) - Task: test framework evaluation (bash + Rust) - -**Scripts:** - -- `scripts/test-baseline-queries.sh` - Layer 0: Validate BaselineTestPlan GraphQL queries -- `scripts/test-indexer-guide-queries.sh` - Layer 0: Validate IndexerTestGuide queries and cast commands -- `scripts/test-baseline-state.sh` - Layer 1: Verify network state matches baseline expectations - -## Graph Explorer - -**[Start Here: explorer/Goal.md](./explorer/Goal.md)** - -- [Goal.md](./explorer/Goal.md) - Task: integrate Graph Explorer with local network - -## Testing Flows - -Step-by-step testing guides: [flows/](./flows/) - -- [EligibilityOracleTesting.md](./flows/EligibilityOracleTesting.md) - REO eligibility cycle -- [IndexingPaymentsTesting.md](./flows/IndexingPaymentsTesting.md) - Dipper indexing payments -- [IndexerAgentTesting.md](./flows/IndexerAgentTesting.md) - Indexer agent behavior +The Rust integration tests under [`tests/`](../tests/) cover the network's +own behaviour (allocations, eligibility, rewards, denial, REO governance) — +see [tests/README.md](../tests/README.md) for the test map. ## Usage -**Service profiles** are enabled by default in `.env`. To customize, edit `COMPOSE_PROFILES`: - -```bash -COMPOSE_PROFILES=rewards-eligibility,indexing-payments,block-oracle,explorer # all (default) -COMPOSE_PROFILES=rewards-eligibility # REO only -``` - -Then `docker compose up -d` applies the active profiles automatically. +The set of services and pinned image versions is selected by a **recipe** in +[../recipes/](../recipes/). `just resolve ` (or `just up `) +materialises the chosen recipe into `.env`, which `docker compose` picks up +automatically. See the project [README](../README.md#recipes) for the recipe +list. -## Documentation Guidelines +## Documentation guidelines -See [CLAUDE.md](../CLAUDE.md) for documentation standards including: +See [CLAUDE.md](../CLAUDE.md) for documentation standards: -- File naming conventions (TitleCase for markdown) -- Directory organization (use subdirectories by topic) -- Linking and navigation (relative paths, cross-references) -- Content maintenance (update with code, archive or delete obsolete docs) +- File naming: TitleCase for markdown +- Directory organization: subdirectories by topic +- Linking: relative paths, cross-references +- Maintenance: update with code; delete obsolete docs by default diff --git a/docs/eligibility-oracle/Goal.md b/docs/eligibility-oracle/Goal.md deleted file mode 100644 index d798bbfc..00000000 --- a/docs/eligibility-oracle/Goal.md +++ /dev/null @@ -1,115 +0,0 @@ -# Rewards Eligibility Oracle - Goal - -## Objective - -Add the Rewards Eligibility Oracle (REO) to the local network so that indexer reward eligibility can be tested end-to-end. This involves two pieces: - -1. **REO contract** - Deploy and configure the `RewardsEligibilityOracle` contract, integrate it with the `RewardsManager` -2. **REO node** - Containerise and run the eligibility-oracle-node service that consumes gateway query data from Redpanda and submits eligible indexers on-chain - -## Background - -The REO determines which **indexers** are eligible for rewards based on their query-serving performance. This is not the Subgraph Availability Oracle (SAO) and has nothing to do with subgraph denial. - -### How It Works - -1. The **gateway** publishes query attempt data to the `gateway_queries` Redpanda topic (already exists in local network) -2. The **REO node** consumes these events, aggregates per-indexer metrics over a rolling window (28 days default), and evaluates eligibility based on: - - Minimum online days (default: 5) - - Minimum subgraphs served (default: 1) - - Maximum latency (default: 5000ms) - - Maximum blocks behind (default: 50000) -3. Eligible indexers are submitted on-chain via `renewIndexerEligibility()` on the REO contract -4. The **RewardsManager** checks `rewardsEligibilityOracle.isEligible(indexer)` when distributing rewards - -### REO Contract Design - -- **Deny by default**: indexers are not eligible until an authorized oracle calls `renewIndexerEligibility()` -- **Time-based**: eligibility expires after `eligibilityPeriod` (default: 14 days) and must be renewed -- **Fail-safe**: if oracles stop updating for `oracleUpdateTimeout` (default: 7 days), all indexers become eligible -- **Global toggle**: `eligibilityValidationEnabled` can disable all eligibility checks (default: disabled) -- **Role-based access**: GOVERNOR, OPERATOR, ORACLE, PAUSE roles - -## What Success Looks Like - -1. **REO contract deployed** as part of `graph-contracts` setup, integrated with `RewardsManager` via `setRewardsEligibilityOracle()` -2. **REO node running** in docker-compose, consuming from local Redpanda `gateway_queries` topic -3. **Indexer marked eligible** after serving queries through the gateway -4. **Rewards gated by eligibility** - can verify via `isEligible()` contract calls and reward distribution behaviour - -## Components - -### Existing (already in local network) - -- **graph-contracts** - Deploys Horizon protocol contracts including `RewardsManager`; `issuance.json` already referenced but issuance package not yet deployed -- **gateway** - Publishes query data to `gateway_queries` Redpanda topic -- **redpanda** - Kafka-compatible message broker, already running -- **graph-node** - Indexes subgraphs, serves queries -- **indexer-agent / indexer-service** - Indexer infrastructure - -### To Be Added - -- **REO contract deployment** - Add Phase 4 to `graph-contracts/run.sh` using issuance package deployment scripts from `packages/deployment/deploy/rewards/eligibility/` -- **REO node containerisation** - Create Dockerfile for the Rust service at `/git/local/eligibility-oracle-node/eligibility-oracle-node` -- **REO node docker-compose service** - Config, Redpanda connection, chain RPC, contract address, signing key -- **Redpanda topic setup** - Create compacted `indexer_daily_metrics` topic for REO node state persistence - -## Source Repositories - -| Component | Location | Branch | -| --------------------------------- | ---------------------------------------------------------------------- | ---------- | -| REO contract + deployment scripts | `/git/graphprotocol/contracts` | `main` | -| REO contract source | `packages/issuance/contracts/eligibility/RewardsEligibilityOracle.sol` | | -| REO deployment scripts | `packages/deployment/deploy/rewards/eligibility/` | | -| REO node (Rust service) | `/git/local/eligibility-oracle-node/eligibility-oracle-node` | | - -## Implementation Tasks - -### 1. Contract Deployment & Integration - -- Update `CONTRACTS_COMMIT` in `.env` to point to `main` branch -- Add local network (chain 1337) support to `packages/deployment` (see [Status.md](./Status.md#gaps-to-fix)) -- Add REO contract deployment as a new phase in `graph-contracts/run.sh` -- Configure roles: grant ORACLE_ROLE to the REO node's signing key -- Integrate with RewardsManager: call `setRewardsEligibilityOracle(reoAddress)` -- Write deployed address to `issuance.json` - -### 2. REO Node Containerisation - -- Create Dockerfile for the Rust workspace at `/git/local/eligibility-oracle-node/eligibility-oracle-node` -- Create `config.toml` template with local network values: - - `kafka.bootstrap_servers` = `redpanda:9092` - - `kafka.input_topic` = `gateway_queries` - - `blockchain.rpc_urls` = `["http://chain:8545"]` - - `blockchain.contract_address` = from `issuance.json` - - `blockchain.chain_id` = `1337` - - `blockchain.private_key` = signing key with ORACLE_ROLE -- Add to docker-compose (likely as an override like indexing-payments) - -### 3. Redpanda Topic Setup - -- Create compacted `indexer_daily_metrics` topic for REO node persistence -- May need a setup script or init container - -### 4. Testing & Validation - -- Send queries through the gateway to generate `gateway_queries` events -- Verify REO node processes events and submits eligibility on-chain -- Check `isEligible(indexerAddress)` returns true -- Verify reward distribution honours eligibility - -## Configuration for Local Network - -For local testing, sensible overrides to the REO node defaults: - -- Shorter `analysis_period_days` (e.g., 1 day instead of 28) -- Lower `min_online_days` (e.g., 1 instead of 5) -- Shorter `scheduling.interval_secs` (e.g., 60 instead of 10800) -- Shorter `staleness_threshold_hours` (e.g., 1 instead of 20) -- Consider starting with `eligibilityValidationEnabled = false` on the contract and enabling once the node is running - -## Related Documentation - -- [Local Network README](../../README.md) -- [REO Contract Spec](file:///git/graphprotocol/contracts/main/packages/issuance/contracts/eligibility/RewardsEligibilityOracle.md) -- [REO Deployment Guide](file:///git/graphprotocol/contracts/main/packages/deployment/docs/deploy/RewardsEligibilityOracleDeployment.md) diff --git a/docs/eligibility-oracle/Status.md b/docs/eligibility-oracle/Status.md deleted file mode 100644 index 261bc630..00000000 --- a/docs/eligibility-oracle/Status.md +++ /dev/null @@ -1,233 +0,0 @@ -# Rewards Eligibility Oracle - Status - -> Last updated: 2026-02-18 - -## Current Phase: End-to-end verified - -### Summary - -REO is fully integrated into the local network. Both workstreams complete and end-to-end flow verified: - -1. **Contract deployment** - DONE: Phase 4 deploys REO, integrates with RewardsManager, grants ORACLE_ROLE -2. **REO node** - DONE: consumes gateway_queries, evaluates eligibility, submits on-chain -3. **End-to-end** - VERIFIED: queries through gateway -> REO node -> `isEligible()` returns true - -## Completed - -- [x] Created goal documentation ([Goal.md](./Goal.md)) -- [x] Surveyed existing local network components (19 services, 3 contract deployment phases) -- [x] Identified `issuance.json` already referenced in `graph-contracts/run.sh` but issuance contracts not yet deployed -- [x] Explored REO contract in `graphprotocol/contracts` post-audit branch -- [x] Explored REO node at `/git/local/eligibility-oracle-node/eligibility-oracle-node` -- [x] Explored deployment package scripts and documentation for local network feasibility -- [x] Identified deployment package gaps and REO node contract signature mismatch -- [x] Created REO node Dockerfile, run.sh, docker-compose override -- [x] Fixed deployment package: added localNetwork support (chain 1337) on post-audit branch -- [x] Added Phase 4 (REO) to `graph-contracts/run.sh` with idempotency, RM integration, ORACLE_ROLE grant -- [x] Fixed REO node ABI mismatch: added `bytes calldata data` param and `uint256` return type -- [x] Updated `CONTRACTS_COMMIT` in `.env` to post-audit branch (`0003fe3a`) -- [x] Fixed address book compatibility: deployment package writes `implementationDeployment` field that indexer-agent rejects; added cleanup step in Phase 4 -- [x] Fixed docker-compose override: added `.env` bind mount for REO node container -- [x] End-to-end verified: gateway queries -> Redpanda -> REO node -> on-chain eligibility - -## Up Next - -- [ ] Clean run test: `docker compose down -v && up` to verify full lifecycle from scratch -- [x] Document testing procedure in `docs/flows/` -- [x] Create automated test script (`scripts/test-reo-eligibility.sh`) - -## Gaps To Fix - -### ~~1. Deployment package: extend for local network (chain 1337) deployment~~ FIXED - -**Branch:** `post-audit` in `graphprotocol/contracts` (commit `bcf73964`) - -**What was done:** - -- `rocketh/config.ts`: added `graphLocalNetworkChain` (id: 1337) and `localNetwork` environment -- `hardhat.config.ts`: added chain 1337 descriptor and `localNetwork` network config (`http://chain:8545`, test mnemonic) -- `lib/address-book-utils.ts`: added `isLocalNetworkMode()` detection and `addresses-local-network.json` resolution for all three packages (horizon, subgraph-service, issuance) -- `00_sync.ts`: no changes needed - chain 1337 naturally passes the `31337` guard, address book resolution handles the rest -- Docs: added localNetwork to quick reference, local network section in LocalForkTesting.md, fixed broken README link - -**Governance handling:** Deploy scripts check if deployer has GOVERNOR_ROLE on-chain. In local network (same account), TXs execute directly inline - no governance batch files needed. - -### ~~5. REO node: contract signature mismatch~~ FIXED - -**Where:** `/git/local/eligibility-oracle-node/eligibility-oracle-node/crates/eligibility-oracle/src/blockchain.rs` - -**What was done:** - -- Updated `sol!` macro: `renewIndexerEligibility(address[] calldata indexers, bytes calldata data) external returns (uint256)` -- Added `Bytes` import from `alloy::primitives` -- Updated call site to pass `Bytes::new()` as the `data` parameter -- Verified: `cargo check -p eligibility-oracle` passes clean - -### ~~6. Deployment package: `06_integrate.ts` hardcodes `canExecuteDirectly=false`~~ FIXED - -**Branch:** `post-audit` in `graphprotocol/contracts` (commit `5e23cde8`) - -**What was done:** - -- Query `eth_accounts` from the provider to check if the governor key is available -- If governor is in the accounts list (e.g., mnemonic-derived), execute directly with governor as executor -- If not (e.g., Safe multisig in production), generate governance TX file as before -- Removed `requireDeployer` dependency since executor is now the governor - -### 7. Indexer-agent: strict address book validation - -**Status:** Worked around in local network; upstream fix recommended - -The indexer-agent validates address book JSON entries and rejects any with unknown fields. The deployment package (post-audit) now writes `implementationDeployment` and `proxyDeployment` metadata to address books, causing the agent to crash with: - -``` -Address book entry contains invalid fields: implementationDeployment -``` - -**Local workaround:** Phase 4 in `graph-contracts/run.sh` strips these fields from `horizon.json` and `subgraph-service.json` after deployment. - -**Proper fix:** The indexer-agent should use permissive parsing that extracts known fields and ignores unknown ones. This would prevent breakage as the address book format evolves. - -## Notes - -### Contract Deployment Approach - -Phase 4 in `graph-contracts/run.sh` runs the full REO lifecycle via a single deployment package invocation, plus one cast call for ORACLE_ROLE: - -```bash -# Full lifecycle: sync → deploy → configure → transfer → integrate → verify -cd /opt/contracts/packages/deployment -npx hardhat deploy --tags rewards-eligibility --network localNetwork --skip-prompts - -# Grant ORACLE_ROLE (not part of standard deployment - local network specific) -cast send ... --private-key="${ACCOUNT1_SECRET}" "${reo_address}" "grantRole(bytes32,address)" "${oracle_role}" "${ACCOUNT0_ADDRESS}" -``` - -### REO Node Architecture - -- Consumes `gateway_queries` topic (already published by gateway in local network) -- Persists aggregated metrics to compacted `indexer_daily_metrics` topic -- Runs in daemon mode (periodic cycles) or single invocation -- Uses Alloy for contract interaction, rdkafka for Redpanda -- Needs `librdkafka` in container (rdkafka crate dependency) - -### Local Network Tuning - -Default REO node config is for production (28-day windows, 3-hour cycles). For local testing need shorter values - see [Goal.md](./Goal.md#configuration-for-local-network). - -### Accounts - -- `ACCOUNT0` (0xf39F...) - deployer, ORACLE_ROLE on REO, signing key for REO node -- `ACCOUNT1` (0x7099...) - governor role in contracts, GOVERNOR_ROLE on REO (after configure) -- `RECEIVER` (0xf4EF...) - indexer - -### Decisions Made - -- Use the deployment package scripts for contract deployment (not local forge/cast workarounds) -- REO node to run in daemon mode with shortened intervals for local testing -- ACCOUNT0 as both NetworkOperator and ORACLE_ROLE holder -- Skip governance transfer step in local network (deployer keeps GOVERNOR_ROLE) - -### Prerequisites for Testing - -- `CONTRACTS_COMMIT` in `.env` must point to a commit on post-audit that includes localNetwork support -- The post-audit branch must be pushed to GitHub (Docker build clones from there) - ---- - -## Log - -### 2026-02-18 - Project started - -- Created [Goal.md](./Goal.md) and this status document -- Explored REO contract in `graphprotocol/contracts` post-audit branch: upgradeable proxy pattern, role-based access, time-based eligibility with fail-safe -- Explored REO node: Rust service, Redpanda consumer, batched on-chain submission, no Dockerfile yet -- Identified key integration point: `RewardsManager.setRewardsEligibilityOracle()` connects the two systems -- Local network already has the `gateway_queries` Redpanda topic from gateway service - -### 2026-02-18 - Deployment package analysis - -- Analysed deployment scripts in `packages/deployment/deploy/rewards/eligibility/` -- Identified 5 gaps blocking local network deployment (see Gaps section above) -- Critical finding: REO node has ABI mismatch with contract (`renewIndexerEligibility` missing `data` param) -- Decision: fix deployment package rather than create local workarounds -- Proceeding with REO node containerisation (independent of contract deployment) - -### 2026-02-18 - REO node containerisation - -- Created `eligibility-oracle-node/Dockerfile` (multi-stage: rust-builder + wrapper-dev) -- Created `eligibility-oracle-node/run.sh` (generates config.toml, creates Redpanda topic, starts daemon) -- Created `overrides/eligibility-oracle/docker-compose.yaml` (override pattern like indexing-payments) -- Symlinked source from `/git/local/eligibility-oracle-node/eligibility-oracle-node` -- Config uses relaxed local network thresholds: 1-day window, 1 min online day, 60s cycle interval -- Uses `ACCOUNT0_SECRET` as the oracle signing key (needs ORACLE_ROLE granted once contract is deployed) -- Updated overrides/README.md with eligibility oracle section - -### 2026-02-18 - Checked post-audit branch - -- Switched target from `baseline` to `post-audit` branch (freshly rebased with all changes) -- Confirmed deployment package gap still present on `post-audit`: no chain 1337 / localNetwork support -- Fork-based testing (chain 31337 + FORK_NETWORK) works fine - this is a new capability needed, not a broken feature -- Rephrased gap #1 to clarify the distinction - -### 2026-02-18 - Fixed deployment package for local network - -- Added localNetwork support to `packages/deployment` on `post-audit` branch (commit `bcf73964`) -- Changes: `rocketh/config.ts` (chain 1337 + environment), `hardhat.config.ts` (chain descriptor + network), `address-book-utils.ts` (`isLocalNetworkMode()` + `addresses-local-network.json` resolution) -- `00_sync.ts` works as-is: chain 1337 bypasses the `31337` guard, address books resolve via updated path functions -- Governance TX handling confirmed: deploy scripts check `hasRole(GOVERNOR_ROLE, deployer)` on-chain, execute directly when true -- Fixed broken README link (`DeploymentDesignPrinciples.md` -> `deploy/ImplementationPrinciples.md`) -- Added localNetwork to DeploymentSetup.md quick reference table -- Added local network section to LocalForkTesting.md - -### 2026-02-18 - Phase 4 and REO node ABI fix - -- Added Phase 4 (REO) to `graph-contracts/run.sh`: - - Idempotency check via `issuance.json` + on-chain code check - - Pre-populates NetworkOperator (ACCOUNT0) in issuance address book - - Runs `npx hardhat deploy --tags rewards-eligibility-configure --network localNetwork --skip-prompts` - - Integrates REO with RewardsManager via cast (ACCOUNT1 governor key) - - Grants ORACLE_ROLE to ACCOUNT0 via cast -- Fixed REO node ABI mismatch in `blockchain.rs`: - - Updated sol! macro to match contract: `renewIndexerEligibility(address[], bytes) returns (uint256)` - - Added `Bytes` import, pass `Bytes::new()` at call site - - Compiles clean: `cargo check -p eligibility-oracle` -- Fixed gap #6: `06_integrate.ts` now checks `eth_accounts` for governor key availability - - Phase 4 updated to use full `rewards-eligibility` tag (single npx invocation) - - Only remaining cast call: ORACLE_ROLE grant (local network specific) -- Note: `CONTRACTS_COMMIT` in `.env` needs updating to post-audit (143 commits behind) - -### 2026-02-18 - End-to-end integration testing - -- Updated `CONTRACTS_COMMIT` in `.env` to `0003fe3a` (post-audit HEAD, already in sync with origin) -- Built all images and started network with REO override -- Phase 4 deployed REO contract at `0x86a2ee8faf9a840f7a2c64ca3d51209f9a02081d` -- RewardsManager integrated, ORACLE_ROLE granted to ACCOUNT0 -- **Bug found**: indexer-agent crashes with `Address book entry contains invalid fields: implementationDeployment` - - Cause: deployment package (post-audit) writes `implementationDeployment` metadata to horizon.json - - indexer-agent strictly validates address book fields and rejects unknown ones - - Fix: added `jq walk(... del(.implementationDeployment, .proxyDeployment))` cleanup after Phase 4 in run.sh - - This is arguably an indexer-agent bug (should ignore unknown fields, not reject them) -- **Bug found**: REO node container missing `.env` bind mount - - Override only had `./config/local:/opt/config:ro`, not `./.env:/opt/config/.env:ro` - - Caused `CHAIN_ID: unbound variable` crash - - Fix: added `.env` bind mount to override's volumes -- After fixes, full flow verified: - - Sent 10 queries through gateway (subgraph `BFr2mx7...`) - - REO node consumed 11 messages from `gateway_queries` (10 + 1 from gateway health check) - - Evaluated: 1 eligible indexer (`0xf4ef...`), days_online=1, good_queries=11 - - Submitted `renewIndexerEligibility` on-chain (tx `0x0f48d617...`) - - `isEligible(0xf4ef...)` returns `true` on-chain - -### 2026-02-18 - Demonstration script and testing documentation - -- Created `scripts/test-reo-eligibility.sh`: automated full-cycle test - - Enables eligibility validation on the REO contract (default is disabled) - - Seeds `lastOracleUpdateTime` via empty `renewIndexerEligibility([])` to disable fail-safe - - Verifies indexer is NOT eligible (deny-by-default) - - Sends queries through gateway, waits for REO node cycle (75s) - - Verifies indexer IS eligible after REO submission -- Created `docs/flows/EligibilityOracleTesting.md`: step-by-step manual and automated testing guide - - Includes contract behaviour reference table (three layers: validation toggle, fail-safe, per-indexer) - - Troubleshooting section for common issues -- Note: previous e2e test did not verify initial ineligibility — only checked `isEligible()` after REO had already submitted. The new script properly demonstrates the full deny→allow transition diff --git a/docs/explorer/Goal.md b/docs/explorer/Goal.md deleted file mode 100644 index 0dbadc13..00000000 --- a/docs/explorer/Goal.md +++ /dev/null @@ -1,154 +0,0 @@ -# Task: Graph Explorer Integration - -> Created: 2026-02-20 - -## Problem - -Several BaselineTestPlan cycles (1-2) reference Explorer UI operations for staking, delegation, and curation. Currently these are done by `graph-contracts` during deployment or by `cast send` in scripts. There is no way to: - -1. Visually verify protocol state during development/testing -2. Test the actual UI flows that indexers use in production -3. Reference exact UI components when documenting test equivalents - -## Objective - -Add Graph Explorer as an optional service in the local network, providing both a visual development tool and a reference for what contract calls the UI makes. - -## Source - -The Graph Explorer repository is available locally at `/git/edgeandnode/graph-explorer/graph-explorer`. - -### Architecture - -- **Next.js 14** frontend, no backend database -- All data from network subgraph GraphQL + direct contract calls via Wagmi/Viem -- Docker support: multi-stage build, port 3000 -- All required infrastructure already exists in local-network (graph-node, subgraphs, chain RPC, IPFS) - -### Key Finding: No API Layer - -Explorer has **no REST/GraphQL API for write operations**. All staking, delegation, and curation operations are direct smart contract calls from the browser via Wagmi hooks. This means: - -- For test automation, `cast send` is the equivalent of what the UI does -- The value of Explorer in local-network is **visual verification and reference**, not scripting -- Test scripts should reference the Explorer component that makes each call - -## Contract Call Reference - -This maps Explorer UI actions to the contract calls test scripts should make: - -### Curation - -| UI Action | Explorer Component | Contract | Function | -| ------------------ | -------------------------- | ---------- | --------------------------------------- | -| Signal (version) | `SignalForm.tsx:238-257` | L2Curation | `mint(bytes32, uint256, uint256)` | -| Signal (named) | `SignalForm.tsx:238-257` | L2GNS | `mintSignal(uint256, uint256, uint256)` | -| Unsignal (version) | `UnsignalForm.tsx:202-224` | L2Curation | `burn(bytes32, uint256, uint256)` | -| Unsignal (named) | `UnsignalForm.tsx:202-224` | L2GNS | `burnSignal(uint256, uint256, uint256)` | - -### Delegation - -| UI Action | Explorer Component | Contract | Function | -| ---------- | -------------------------------------- | -------------- | ---------------------------------------------- | -| Delegate | `DelegateTransactionContext.tsx:40-62` | HorizonStaking | `delegate(address, address, uint256, uint256)` | -| Undelegate | `UndelegateFormDefinition.tsx:62-87` | HorizonStaking | `undelegate(address, address, uint256)` | - -### Staking - -| UI Action | Explorer Component | Contract | Function | -| --------- | ------------------------- | -------------- | ----------------------------------------------------------------- | -| Stake | `StakeForm.tsx:104-120` | HorizonStaking | `stake(uint256)` or `stakeToProvision(address, address, uint256)` | -| Unstake | `UnstakeForm.tsx:101-146` | HorizonStaking | `unstake(uint256)` or `thaw(address, address, uint256)` | - -### Token Approval (all operations) - -| UI Action | Explorer Component | Contract | Function | -| ------------- | ---------------------------- | ------------ | --------------------------- | -| Approve spend | `GraphTokenApprovalFlow.tsx` | L2GraphToken | `approve(address, uint256)` | - -All Explorer component paths are relative to `/git/edgeandnode/graph-explorer/graph-explorer/src/`. - -## Implementation Approach - -### Override Pattern - -Add as a profiled service in `docker-compose.yaml`: - -```yaml -# profiles: [explorer] -``` - -### Docker Compose Override - -```yaml -services: - graph-explorer: - build: - context: /git/edgeandnode/graph-explorer/graph-explorer - dockerfile: Dockerfile - ports: - - "${EXPLORER_PORT:-3001}:3000" - environment: - - ENVIRONMENT=local - - DEFAULT_CHAIN_NAME=hardhat - - GRAPH_NETWORK_ID=1337 - - IS_TESTNET=true - depends_on: - ready: - condition: service_completed_successfully -``` - -### Configuration Challenges - -1. **Chain configuration**: Explorer expects Ethereum/Arbitrum chains. Local network uses hardhat (chainId 1337). May need chain config overrides or patches. - -2. **Contract addresses**: Explorer resolves addresses from `@graphprotocol/address-book`. Local network deploys fresh addresses each time. Need to either: - - Override address resolution at runtime - - Build with local addresses baked in - - Patch the address-book module - -3. **Private npm dependencies**: `@edgeandnode/gds` and `@edgeandnode/graph-auth-kit` require npm authentication. The Dockerfile uses `.npmrc` secret mounting. - -4. **Wallet connection**: MetaMask or similar needs to connect to the local hardhat chain (chainId 1337, RPC at localhost:8545). - -### Complexity Assessment - -| Aspect | Difficulty | Notes | -| ------------------ | ----------- | ------------------------------ | -| Docker build | Low | Dockerfile exists, port 3000 | -| Chain config | Medium | Needs hardhat chain support | -| Address resolution | Medium-High | Fresh addresses per deployment | -| npm auth for build | Low | `.npmrc` pattern exists | -| Wallet integration | Medium | MetaMask + hardhat chain | -| Overall | Medium | Not blocking, but non-trivial | - -## Approach - -### Phase 1: Investigate Feasibility - -- [ ] Attempt local Docker build with npm auth -- [ ] Identify all hardcoded chain/network assumptions -- [ ] Test if address-book can be overridden at runtime -- [ ] Document blockers - -### Phase 2: Minimal Integration - -- [ ] Add graph-explorer service to `docker-compose.yaml` with `profiles: [explorer]` -- [ ] Configure for local hardhat chain -- [ ] Verify read-only operations work (view indexers, allocations, subgraphs) - -### Phase 3: Full Integration - -- [ ] Enable wallet connection to local chain -- [ ] Test write operations (delegate, signal, stake) via UI -- [ ] Document setup in README - -## Value Assessment - -**For testing**: Medium — test automation uses `cast send` regardless; Explorer adds visual verification but isn't required for scripted tests. - -**For development**: High — seeing protocol state visually accelerates debugging and makes the local network more approachable. - -**For documentation**: High — can reference exact UI flows and screenshot expected states. - -**Recommendation**: Worth pursuing but not a strict dependency for test automation. The contract call reference table above (linking UI components to `cast` equivalents) bridges the gap for scripted tests. diff --git a/docs/flows/EligibilityOracleTesting.md b/docs/flows/EligibilityOracleTesting.md deleted file mode 100644 index de5172e4..00000000 --- a/docs/flows/EligibilityOracleTesting.md +++ /dev/null @@ -1,171 +0,0 @@ -# Eligibility Oracle Testing Flow - -Test the Rewards Eligibility Oracle (REO) end-to-end: indexer starts ineligible, serves queries through the gateway, and is marked eligible by the REO node. - -## Prerequisites - -1. Local network running with the rewards-eligibility profile enabled (`COMPOSE_PROFILES=rewards-eligibility` in `.env`, enabled by default): - ```bash - docker compose up -d --build - ``` - -2. All core services healthy (gateway, graph-node, redpanda, chain, graph-contracts): - ```bash - docker compose ps - ``` - -3. REO contract deployed (Phase 4 in graph-contracts logs): - ```bash - docker compose logs graph-contracts | grep "Phase 4" - ``` - -4. REO node running and connected: - ```bash - docker compose logs --tail 20 eligibility-oracle-node - ``` - -5. `cast` available on the host (installed with Foundry). - -6. Source environment variables: - ```bash - source .env - ``` - -## Automated Test - -Run the full cycle with a single script: - -```bash -./scripts/test-reo-eligibility.sh # default: 10 queries -./scripts/test-reo-eligibility.sh 50 # send 50 queries -``` - -The script: -1. Checks eligibility validation is enabled (done by deployment, errors if not) -2. Seeds `lastOracleUpdateTime` to disable the fail-safe (if needed) -3. Verifies the indexer is NOT eligible -4. Sends queries through the gateway -5. Polls `isEligible()` every 10s until true or timeout (150s) - -## Manual Step-by-Step - -### 1. Read REO contract address - -```bash -source .env -REO=$(docker exec graph-node cat /opt/config/issuance.json | jq -r '.["1337"].RewardsEligibilityOracle.address') -RPC="http://localhost:${CHAIN_RPC_PORT}" -echo "REO: $REO" -``` - -### 2. Check contract state - -```bash -# Is eligibility validation enabled? -cast call --rpc-url="$RPC" "$REO" "getEligibilityValidation()(bool)" - -# When was the last oracle update? -cast call --rpc-url="$RPC" "$REO" "getLastOracleUpdateTime()(uint256)" - -# Is the indexer eligible? -cast call --rpc-url="$RPC" "$REO" "isEligible(address)(bool)" "$RECEIVER_ADDRESS" -``` - -### 3. Verify eligibility validation is enabled - -Deployment (Phase 4) enables validation automatically. Confirm: - -```bash -cast call --rpc-url="$RPC" "$REO" "getEligibilityValidation()(bool)" -# Expected: true -``` - -If not enabled, re-run graph-contracts or enable manually: -```bash -# Requires OPERATOR_ROLE (ACCOUNT0) -cast send --rpc-url="$RPC" --confirmations=0 \ - --private-key="$ACCOUNT0_SECRET" \ - "$REO" "setEligibilityValidation(bool)" true -``` - -### 4. Seed the oracle timestamp - -If `lastOracleUpdateTime` is 0 (never updated), the fail-safe makes everyone eligible regardless. Seed it with an empty update: - -```bash -# Requires ORACLE_ROLE (ACCOUNT0) -cast send --rpc-url="$RPC" --confirmations=0 \ - --private-key="$ACCOUNT0_SECRET" \ - "$REO" "renewIndexerEligibility(address[],bytes)" "[]" "0x" -``` - -### 5. Verify indexer is NOT eligible - -```bash -cast call --rpc-url="$RPC" "$REO" "isEligible(address)(bool)" "$RECEIVER_ADDRESS" -# Expected: false -``` - -### 6. Send queries through the gateway - -```bash -# Mine blocks first to keep the gateway happy -./scripts/mine-block.sh 5 - -# Send 10 queries -./scripts/query_gateway.sh 10 -``` - -### 7. Wait for the REO node cycle - -The REO node cycles every 60 seconds in local network configuration. Watch the logs: - -```bash -docker compose logs -f eligibility-oracle-node -``` - -Look for: -- `Consumed N messages from gateway_queries` -- `Eligible indexers: [0xf4ef...]` -- `renewIndexerEligibility` transaction submitted - -### 8. Verify indexer IS eligible - -```bash -cast call --rpc-url="$RPC" "$REO" "isEligible(address)(bool)" "$RECEIVER_ADDRESS" -# Expected: true -``` - -## Understanding the Contract Behaviour - -The REO contract has three layers of eligibility logic: - -| Condition | `isEligible()` returns | Notes | -|---|---|---| -| Validation disabled | `true` (all) | Default after deployment | -| Validation enabled, oracle never updated (fail-safe) | `true` (all) | `lastOracleUpdateTime=0`, timeout expired | -| Validation enabled, oracle active, indexer not renewed | `false` | Deny-by-default | -| Validation enabled, oracle active, indexer renewed | `true` | Within `eligibilityPeriod` (14 days) | -| Validation enabled, oracle stale (`> oracleUpdateTimeout`) | `true` (all) | Fail-safe for oracle downtime | - -The automated test script handles states 1 and 2 by enabling validation and seeding the oracle timestamp. - -## Troubleshooting - -### Indexer already eligible before test -The REO node may have already submitted eligibility in a previous cycle. Wait for the `eligibilityPeriod` (14 days on-chain, but you can check the configured value) to expire, or redeploy the contracts with `docker compose down -v && up`. - -### REO node not submitting on-chain -Check that: -- The `gateway_queries` Redpanda topic has messages: `docker compose exec redpanda rpk topic consume gateway_queries --num 1` -- The node has ORACLE_ROLE: `cast call --rpc-url="$RPC" "$REO" "hasRole(bytes32,address)(bool)" "$(cast call --rpc-url=$RPC $REO 'ORACLE_ROLE()(bytes32)')" "$ACCOUNT0_ADDRESS"` -- The node can reach the chain: check logs for RPC errors - -### All queries failing (HTTP != 200) -- Mine blocks: `./scripts/mine-block.sh 10` -- Check gateway health: `docker compose ps gateway` -- Ensure at least one subgraph is allocated and synced - -### Cast command fails -- Ensure Foundry is installed: `cast --version` -- Check chain is running: `cast block-number --rpc-url="$RPC"` diff --git a/docs/flows/IndexerAgentTesting.md b/docs/flows/IndexerAgentTesting.md index d3c0d854..e6866123 100644 --- a/docs/flows/IndexerAgentTesting.md +++ b/docs/flows/IndexerAgentTesting.md @@ -86,7 +86,7 @@ yarn test - The test script changes directories to `$INDEXER_AGENT_SOURCE_ROOT` during execution - Test output files are created in the directory where you run the script - After debugging, you might be in `$INDEXER_AGENT_SOURCE_ROOT` instead of the local-network root -- Use absolute paths when in doubt: `/home/pablo/repos/local-network/scripts/test-indexer-agent.sh` +- Use absolute paths when in doubt ### Understanding Test Output diff --git a/docs/flows/IndexingPaymentsTesting.md b/docs/flows/IndexingPaymentsTesting.md deleted file mode 100644 index 9e0c1540..00000000 --- a/docs/flows/IndexingPaymentsTesting.md +++ /dev/null @@ -1,206 +0,0 @@ -# Indexing Payments Testing Flow - -This guide walks through testing the Indexing Payments system in the local-network environment. - -**What is Indexing Payments?** Indexing Payments is a system for paying indexers to index specific subgraphs. Unlike query fees, indexing payments incentivize indexers to allocate resources to index subgraphs that may not yet have query traffic. - -## Prerequisites - -1. All services running and healthy: - - ```bash - docker compose ps - ``` - -2. Dipper service running (enable `indexing-payments` profile in `.env`): - - ```bash - # Add indexing-payments to COMPOSE_PROFILES in .env, then: - docker compose up -d --build dipper - ``` - -3. Source environment variables: - ```bash - source .env - ``` - -## Setup Dipper CLI - -You have two options for running the dipper CLI: - -### Option 1: Use the Wrapper Script (Recommended) - -```bash -# From repo root - automatically handles environment variables -./scripts/dipper-cli.sh [command] -``` - -### Option 2: Run from Source - -```bash -# Set DIPPER_SOURCE_ROOT to a local clone of edgeandnode/dipper -cd $DIPPER_SOURCE_ROOT -# All commands will be run from this directory using cargo -# Note: You'll need to set environment variables manually (see below) -``` - -## Configure Authentication - -**Important**: The dipper CLI requires environment variables to be set for EVERY command. - -```bash -# Set up dipper CLI authentication (valid for current shell session) -source .env # Load environment from repo root -export INDEXING_SIGNING_KEY="${RECEIVER_SECRET}" -export INDEXING_SERVER_URL="http://localhost:${DIPPER_ADMIN_RPC_PORT}/" -``` - -**Note**: The CLI will fail with `missing field 'server_url'` if these environment variables are not set. - -## Testing Flow - -### 1. Register an Indexing Request - -```bash -# Using wrapper script (from repo root): -./scripts/dipper-cli.sh requests register "QmNngXzFajkQHRj3ZjAJAF7jc2AibTQKB4dwftjiKXC9RP" 1337 - -# OR using cargo directly (from $DIPPER_SOURCE_ROOT): -cargo run --bin dipper-cli -- requests register "QmNngXzFajkQHRj3ZjAJAF7jc2AibTQKB4dwftjiKXC9RP" 1337 - -# Expected output: -# Creating indexing request for deployment ID: DeploymentId(QmNngXzFajkQHRj3ZjAJAF7jc2AibTQKB4dwftjiKXC9RP) -# Created indexing request with ID: 01983d54-a2a0-7933-a4f5-bb96d7f4dd52 -``` - -### 2. Verify Registration - -```bash -# Using wrapper script (from repo root): -./scripts/dipper-cli.sh requests list - -# OR using cargo directly (from $DIPPER_SOURCE_ROOT): -cargo run --bin dipper-cli -- requests list - -# Expected output: JSON array showing your indexing request with status "OPEN" -# Example: -# [{ -# "id": "01983d54-a2a0-7933-a4f5-bb96d7f4dd52", -# "status": "OPEN", -# "requested_by": "0xf4ef6650e48d099a4972ea5b414dab86e1998bd3", -# "deployment_id": "QmNngXzFajkQHRj3ZjAJAF7jc2AibTQKB4dwftjiKXC9RP" -# }] -``` - -### 3. Check Dipper Logs - -Monitor dipper service logs for payment processing: - -```bash -# Watch for indexing registration and payment activity -docker compose logs -f dipper - -# Or filter for specific events: -docker compose logs -f dipper | grep -E "(payment|receipt|indexing|registered)" - -# Expected log patterns: -# - "Indexing request registered" -# - "Processing payment" -# - "Receipt validated" -``` - -### 4. Verify Database State - -Check PostgreSQL for indexing payment data: - -```bash -docker compose exec postgres psql -U postgres -d dipper -c "SELECT * FROM indexing_requests;" -``` - -### 5. Verify Indexer Allocation - -Indexing Payments is about paying for indexing, not queries. To verify the agreement is working, check if the indexer has allocated to the subgraph: - -```bash -# Query the network subgraph to check allocations -curl -s http://localhost:8000/subgraphs/name/graph-network -X POST \ - -H 'content-type: application/json' \ - -d '{ - "query": "{ indexer(id: \"0xf4ef6650e48d099a4972ea5b414dab86e1998bd3\") { allocations { id subgraphDeployment { ipfsHash } status } } }" - }' | jq . - -# Look for an allocation with your deployment ID -# Note: This can take several minutes as the indexer-agent processes the agreement -``` - -**Important**: Check indexer-agent logs while waiting: - -```bash -docker logs indexer-agent --tail 50 -f | grep -E "(allocation|QmNng|agreement)" -``` - -**Timing**: The indexer-agent runs on a cycle and may take 5-10 minutes to create the allocation after the indexing payment agreement is established. - -### 6. Cancel an Indexing Request - -```bash -# Get the UUID from the list command -cargo run --bin dipper-cli -- requests cancel - -# Example: -cargo run --bin dipper-cli -- requests cancel 01983d54-a2a0-7933-a4f5-bb96d7f4dd52 -``` - -## Verification Steps - -1. **Dipper Health**: Check endpoint returns 405 (expected for root path): `curl http://localhost:9000/` -2. **Agreement Created**: Look for "Agreement proposal accepted" in dipper logs -3. **Indexer Allocation**: Query network subgraph for active allocations -4. **Indexer Agent Activity**: Monitor logs for allocation creation -5. **Payment Flow**: Admin → Dipper → Indexer Service (port 7602) → Indexer Agent - -## Common Issues - -### Dipper Not Starting - -- Verify `indexing-payments` profile is in COMPOSE_PROFILES -- Check `DIPPER_VERSION` is set in `.env` -- Check logs: `docker compose logs dipper` -- Ensure Postgres is healthy and migrations completed - -### Authentication Errors - -- Verify `INDEXING_SIGNING_KEY` is set correctly -- Ensure `RECEIVER_SECRET` is available in .env -- Check `INDEXING_SERVER_URL` includes the port -- Try: `echo $INDEXING_SIGNING_KEY` to verify it's set - -### CLI Connection Issues - -- Ensure dipper service is healthy: `docker compose ps dipper` -- Check admin RPC is accessible: `curl http://localhost:9000/` -- Verify port mapping in docker-compose.yaml - -### Environment Variable Issues - -- **"missing field 'server_url'"**: Environment variables not set -- Remember: Variables must be set for EVERY dipper-cli command -- If switching terminals/sessions, re-export the variables -- Alternative: Create a shell script that sets variables and runs commands - -### No Payment Activity - -- Ensure gateway is healthy and can route queries -- Verify indexer-service has the indexing payment RPC port exposed (7602) -- Check that an allocation exists for the subgraph -- Look for errors in indexer-service logs: `docker compose logs indexer-service` - -## Cleanup - -```bash -# Stop watching logs/consumers -# Ctrl+C to exit - -# Optionally restart services -docker compose restart dipper gateway -``` diff --git a/docs/flows/README.md b/docs/flows/README.md index 1f351384..f1a0296d 100644 --- a/docs/flows/README.md +++ b/docs/flows/README.md @@ -1,50 +1,27 @@ -# Testing Flows - -This directory contains step-by-step guides for testing specific features and workflows in the local-network environment. - -## Available Flows - -### [Indexing Payments Testing](./IndexingPaymentsTesting.md) -Test the Indexing Payments system including: -- Setting up dipper credentials -- Registering indexing requests -- Monitoring payment flows -- Verifying receipt aggregation - -### [Eligibility Oracle Testing](./EligibilityOracleTesting.md) -Test the Rewards Eligibility Oracle (REO) end-to-end cycle: -- Verifying deny-by-default (indexer not eligible) -- Sending gateway queries to generate eligibility data -- REO node evaluation and on-chain submission -- Verifying indexer becomes eligible -- Automated script: `./scripts/test-reo-eligibility.sh` - -### [Indexer Setup](./indexer-setup.md) *(coming soon)* -Complete workflow for setting up a new indexer including: -- Indexer registration -- Allocation management -- Cost model configuration -- Health monitoring - -### [Subgraph Deployment](./start-indexing.md) *(coming soon)* -Deploy and test subgraphs including: -- IPFS upload verification -- Allocation creation -- Query testing -- Indexing status monitoring - -### [Gateway Testing](./gateway-testing.md) *(coming soon)* -Test gateway query routing including: -- Query submission with payment -- Receipt verification -- Load balancing behavior -- Error handling - -## Creating New Flow Documentation - -When documenting a new flow, include: +# Testing flows + +Step-by-step guides for testing local-network features and workflows. + +## Available flows + +### [Indexer Agent Testing](./IndexerAgentTesting.md) + +Run the upstream indexer-agent test suite against a Postgres started by +`scripts/test-indexer-agent.sh`. Requires a local clone of +graphprotocol/indexer in `$INDEXER_AGENT_SOURCE_ROOT`. + +## Other test coverage + +- **Rust integration tests** under [`../../tests/`](../../tests/) — the network's + own behaviour (allocations, eligibility, rewards, denial, REO governance). + See [tests/README.md](../../tests/README.md). + +## Creating new flow documentation + +Include: + 1. Prerequisites (services that must be running, initial state) 2. Step-by-step commands with expected outputs 3. Verification steps 4. Common issues and troubleshooting -5. Cleanup procedures \ No newline at end of file +5. Cleanup procedures diff --git a/docs/indexing-payments/RecurringCollectorDeployment.md b/docs/indexing-payments/RecurringCollectorDeployment.md deleted file mode 100644 index 58c41a10..00000000 --- a/docs/indexing-payments/RecurringCollectorDeployment.md +++ /dev/null @@ -1,71 +0,0 @@ -# RecurringCollector Deployment — Outstanding Work - -Status: **not yet deployed** in local network or production. - -Dipper references `recurring_collector` in its config but currently uses the null address. -The contract source exists in the `rem-baseline-merge` contracts branch but is not wired -into any deployment path. - -## Contracts repo (`graphprotocol/contracts`) - -### 1. Ignition modules (local network / Hardhat) - -The `deploy:protocol` Hardhat task deploys SubgraphService via Ignition modules. -The SubgraphService Solidity constructor now expects a 5th parameter (`recurringCollector`), -but the Ignition module still passes only 4 — deployment will fail on the current baseline. - -Commit `f3fdc5114` ("feat: add RecurringCollector, indexingFeesCut, and library linking to -ignition deployment") adds the required Ignition wiring but is **not merged** into the -baseline branch. It needs to be cherry-picked or merged. That commit adds: - -- `packages/horizon/ignition/modules/core/RecurringCollector.ts` -- RecurringCollector import in `core.ts` -- 5th constructor arg in `SubgraphService.ts` Ignition module -- Config patching in `deploy.ts` task - -### 2. Deployment package (production / testnet) - -`packages/deployment/deploy/service/subgraph/01_deploy.ts` constructs SubgraphService with -4 args (Controller, DisputeManager, GraphTallyCollector, Curation). Once the contract -requires 5, this script must also be updated: - -- Add RecurringCollector to the contract registry or fetch it as a dependency -- Deploy RecurringCollector (or reference an existing deployment) before SubgraphService -- Pass `recurringCollectorAddress` as the 5th constructor arg -- Update `02_upgrade.ts` if the upgrade path needs the new implementation - -`Directory.sol` gains an immutable `RECURRING_COLLECTOR` field and a -`recurringCollector()` getter. Since Solidity immutables are embedded in bytecode -(not storage), this does not break storage layout — it's a standard proxy -implementation upgrade via `upgradeAndCall()`. - -## Local network (`rem-local-network`) - -After the contracts branch includes RecurringCollector in Ignition: - -1. **`.env`** — update `CONTRACTS_COMMIT` to the new contracts commit -2. **`containers/core/graph-contracts/run.sh`** — extract RecurringCollector address from - the deployed address book (likely `horizon.json`) -3. **`containers/indexing-payments/dipper/run.sh`** — replace null address with: - ```bash - recurring_collector=$(contract_addr RecurringCollector.address horizon) - ``` - -## Dipper - -No code changes needed — Dipper already has full RCA support (EIP-712 signing, agreement -lifecycle, chain listener, on-chain cancellation). It uses hand-written `sol!` macro -bindings, not a contracts submodule, so no dependency to bump. It just needs the real -contract address in its config. - -## Summary of blocking order - -``` -contracts: merge Ignition commit (f3fdc5114) into baseline - ↓ -contracts: update deployment package for 5-arg SubgraphService - ↓ -local-network: bump CONTRACTS_COMMIT, wire RecurringCollector address - ↓ -dipper config picks up real address — RCA functional end-to-end -``` diff --git a/docs/indexing-payments/archive/IntegrationSummary.md b/docs/indexing-payments/archive/IntegrationSummary.md deleted file mode 100644 index 08e1c05c..00000000 --- a/docs/indexing-payments/archive/IntegrationSummary.md +++ /dev/null @@ -1,51 +0,0 @@ -# Indexing Payments Integration - -ARCHIVED: This document describes the initial integration. Current setup uses published images (no submodules). - -## Quick Start - -To enable Indexing Payments on local-network: - -```bash -# Enable indexing-payments profile in .env: -# COMPOSE_PROFILES=indexing-payments -docker compose up -``` - -## What Was Implemented - -**Infrastructure (Phase 1):** - -- Database: `dipper_1` created in postgres -- Environment: `DIPPER_ADMIN_RPC_PORT`, `DIPPER_INDEXER_RPC_PORT` -- Documentation: [docs/indexing-payments/](../indexing-payments/), [docs/flows/](../../flows/) -- Scripts: merge-contracts, dipper-cli, test helpers - -**Service Files (Phase 2):** - -- [containers/indexing-payments/dipper/Dockerfile](../../../containers/indexing-payments/dipper/Dockerfile) - Wrapper image -- [containers/indexing-payments/dipper/run.sh](../../../containers/indexing-payments/dipper/run.sh) - Configuration script - -**Override (Phase 3):** - -- Service definitions in `docker-compose.yaml` with `profiles: [indexing-payments]` - -**Documentation (Phase 4):** - -- Updated [README.md](../../README.md) -- All terminology updated (DIPs → Indexing Payments) - -## Notes - -Dipper now uses a published image (`ghcr.io/edgeandnode/dipper-service`). No submodule required. - -## Documentation - -- Enable via `COMPOSE_PROFILES=indexing-payments` in `.env` -- [docs/indexing-payments/](../) - Architecture & implementation plans -- [docs/flows/IndexingPaymentsTesting.md](../../flows/IndexingPaymentsTesting.md) - Testing guide - -## Next Steps - -1. Build: `docker compose build dipper iisa-mock` -2. Test: Follow [docs/flows/IndexingPaymentsTesting.md](../../flows/IndexingPaymentsTesting.md) diff --git a/docs/indexing-payments/archive/UserExperience.md b/docs/indexing-payments/archive/UserExperience.md deleted file mode 100644 index a03c8f5d..00000000 --- a/docs/indexing-payments/archive/UserExperience.md +++ /dev/null @@ -1,70 +0,0 @@ -# Indexing Payments User Experience - -**What changes when using Indexing Payments override?** - -## TL;DR - -- **Default:** Graph Tally for query fees (no change) -- **With Override:** Graph Tally + Indexing Payments (dipper service added) -- **Enable:** Set `COMPOSE_PROFILES=indexing-payments` in `.env`, then `docker compose up` - -## Key Differences - -### Payment Methods - -| Aspect | Graph Tally (Default) | Indexing Payments (Override) | -| ------------ | ---------------------- | ----------------------------- | -| **Use Case** | Query fees | Indexing fees | -| **Method** | Allocations + receipts | GRT transfers | -| **Capital** | $50-$1000 allocations | Minimal (just payment amount) | -| **Response** | Synchronous | Asynchronous (receipt ID) | -| **Burn** | No burn | 1% protocol burn | - -### What's Added - -**New Service:** - -- `dipper` container running on ports 9000 (admin) and 9001 (indexer) - -**New Database:** - -- `dipper_1` database in postgres (unused in default setup) - -**New Workflow:** - -1. Admin registers indexing request -2. Indexer receives request via dipper -3. Indexer performs work, submits report -4. Dipper processes payment, returns receipt ID -5. Indexer polls receipt status (PENDING → SUBMITTED/FAILED) - -### What Stays the Same - -- All default services run unchanged -- Graph Tally query fees continue working -- Graph node, gateway, indexer-agent unaffected -- Can switch back to default by stopping override - -## Usage Comparison - -**Default (Graph Tally Only):** - -```bash -docker compose up -# All services except dipper -``` - -**With Indexing Payments:** - -```bash -# Set COMPOSE_PROFILES=indexing-payments in .env -docker compose up -# All services including dipper -``` - -## Documentation - -For detailed architecture and testing, see: - -- [Architecture](../safe-based/Architecture.md) -- [Testing Guide](../../flows/IndexingPaymentsTesting.md) diff --git a/docs/indexing-payments/safe-based/Architecture.md b/docs/indexing-payments/safe-based/Architecture.md deleted file mode 100644 index 347fd814..00000000 --- a/docs/indexing-payments/safe-based/Architecture.md +++ /dev/null @@ -1,286 +0,0 @@ -# Indexing Payments Architecture - -## Overview - -This document describes the architecture for implementing Safe-based on-chain payments for Indexing Payments in The Graph Protocol. This implementation replaces Graph Tally for indexing fees due to impractical allocation requirements. - -## Background - -Graph Tally has the following critical issues for Indexing Payments: - -- **High Capital Requirements**: $50-$1000 allocations needed for $5-$100 monthly payments -- **Complex Allocation Management**: Variable allocation amounts create operational complexity -- **Capital Inefficiency**: Large amounts of stake must be kept free for indexing payments -- **Missing Infrastructure**: Graph Tally escrow management functionality not yet implemented - -## Architecture Overview - -### Core Design Principles - -1. **Asynchronous Processing**: Non-blocking receipt ID system for immediate responses -2. **Safe Module Pattern**: Direct execution via `execTransactionFromModule` without multi-sig complexity -3. **State Machine**: Clear PENDING → SUBMITTED/FAILED status tracking -4. **Protocol Compliance**: 1% burn on all payments -5. **Clear Separation**: Indexing payments use Safe payments, query fees continue using Graph Tally - -### System Components - -``` -┌─────────────┐ RPC ┌──────────────┐ Worker ┌──────────────┐ -│ Indexer │ ──────────> │ Dipper │ ──────────> │ Payment │ -│ Agent │ <────────── │ Service │ │ Handler │ -└─────────────┘ Receipt ID └──────────────┘ └──────────────┘ - │ │ │ - │ Poll Status │ │ - ▼ ▼ ▼ -┌─────────────┐ ┌──────────────┐ ┌──────────────┐ -│ Receipt │ │ Receipt │ │ Safe Module │ -│ Storage │ │ Registry │ │ Client │ -└─────────────┘ └──────────────┘ └──────────────┘ - │ │ - │ ▼ - │ ┌──────────────┐ - └──────────────────────>│ Blockchain │ - Status Update │ (GRT + Burn) │ - └──────────────┘ -``` - -## Payment Flow - -### 1. Payment Collection (Indexer → Dipper) - -The indexer initiates payment collection by reporting completed work: - -``` -Indexer sends collect_payment request: -- Agreement ID -- Work metrics (entity count, etc.) -- Indexer address -``` - -### 2. Receipt Creation (Dipper Service) - -Dipper validates the work and creates a receipt: - -1. Validate work report against agreement -2. Calculate payment amount including 1% burn -3. Create receipt record with PENDING status -4. Queue PayOnChain job for async processing -5. Return Receipt ID immediately - -### 3. Asynchronous Payment Processing (Worker) - -Worker processes payments in the background: - -1. Retrieve receipt from registry -2. Verify PENDING status (skip if already processed) -3. Calculate exact burn amount (1% of total) -4. Submit payment via Safe Module: - - Transfer GRT to indexer - - Burn 1% to protocol address -5. Update receipt status: - - SUBMITTED with transaction hash on success - - FAILED with error message on failure - -### 4. Status Polling (Indexer) - -Indexers poll for payment status: - -1. Request status using Receipt ID -2. Receive current status and details -3. For SUBMITTED status, get transaction hash -4. Verify payment on-chain if desired - -## State Machine - -``` - ┌─────────┐ - │ PENDING │──────┐ - └────┬────┘ │ - │ │ - Submit│ │Fatal - Success│ │Error - │ │ - ▼ ▼ - ┌──────────┐ ┌────────┐ - │SUBMITTED │ │ FAILED │ - └──────────┘ └────────┘ -``` - -**State Definitions:** - -- **PENDING**: Receipt created, payment queued for processing -- **SUBMITTED**: Payment successfully submitted to blockchain with transaction hash -- **FAILED**: Payment failed due to error (gas, balance, revocation, etc.) - -## Safe Module Implementation - -### Configuration - -The Safe Module pattern requires: - -- Dipper EOA authorized as Safe Module -- Safe contract holding GRT tokens -- Module can execute transactions directly -- No multi-sig coordination required - -### Transaction Execution - -Payments are executed as batch operations using the Safe MultiSend contract: - -1. **Batch Construction**: - - Create array of operations (GRT transfer + 1% burn) - - Encode using MultiSend contract interface -2. **Execution Flow**: - - Module calls `execTransactionFromModule` on the Safe - - Safe delegatecalls to MultiSend contract - - MultiSend executes both operations atomically: - - Transfer GRT to indexer - - Burn 1% to protocol address - -3. **Benefits**: - - Single transaction for multiple operations - - Atomic execution (all or nothing) - - Gas efficient batching - - Standard Safe pattern - -## Security Considerations - -### Access Control - -- Safe Module authorization can be revoked by Safe owners -- Module limited to specific operations (GRT transfers) -- No ability to modify Safe configuration - -### Key Management - -- Module signer key stored securely (environment variables) -- Regular key rotation capability -- No hardcoded keys in code - -### Transaction Security - -- Nonce management prevents replay attacks -- Gas price limits prevent excessive fees -- Amount validation ensures correct payments - -## Database Schema - -### Receipt Status Tracking - -```sql --- Enhanced receipts table -ALTER TABLE indexing_receipts -ADD COLUMN payment_status TEXT DEFAULT 'PENDING', -ADD COLUMN transaction_hash TEXT, -ADD COLUMN payment_submitted_at TIMESTAMP, -ADD COLUMN payment_error TEXT, -ADD COLUMN retry_count INTEGER DEFAULT 0; - -CREATE INDEX idx_receipts_payment_status ON indexing_receipts(payment_status); -``` - -## API Specifications - -### gRPC Interface (Indexer Service → Dipper) - -#### CollectPaymentRequest (Unchanged) - -```protobuf -message CollectPaymentRequest { - uint64 version = 1; - bytes signed_collection = 2; // ERC-712 signed work report -} -``` - -#### CollectPaymentResponse (Modified) - -```protobuf -message CollectPaymentResponse { - uint64 version = 1; - string receipt_id = 2; // Receipt ID for polling (replaces graph_tally_receipt) - string amount = 3; // Total amount including burn - string status = 4; // Initial status: "PENDING" -} -``` - -### Dipper gRPC Interface Extension - -#### GetReceiptById (New RPC method) - -```protobuf -message GetReceiptByIdRequest { - uint64 version = 1; - string receipt_id = 2; -} - -message GetReceiptByIdResponse { - uint64 version = 1; - string receipt_id = 2; - string status = 3; // "PENDING" | "SUBMITTED" | "FAILED" - string transaction_hash = 4; // Present when SUBMITTED - string error_message = 5; // Present when FAILED - string amount = 6; - string payment_submitted_at = 7; // ISO timestamp when SUBMITTED -} -``` - -## Configuration - -### Dipper Service - -```yaml -safe_payment: - safe_address: "0x..." # Safe with module - grt_token_address: "0x..." # GRT token contract - module_signer_key: "${KEY}" # Module EOA key - rpc_url: "https://..." # Ethereum RPC - burn_percentage: 1 # Protocol tax - min_payment_amount_grt: "50" # Minimum payment -``` - -### Indexer Agent - -```yaml -payment_collection: - dipper_endpoint: "http://dipper:8000" -``` - -## Components to Modify - -### 1. Dipper Service (Gateway Side) - -The dipper service requires the most significant changes as it manages the payment flow: - -- **Receipt Registry**: Add payment status tracking (PENDING/SUBMITTED/FAILED) -- **Worker System**: Add PayOnChain message handler for async payment processing -- **Safe Module Client**: Implement GRT transfers with 1% burn via execTransactionFromModule -- **RPC Interface**: Return Receipt IDs instead of Graph Tally receipts, add polling endpoint - -### 2. Indexer Agent - -The indexer agent needs updates to handle the new async payment pattern: - -- **Payment Collector**: Replace Graph Tally receipt storage with Receipt ID tracking -- **Polling Mechanism**: Add background task to poll for payment status -- **Database Models**: Update to store Receipt IDs and payment status -- **RPC Client**: Update to handle new response format and polling endpoint - -### 3. Indexer Service - -The indexer service requires minimal changes, primarily to the gRPC protocol definitions: - -- **Protocol Update**: Modify `CollectPaymentResponse` in `gateway.proto` to return Receipt ID -- **Response Handling**: Update response structure to include status field -- **No Core Logic Changes**: The indexer service acts as a pass-through for indexing payments - -## Success Criteria - -The implementation succeeds when: - -- ✅ Non-blocking payment requests with Receipt IDs -- ✅ Asynchronous GRT transfers via Safe Module -- ✅ 1% protocol burn on all payments -- ✅ Clear state machine transitions -- ✅ Indexers can verify payments on-chain -- ✅ Complete separation from Graph Tally for indexing fees diff --git a/docs/indexing-payments/safe-based/DipperServicePlan.md b/docs/indexing-payments/safe-based/DipperServicePlan.md deleted file mode 100644 index bbb24ccc..00000000 --- a/docs/indexing-payments/safe-based/DipperServicePlan.md +++ /dev/null @@ -1,308 +0,0 @@ -# Dipper Service Implementation Plan - -## Overview - -This plan details the changes required to the Dipper service to implement Safe-based GRT payments for Indexing Payments, replacing the current Graph Tally receipt system. - -## Current State - -The Dipper service currently: -- Generates Graph Tally receipts for payment collection -- Returns receipts synchronously -- Has worker infrastructure for async tasks -- Stores receipts in PostgreSQL database - -## Required Changes - -### 1. Database Schema Updates - -**Migration**: `dipper/source/migrations/20250130_payment_status.sql` - -```sql --- Add payment tracking to receipts -ALTER TABLE indexing_receipts -ADD COLUMN payment_status TEXT DEFAULT 'PENDING' - CHECK (payment_status IN ('PENDING', 'SUBMITTED', 'FAILED')), -ADD COLUMN transaction_hash TEXT, -ADD COLUMN payment_submitted_at TIMESTAMP, -ADD COLUMN payment_error TEXT, -ADD COLUMN retry_count INTEGER DEFAULT 0; - --- Indexes for efficient queries -CREATE INDEX idx_receipts_payment_status ON indexing_receipts(payment_status); -CREATE INDEX idx_receipts_transaction_hash ON indexing_receipts(transaction_hash); -``` - -### 2. Registry Updates - -**Location**: `dipper/source/bin/dipper-service/src/store/registry.rs` - -Add payment status management: - -``` -New methods needed: -- update_receipt_payment_status(): Atomic status updates -- get_receipt_with_status(): Retrieve receipt with payment info -- get_receipts_by_status(): Query receipts by status -- increment_retry_count(): Track retry attempts -``` - -### 3. Worker System Integration - -**Location**: `dipper/source/bin/dipper-service/src/worker/` - -#### Add PayOnChain Message Type - -``` -WorkerMessage enum addition: - PayOnChain { - receipt_id: ReceiptId, - amount: U256, - recipient: Address, - agreement_id: AgreementId, - } -``` - -#### Implement Payment Handler - -``` -Payment handler logic: -1. Verify receipt is still PENDING -2. Calculate 1% burn amount -3. Submit payment via Safe Module -4. Update receipt status based on result -5. Handle retries for transient errors -``` - -### 4. Safe Module Client - -**Location**: `dipper/source/bin/dipper-service/src/safe_client/` - -Replace stub with working implementation: - -``` -Safe Module client needs: -- Initialize with Safe, GRT token, and MultiSend contracts -- Build batch transaction using MultiSend: - 1. Encode GRT transfer operation - 2. Encode 1% burn operation - 3. Pack operations into MultiSend call data -- Execute via execTransactionFromModule: - - Target: Safe contract - - Operation: delegatecall to MultiSend - - Data: Encoded batch operations -- Handle nonce management for module transactions -- Return transaction hash after confirmation -``` - -### 5. gRPC Interface Updates - -**Location**: `dipper/source` (proto files and service implementation) - -#### Update CollectPayment - -``` -Changes: -1. Create receipt with PENDING status -2. Queue PayOnChain worker job -3. Return Receipt ID instead of Graph Tally receipt in CollectPaymentResponse -4. Include initial status in response -``` - -#### Add GetReceiptById - -``` -New gRPC method: -- Add to proto definition -- Input: GetReceiptByIdRequest with receipt_id -- Query receipt with current status from database -- Return GetReceiptByIdResponse with status, transaction hash, error info -- Handle not found gracefully -``` - -### 6. Configuration - -**Location**: `dipper/source/bin/dipper-service/src/config.rs` - -``` -Safe payment configuration: -- safe_address: Address of Safe contract -- grt_token_address: GRT token contract -- module_signer_key: Private key for module EOA -- rpc_url: Ethereum RPC endpoint -- burn_percentage: 1% protocol tax -- gas_settings: Limits and pricing -``` - -## Implementation Phases - -### Phase 1: Database & Registry -1. Create and run migration -2. Update registry trait and implementation -3. Add status update methods -4. Test atomic operations - -### Phase 2: Worker Integration -1. Add PayOnChain message type -2. Implement payment handler -3. Add retry logic -4. Integrate with registry - -### Phase 3: Safe Client -1. Set up contract interfaces (Safe, GRT, MultiSend) -2. Implement batch transaction building: - - Encode ERC20 transfer call for GRT to indexer - - Encode ERC20 transfer call for 1% burn - - Pack both into MultiSend.multiSend() call data -3. Add execTransactionFromModule calls: - - Target: Safe contract - - Value: 0 (no ETH) - - Data: Delegatecall to MultiSend with packed operations - - Operation: 1 (delegatecall) -4. Handle nonce and gas management - -### Phase 4: RPC Updates -1. Modify collect_payment flow -2. Add polling endpoint -3. Remove Graph Tally generation -4. Update response types - -### Phase 5: Testing -1. Unit tests for all components -2. Integration tests -3. Local network testing -4. Testnet deployment - -## Testing Strategy - -### Unit Tests -``` -Test coverage needed: -- Registry status updates -- Worker message handling -- Safe transaction building -- 1% burn calculations -- RPC response formatting -``` - -### Integration Tests -``` -End-to-end scenarios: -- Receipt creation and status updates -- Payment execution flow -- Error handling and retries -- Concurrent payment processing -- Database consistency -``` - -### Local Testing -``` -Validation steps: -1. Deploy with test Safe -2. Configure module authorization -3. Fund with test GRT -4. Process test payments -5. Verify on local chain -``` - -## Security Considerations - -### Key Management -- Module key in environment variable -- No hardcoded keys -- Rotation capability - -### Transaction Security -- Validate payment amounts -- Check Safe authorization -- Monitor gas prices -- Handle reverted transactions - -### Access Control -- Verify indexer signatures -- Rate limit requests -- Validate work reports - -## Monitoring - -### Metrics -``` -Key metrics: -- receipt_creation_rate -- payment_processing_time -- payment_success_rate -- retry_count_by_error -- gas_cost_per_payment -``` - -### Logging -``` -Important events: -- Receipt creation -- Payment submission -- Status transitions -- Transaction hashes -- Error details -``` - -### Alerts -``` -Alert conditions: -- High failure rate -- Stuck PENDING receipts -- Low Safe balance -- Module authorization issues -``` - -## Rollback Plan - -1. Keep Graph Tally code available -2. Feature flag for payment method -3. Database backups before migration -4. Manual payment capability -5. Clear rollback procedures - -## Dependencies - -### External -- Ethereum RPC provider -- Safe Module authorization -- GRT token in Safe -- Gas for transactions - -### Internal -- Worker system operational -- Database available -- Registry functioning -- RPC server running - -## Configuration Example - -```toml -[safe_payment] -safe_address = "0x1234..." # Safe contract with dipper EOA as module -grt_token_address = "0x5678..." # GRT token contract -multisend_address = "0xA238..." # Safe MultiSend contract -module_signer_key = "${SAFE_MODULE_KEY}" # Private key for module EOA -rpc_url = "https://sepolia.infura.io/v3/${KEY}" -burn_percentage = 1 -burn_address = "0x0000000000000000000000000000000000000000" # Or protocol burn address -min_payment_amount_grt = "50000000000000000000" # 50 GRT - -[safe_payment.gas] -max_price_gwei = 100 -limit = 300000 # Higher limit for batch operations - -[worker] -payment_retry_attempts = 3 -payment_retry_delay_seconds = 60 -``` - -## Success Criteria - -- Receipt IDs returned immediately -- Payments process within 60 seconds -- State machine works correctly -- 1% burn executed properly -- No Graph Tally receipts for indexing payments -- Transactions verifiable on chain \ No newline at end of file diff --git a/docs/indexing-payments/safe-based/IndexerAgentPlan.md b/docs/indexing-payments/safe-based/IndexerAgentPlan.md deleted file mode 100644 index 91ea554b..00000000 --- a/docs/indexing-payments/safe-based/IndexerAgentPlan.md +++ /dev/null @@ -1,305 +0,0 @@ -# Indexer Agent Implementation Plan - -## Overview - -This plan details the changes required to the Indexer Agent to support the new Safe-based Indexing Payments system. The implementation replaces Graph Tally receipt collection with a Receipt ID polling mechanism. - -## Development Branch - -**Branch**: `dips-horizon-rebase` - -This work will continue on top of the existing `dips-horizon-rebase` branch, which already contains indexing payments improvements. - -## Current State - -The Indexer Agent currently: -- Uses payment collector class to collect Graph Tally receipts from the gateway -- Stores Graph Tally receipts locally for later redemption -- Makes synchronous calls expecting immediate receipt data -- Has existing database models for tracking receipts - -## Required Changes - -### 1. Database Schema Updates - -**Location**: `indexer-agent/source/packages/indexer-common/src/indexer-management/models/` - -Create a new model for indexing payment receipts: - -``` -IndexingPaymentReceipt model: -- receiptId: string (primary key - unique identifier from dipper) -- agreementId: foreign key to indexing_agreements -- amount: bigint (payment amount in GRT wei) -- status: enum ['PENDING', 'SUBMITTED', 'FAILED'] -- transactionHash: string (optional, populated when SUBMITTED) -- errorMessage: string (optional, populated when FAILED) -- createdAt: timestamp -- updatedAt: timestamp -``` - -**Note**: These schema changes will also need to be mirrored in the indexer-service migrations for Rust testing compatibility. - -### 2. Update Payment Collector Class - -**Location**: `indexer-agent/source/packages/indexer-common/src/indexing-fees/indexing-payments.ts` - -#### Remove Graph Tally Dependencies - -- Remove all Graph Tally receipt handling code -- Remove receipt signature verification -- Remove Graph Tally-specific imports and types - -#### Implement Receipt ID Collection - -Update the payment collection flow: - -``` -async collectPayment(agreement): - 1. Calculate work metrics (entity count, etc.) - 2. Call dipper.collect_payment with work report - 3. Receive Receipt ID and initial status - 4. Store Receipt ID in database with PENDING status - 5. Return immediately (no polling here) -``` - -**Critical**: The collectPayment method does NOT start any polling. All polling is handled by a single background task that processes all pending receipts together. - -### 3. Update gRPC Client - -**Location**: `indexer-agent/source/packages/indexer-common/src/indexing-fees/gateway-indexing-service-client.ts` - -#### Update gRPC Calls - -Extend the Dipper gRPC client: - -``` -CollectPayment: - Input: Same as before (work report) - Output: { - receiptId: string - amount: string - status: 'PENDING' - } - -GetReceiptById (new): - Input: { receiptId: string } - Output: { - receiptId: string - status: 'PENDING' | 'SUBMITTED' | 'FAILED' - transactionHash?: string - errorMessage?: string - amount: string - } -``` - -### 4. Update collectAllPayments Method - -**Location**: `indexer-agent/source/packages/indexer-common/src/indexing-fees/indexing-payments.ts` - -Extend the existing `collectAllPayments` method to also poll for pending receipts: - -``` -async collectAllPayments(): - // Existing logic - collect new payments - 1. Find outstanding agreements - 2. For each agreement: tryCollectPayment() - - // New logic - poll pending receipts - 3. Query database for ALL PENDING receipts - 4. For each receipt: - - Call dipper.get_receipt_by_id - - Update database with new status if changed - - Log state transitions -``` - -**Benefits of this approach**: -- Reuses existing periodic task (runs every 60 seconds) -- Keeps all indexing payment logic in one place -- No need for separate background task -- Natural fit since both operations deal with payment lifecycle -- Simplifies the implementation - -### 5. Monitoring and Logging - -**Location**: Throughout the codebase - -Add comprehensive logging: - -``` -Log Events: -- Payment request initiated -- Receipt ID received -- Each polling attempt -- Status transitions -- Transaction hash when SUBMITTED -- Error details when FAILED -- Polling attempts -``` - -Add metrics: - -``` -Metrics to track: -- payment_requests_total -- receipt_status_transitions -- polling_duration_seconds -- payment_success_rate -- payment_failure_reasons -``` - -## Implementation Steps - -### Phase 1: Database and Models - -1. Create migration for new receipt fields -2. Update IndexingPaymentReceipt model -3. Add status enum type -4. Test database changes - -### Phase 2: RPC Client Updates - -1. Add GetReceiptById method to RPC client -2. Update CollectPayment response handling -3. Remove Graph Tally-specific response parsing -4. Add proper error handling - -### Phase 3: Core Collection Logic - -1. Refactor Payment Collector class -2. Remove all Graph Tally receipt logic -3. Implement Receipt ID storage -4. Add polling mechanism -5. Handle all status transitions - -### Phase 4: Extend collectAllPayments - -1. Add receipt polling logic to existing method -2. Implement timeout handling for stale receipts -3. Add batch status checking -4. Ensure fault tolerance - -### Phase 5: Testing and Validation - -1. Unit tests for new collection flow -2. Integration tests with mock dipper -3. Test status polling mechanism -4. Test error scenarios -5. Performance testing - -## Testing Plan - -### Unit Tests - -``` -Test Cases: -- Receipt ID storage and retrieval -- Status update logic -- Polling state tracking -- Error propagation -``` - -### Integration Tests - -``` -Test Scenarios: -- Full payment flow with mock dipper -- Status transitions (PENDING → SUBMITTED) -- Failure scenarios (PENDING → FAILED) -- Network interruption handling -- Concurrent receipt polling -``` - -### Manual Testing - -``` -Validation Steps: -1. Deploy to local-network -2. Create test indexing agreement -3. Trigger payment collection -4. Monitor Receipt ID creation -5. Verify polling behavior -6. Check final transaction on chain -``` - -## Configuration Changes - -### Environment Variables - -```bash -# Dipper endpoint -DIPPER_ENDPOINT=http://dipper:8000 -``` - -### Command Line Arguments - -```bash -indexer-agent start \ - --dipper-endpoint http://dipper:8000 -``` - -## Error Handling - -### Transient Errors -- Network timeouts: Retry polling -- Dipper unavailable: Exponential backoff -- Database errors: Log and retry - -### Fatal Errors -- Invalid Receipt ID: Log error -- Authentication errors: Alert operator - -## Migration Considerations - -### Backward Compatibility -- Keep Graph Tally code for query fees -- Add feature flag for new payment system -- Support gradual rollout - -### Data Migration -- No migration needed for existing Graph Tally receipts -- New receipts use Receipt ID system -- Clear separation in database - -## Success Metrics - -### Functional Metrics -- All payments create Receipt IDs -- Status polling works reliably -- Transactions appear on chain -- No Graph Tally receipts for indexing fees - -### Performance Metrics -- Receipt creation < 1 second -- Status updates within 30 seconds -- Polling doesn't impact performance -- Database queries remain efficient - -## Rollback Plan - -If issues arise: -1. Disable new payment flow via feature flag -2. Revert to Graph Tally receipt collection -3. Investigate and fix issues -4. Re-deploy with fixes - -## Dependencies - -### External Dependencies -- Dipper service with new RPC methods -- Safe Module configuration on chain -- GRT tokens in Safe - -### Internal Dependencies -- Database schema changes -- RPC client updates -- Background task system - -## Implementation Order - -1. Database updates -2. RPC client changes -3. Core logic implementation -4. Background tasks -5. Testing and validation -6. Integration and deployment \ No newline at end of file diff --git a/docs/indexing-payments/safe-based/IndexerServicePlan.md b/docs/indexing-payments/safe-based/IndexerServicePlan.md deleted file mode 100644 index ebfd55a2..00000000 --- a/docs/indexing-payments/safe-based/IndexerServicePlan.md +++ /dev/null @@ -1,250 +0,0 @@ -# Indexer Service Implementation Plan - -## Overview - -This plan details the minimal changes required to the Indexer Service to support the new Safe-based Indexing Payments system. The indexer service acts primarily as a pass-through for indexing payments, so changes are limited to protocol definitions and response handling. - -## Development Branch - -**Branch**: `pcv/ipfs-dips-timeout` - -This work will continue on the existing `pcv/ipfs-dips-timeout` branch, which has been used for debugging and improving indexing payment functionality. - -## Current State - -The Indexer Service currently: -- Implements gRPC server for indexing payments using protocol buffers -- Forwards collect payment requests to the gateway -- Returns Graph Tally receipts in the response -- Has no business logic for payment processing (pass-through) - -## Required Changes - -### 1. Database Migration for Indexing Payment Receipts - -**Location**: `indexer-service/source/migrations/` - -Create a new migration for indexing payment receipts table to support testing: - -```sql --- 20250XXX_indexing_receipts.up.sql -CREATE TABLE IF NOT EXISTS indexing_receipts ( - id UUID PRIMARY KEY, - agreement_id UUID NOT NULL REFERENCES indexing_agreements(id), - receipt_id VARCHAR(255) NOT NULL UNIQUE, - amount NUMERIC(39) NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'PENDING', - transaction_hash CHAR(66), - error_message TEXT, - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL, - last_polled_at TIMESTAMP WITH TIME ZONE, - CONSTRAINT valid_status CHECK (status IN ('PENDING', 'SUBMITTED', 'FAILED')) -); - -CREATE INDEX idx_indexing_receipts_agreement_id ON indexing_receipts(agreement_id); -CREATE INDEX idx_indexing_receipts_status ON indexing_receipts(status); -CREATE INDEX idx_indexing_receipts_receipt_id ON indexing_receipts(receipt_id); -``` - -### 2. Protocol Buffer Updates - -**Location**: `indexer-service/source/crates/indexing/proto/gateway.proto` - -#### Update CollectPaymentResponse - -The main change is to modify the response to return a Receipt ID instead of Graph Tally receipt: - -```protobuf -// Current definition -message CollectPaymentResponse { - uint64 version = 1; - CollectPaymentStatus status = 2; - bytes tap_receipt = 3; -} - -// New definition -message CollectPaymentResponse { - uint64 version = 1; - CollectPaymentStatus status = 2; - string receipt_id = 3; // Receipt ID for polling - string amount = 4; // Payment amount in GRT - string payment_status = 5; // Initial status: "PENDING" -} -``` - -#### Add GetReceiptById RPC Method - -Add a new RPC method to the service definition: - -```protobuf -service GatewayIndexingService { - // ... existing methods ... - - /** - * Get the status of a payment receipt by ID. - * - * This method allows the indexer to poll for the status of a previously - * initiated payment collection. - */ - rpc GetReceiptById(GetReceiptByIdRequest) returns (GetReceiptByIdResponse); -} - -message GetReceiptByIdRequest { - uint64 version = 1; - string receipt_id = 2; -} - -message GetReceiptByIdResponse { - uint64 version = 1; - string receipt_id = 2; - string status = 3; // "PENDING" | "SUBMITTED" | "FAILED" - string transaction_hash = 4; // Present when SUBMITTED - string error_message = 5; // Present when FAILED - string amount = 6; - string payment_submitted_at = 7; // ISO timestamp when SUBMITTED -} -``` - -#### Remove Graph Tally-specific Status Codes - -Review and potentially update the `CollectPaymentStatus` enum if any codes are Graph Tally-specific: - -```protobuf -enum CollectPaymentStatus { - ACCEPT = 0; // Keep - payment request accepted - ERR_TOO_EARLY = 1; // Keep - still relevant - ERR_TOO_LATE = 2; // Keep - still relevant - ERR_AMOUNT_OUT_OF_BOUNDS = 3; // Keep - still relevant - ERR_UNKNOWN = 99; // Keep - generic error -} -``` - -### 3. Generated Code Updates - -**Location**: `indexer-service/source/crates/indexing/src/proto/` - -After updating the proto files: - -1. Regenerate the Rust bindings using the build script -2. The generated files will automatically include the new fields -3. No manual edits needed to generated code - -### 4. Client Code Updates (if any) - -**Location**: Check for any client code that constructs or handles `CollectPaymentResponse` - -Search for usage of `CollectPaymentResponse` to ensure compatibility: -- Update any code that accesses the old `tap_receipt` field -- Add handling for new fields (`receipt_id`, `amount`, `payment_status`) - -## Implementation Steps - -### Step 1: Create Database Migration - -1. Create new migration file for indexing_receipts table -2. Add down migration to drop the table -3. Test migration up and down - -### Step 2: Update Protocol Definitions - -1. Modify `gateway.proto` with new response structure -2. Remove `tap_receipt` field -3. Add `receipt_id`, `amount`, and `payment_status` fields -4. Ensure backward compatibility considerations - -### Step 3: Regenerate Protocol Bindings - -```bash -cd indexer-service/source -cargo build -p indexing -``` - -This will trigger the build script to regenerate the proto bindings. - -### Step 4: Update Response Handling - -Search for any code that constructs `CollectPaymentResponse`: - -```bash -# Find usage of CollectPaymentResponse -grep -r "CollectPaymentResponse" crates/ -``` - -Update any found usage to use the new fields. - -### Step 5: Version Compatibility - -Consider protocol version handling: -- Current version = 1 -- Decide if version bump is needed -- Document any breaking changes - -## Testing Requirements - -### Unit Tests - -1. Test proto serialization/deserialization with new fields -2. Verify generated code compiles correctly -3. Test any client code that uses the response - -### Integration Tests - -1. Test with mock gateway returning new response format -2. Verify indexer-agent can handle new response -3. Test error scenarios with new fields - -### Manual Testing - -1. Deploy updated indexer-service -2. Trigger payment collection from indexer-agent -3. Verify Receipt ID is returned -4. Confirm no Graph Tally receipts are generated - -## Configuration - -No configuration changes required for indexer-service. The service continues to forward requests to the gateway. - -## Rollback Plan - -If issues arise: -1. Revert to previous proto definitions -2. Regenerate bindings -3. Deploy previous version -4. Ensure indexer-agent compatibility - -## Dependencies - -### External Dependencies -- Gateway must implement new response format -- Indexer-agent must handle Receipt IDs - -### Build Dependencies -- Protocol buffer compiler (protoc) -- Prost build dependencies - -## Implementation Sequence - -This requires both schema and protocol changes: - -1. Create database migration for indexing_receipts table -2. Update protocol definitions in gateway.proto -3. Regenerate bindings and test compilation -4. Search and update any response handling code -5. Write and run tests -6. Integration testing with other components - -## Success Criteria - -- Protocol definitions updated with new fields -- Generated code compiles without errors -- No Graph Tally receipt field in responses -- Receipt ID returned successfully -- Integration tests pass with indexer-agent - -## Notes - -- The indexer-service remains a thin pass-through layer -- No business logic changes required -- Main change is protocol definition only -- Ensure coordination with gateway and indexer-agent teams \ No newline at end of file diff --git a/docs/indexing-payments/safe-based/README.md b/docs/indexing-payments/safe-based/README.md deleted file mode 100644 index ca6a1bdc..00000000 --- a/docs/indexing-payments/safe-based/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# Indexing Payments Documentation - -This folder contains all documentation for the Indexing Payments Safe-based payment system implementation. - -This approach is obsolete. - -## Overview - -The Indexing Payments Safe payment system replaces Graph Tally for indexing fee payments, using on-chain GRT transfers via Safe Module pattern with asynchronous processing. - -## Documentation Structure - -### Architecture - -- [`Architecture.md`](./Architecture.md) - System architecture, design principles, and component interactions - -### Implementation Plans - -- [`IndexerAgentPlan.md`](./IndexerAgentPlan.md) - Changes needed for the Indexer Agent to handle Receipt IDs and polling -- [`DipperServicePlan.md`](./DipperServicePlan.md) - Core payment processing implementation in the Dipper service -- [`IndexerServicePlan.md`](./IndexerServicePlan.md) - Minimal protocol buffer updates for the Indexer Service - -## Quick Links - -### For Indexer Agent Development - -Start with the [Indexer Agent Plan](./IndexerAgentPlan.md) which covers: - -- Receipt ID tracking -- Status polling mechanism -- Database schema updates - -### For Dipper Development - -See the [Dipper Plan](./DipperServicePlan.md) for: - -- Safe Module client implementation -- Worker-based payment processing -- Receipt status management - -### For Understanding the System - -Read the [Architecture Document](./Architecture.md) to understand: - -- Payment flow and state machine -- Security considerations -- API specifications - -## Key Concepts - -- **Receipt ID**: Replaces Graph Tally receipts, enables async processing -- **State Machine**: PENDING → SUBMITTED/FAILED -- **Safe Module**: Direct execution pattern for GRT transfers -- **1% Protocol Burn**: Automatic burn on all payments diff --git a/docs/testing/TestFramework.md b/docs/testing/TestFramework.md deleted file mode 100644 index 5e54d700..00000000 --- a/docs/testing/TestFramework.md +++ /dev/null @@ -1,245 +0,0 @@ -# Task: Test Framework for Local Network Automation - -> Created: 2026-02-20 - -## Problem - -Test automation currently uses bash scripts with custom PASS/FAIL helpers. This works well for Layers 0-1 (query validation, state observation) but will not scale to Layers 2-3 (operational lifecycle, timing-dependent flows) which require polling, retries, state tracking, parallel execution, and structured assertions. - -## Current State - -### Scripts - -| Script | Layer | Lines | Pattern | -| ------------------------------- | ----- | ----- | -------------------------- | -| `test-baseline-queries.sh` | 0 | 192 | curl + grep for errors | -| `test-indexer-guide-queries.sh` | 0 | 182 | curl + cast + grep | -| `test-baseline-state.sh` | 1 | 261 | curl + jq assertions | -| `test-reo-eligibility.sh` | 2-3 | ~200 | curl + cast + polling loop | - -### Strengths - -- Clean, readable, well-documented -- Direct access to curl, cast, docker exec, jq -- Zero compilation overhead -- Familiar to operations-oriented teams - -### Pain Points Growing With Scale - -- Manual assertion logic (string comparison via `eval`) -- No parallel execution -- Duplicated helpers across scripts (`gql()`, `check()`, `run_query()`) -- Polling/retry patterns fragile in bash -- No structured test reporting (JSON/TAP/XML) -- No test filtering or selective execution - -## Options Evaluated - -### Option A: Bash + Shared Helpers - -Extract common functions into `scripts/lib/test-helpers.sh`, keep writing bash. - -| Aspect | Rating | -| -------------- | ------------------------------------------------------- | -| Learning curve | None | -| Layer 0-1 fit | Excellent | -| Layer 2-3 fit | Poor — polling, state machines, parallelism are fragile | -| Maintenance | Degrades as test count grows | - -**Verdict**: Right for Layers 0-1. Not sufficient for Layers 2-3. - -### Option B: Python pytest - -Already installed in devcontainer (v9.0.2 + pytest-cov). - -| Aspect | Rating | -| ----------------- | -------------------------------------------------------- | -| Learning curve | Low — 1-2 hours for bash-familiar developers | -| Layer 0-1 fit | Overkill — just curl + jq | -| Layer 2-3 fit | Strong — fixtures, retry, async, parallel (`-n auto`) | -| JSON assertions | Native dict access, no jq dependency | -| Subprocess calls | `subprocess.run(["cast", ...])` — more verbose than bash | -| Failure reporting | Excellent — diffs, tracebacks, captured output | - -**Available plugins**: `pytest-asyncio`, `pytest-xdist` (parallel), `pytest-timeout`, `pytest-retry`. Would need `pip install` in Dockerfile (4 lines). - -### Option C: Rust + cargo-nextest - -Already installed: `cargo-nextest` 0.9.127, `cargo-make` 0.37.24, full async toolchain. The eligibility-oracle project at `/git/local/eligibility-oracle-node/` demonstrates the exact patterns needed. - -| Aspect | Rating | -| ------------------ | --------------------------------------------------------------- | -| Learning curve | Medium — team already writes Rust | -| Layer 0-1 fit | Acceptable — more verbose than bash but type-safe | -| Layer 2-3 fit | Strong — `tokio::test`, `reqwest`, structured error handling | -| JSON assertions | `serde_json` value access + `pretty_assertions` for diffs | -| Subprocess calls | `std::process::Command` — safe (no shell escaping), but verbose | -| Failure reporting | Good — backtraces, `anyhow` context, `pretty_assertions` | -| Compile time | 20-30s initial, 2-5s incremental | -| Parallel execution | Built into nextest — automatic, zero config | -| IDE support | Full rust-analyzer autocomplete, inline docs | - -**Key advantage**: Primary language of the devcontainer and team. No context-switching. The `reqwest` + `serde_json` + `tokio::test` pattern is already proven in the workspace. - -**Key trade-off**: 20-30s initial compile per session vs instant bash execution. - -### Option D: BATS or Node.js - -- **BATS**: Not installed, marginal benefit over bash + helpers, still bash underneath -- **Node.js (jest/vitest)**: Available but no advantage over Python or Rust for CLI orchestration - -Neither recommended. - -## Comparison: Layer 2-3 Test Example - -A test that creates an allocation, advances epochs, and verifies rewards: - -### Bash - -```bash -# Create allocation (fragile string parsing) -result=$(curl -s "$AGENT_URL" -H 'content-type: application/json' \ - -d '{"query": "mutation { createAllocation(...) { id } }"}') -alloc_id=$(echo "$result" | jq -r '.data.createAllocation.id') - -# Advance 3 epochs (manual loop) -for i in 1 2 3; do - ./scripts/advance-epoch.sh -done - -# Poll until closed (manual timeout) -elapsed=0 -while [ $elapsed -lt 120 ]; do - status=$(curl -s "$SUBGRAPH_URL" ... | jq -r '.data.allocations[0].status') - [ "$status" = "Closed" ] && break - sleep 5; elapsed=$((elapsed + 5)) -done -[ "$status" = "Closed" ] || { echo "FAIL: timed out"; exit 1; } -``` - -### Rust - -```rust -#[tokio::test] -async fn test_allocation_lifecycle() -> Result<()> { - let net = TestNetwork::from_env()?; - - let alloc = net.create_allocation(&deployment).await?; - net.advance_epochs(3).await?; - net.close_allocation(&alloc.id).await?; - - let result = net.poll_until(Duration::from_secs(120), || async { - let a = net.query_allocation(&alloc.id).await?; - Ok(a.status == "Closed") - }).await?; - - assert!(result.indexing_rewards > 0, "Expected rewards, got 0"); - Ok(()) -} -``` - -### Python - -```python -def test_allocation_lifecycle(network): - alloc = network.create_allocation(deployment) - network.advance_epochs(3) - network.close_allocation(alloc["id"]) - - result = network.poll_until( - timeout=120, - check=lambda: network.query_allocation(alloc["id"])["status"] == "Closed" - ) - - assert result["indexingRewards"] != "0" -``` - -## Recommendation: Rust for Layers 2-3, Keep Bash for Layers 0-1 - -### Rationale - -1. **Layers 0-1 are done and working** in bash. Moving them gains nothing. -2. **Layers 2-3 need orchestration** that bash handles poorly. -3. **Rust is the team's primary language** — the devcontainer, the eligibility-oracle, and the broader Graph ecosystem tooling are Rust-first. -4. **The tooling is already paid for**: cargo-nextest, tokio, reqwest, serde_json are all installed and proven in the workspace. -5. **pytest is the pragmatic alternative** if Rust compile times prove too disruptive during rapid test development. It's installed and ready. - -### Decision Point - -Try Rust first on one test (port `test-reo-eligibility.sh` to a Rust integration test). If the compile-time overhead is acceptable during active development, continue with Rust. If not, fall back to pytest — the test structure and helper patterns are identical, just in a different language. - -## Implementation Plan - -### Phase 1: Shared Bash Helpers (immediate) - -Extract duplicated functions into a shared library: - -``` -scripts/lib/ - test-helpers.sh # gql(), check(), jq_test(), run_query(), run_cast() - test-constants.sh # URL resolution, env loading, PATH setup -``` - -Refactor existing Layer 0-1 scripts to source these. No behavior change. - -### Phase 2: Rust Test Crate (next) - -``` -tests/ - Cargo.toml - src/ - lib.rs # TestNetwork struct, shared helpers - graphql.rs # GraphQL query helpers - cast.rs # cast CLI wrapper - polling.rs # poll_until, retry logic - tests/ - reo_eligibility.rs # Port of test-reo-eligibility.sh - allocation_cycle.rs # Layer 2: create → close → verify -``` - -Minimal `Cargo.toml`: - -```toml -[package] -name = "local-network-tests" -version = "0.1.0" -edition = "2024" - -[dependencies] -reqwest = { version = "0.12", features = ["json"] } -serde_json = "1" -tokio = { version = "1", features = ["full"] } -anyhow = "1" - -[dev-dependencies] -pretty_assertions = "1" -``` - -### Phase 3: Migrate Remaining Tests - -Once the pattern is proven: - -- Layer 2 operational lifecycle tests in Rust -- Layer 3 timing-dependent tests in Rust -- Keep bash scripts for quick manual validation (they remain useful documentation) - -### Integration - -```bash -# Run bash tests (Layers 0-1) -./scripts/test-baseline-queries.sh -./scripts/test-baseline-state.sh -./scripts/test-indexer-guide-queries.sh - -# Run Rust tests (Layers 2-3) -cd tests && cargo nextest run - -# Run everything -cargo make test-all # Orchestrates both -``` - -## Dependencies - -- Extract shared bash helpers (no new deps) -- Rust test crate: `reqwest`, `serde_json`, `tokio`, `anyhow` (all already in workspace) -- Optional: `pretty_assertions` for better diff output diff --git a/docs/testing/reo/CurationSignal.md b/docs/testing/reo/CurationSignal.md deleted file mode 100644 index 9b12d42c..00000000 --- a/docs/testing/reo/CurationSignal.md +++ /dev/null @@ -1,115 +0,0 @@ -# Task: Add Curation Signal to Local Network Setup - -> Created: 2026-02-20 -> Status: RESOLVED (2026-02-22) — implemented in `start-indexing/run.sh` and `graph-contracts/run.sh` - -## Problem - -BaselineTestPlan test 4.1 filters for `signalledTokens_not: 0` and returns empty on the local network because no curation signal is added during setup. This means any test that depends on curation data (signal amounts, curator entities, deployment filtering by signal) cannot run. - -## Objective - -Add curation signal to deployed subgraphs as part of the standard `start-indexing` setup flow, so the local network starts with realistic curation state. - -## Scope - -Small change (~20-30 lines) in `start-indexing/run.sh`. No new services, no new containers. - -## Implementation - -### Contracts - -| Contract | Config file | Key | -|----------|------------|-----| -| L2Curation | `subgraph-service.json` | `.["1337"].L2Curation.address` | -| L2GraphToken | `horizon.json` | `.["1337"].L2GraphToken.address` | -| L2GNS | `subgraph-service.json` | `.["1337"].L2GNS.address` | - -Addresses resolved via `contract_addr` helper in [shared/lib.sh](../../../shared/lib.sh). - -### Insertion Point - -In [start-indexing/run.sh](../../../start-indexing/run.sh), after GNS publishing (line 74) and before setting indexing rules (line 76): - -``` -line 54-74: Publish subgraphs to GNS -line ??: << ADD CURATION SIGNAL HERE >> -line 76-80: Set indexing rules -``` - -### Steps - -For each deployed subgraph (network, block_oracle): - -1. **Convert deployment IPFS hash to bytes32** (already done for GNS publishing — reuse `dep_hex`) - -2. **Approve L2Curation to spend GRT**: - ```bash - cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ - --confirmations=0 --mnemonic="${MNEMONIC}" \ - "${grt}" 'approve(address,uint256)' "${curation}" "${SIGNAL_AMOUNT}" - ``` - -3. **Mint signal via L2Curation**: - ```bash - cast send --rpc-url="http://chain:${CHAIN_RPC_PORT}" \ - --confirmations=0 --mnemonic="${MNEMONIC}" \ - "${curation}" 'mint(bytes32,uint256,uint256)(uint256,uint256)' \ - "0x${dep_hex}" "${SIGNAL_AMOUNT}" "0" - ``` - -### Parameters - -- **Curator account**: ACCOUNT0 (`0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266`) — same account that publishes subgraphs -- **Signal amount**: 1000 GRT per subgraph (1000000000000000000000 wei) — enough to be meaningful, small relative to total supply -- **Min signal out**: 0 (no slippage protection needed on local network) - -### Graph Explorer UI Equivalent - -In Graph Explorer, this is the "Signal" action on a subgraph detail page: - -| Explorer component | Contract call | -|---|---| -| [SignalForm.tsx:238-257](../../../) `onSignal()` — version signal path | `L2Curation.mint(deploymentId, amount, minSignal)` | -| [SignalForm.tsx:238-257](../../../) `onSignal()` — named signal path | `L2GNS.mintSignal(subgraphId, amount, minNameSignal)` | - -For local network setup, the direct `L2Curation.mint()` call is simpler since we have deployment IDs directly and don't need NFT subgraph IDs. - -### Verification - -After signal is added, this query should return results: - -```graphql -{ - subgraphDeployments(where: { signalledTokens_not: "0" }) { - ipfsHash - signalledTokens - curatorSignals { - curator { id } - signal - signalledTokens - } - } -} -``` - -### Idempotency - -Check `signalledTokens` before minting — if already non-zero, skip. Follows the same pattern used for GNS publishing (check `subgraph_count` before publishing). - -## Dependencies - -None. All contracts already deployed. ACCOUNT0 already has GRT from protocol initialization. - -## Affected Tests - -- BaselineTestPlan 4.1: `subgraphDeployments(where: { signalledTokens_not: 0 })` — currently returns empty, will return data -- Any future Layer 2 tests involving curation operations -- Enables testing of curation-dependent reward calculations - -## Files to Modify - -| File | Change | -|------|--------| -| `start-indexing/run.sh` | Add curation signal block after GNS publishing | -| `scripts/test-baseline-state.sh` | Add check for `signalledTokens` non-zero | diff --git a/docs/testing/reo/Goal.md b/docs/testing/reo/Goal.md deleted file mode 100644 index b77f68f7..00000000 --- a/docs/testing/reo/Goal.md +++ /dev/null @@ -1,102 +0,0 @@ -# Test Plan Automation - Goal - -## Objective - -Automate the verification queries and commands from the indexer test plans so they are repeatable, catch schema drift early, and progressively cover more of the operational workflow. - -The test plans live in [graphprotocol/contracts](https://github.com/graphprotocol/contracts) and are designed for human indexers running against Arbitrum Sepolia. This automation adapts them for the local network, where we control the full stack and can cycle through epochs in seconds. - -## Source Test Plans - -| Document | Scope | Tests | Automated | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------ | ----------------- | -| [BaselineTestPlan.md](https://github.com/graphprotocol/contracts/blob/main/packages/issuance/docs/testing/reo/BaselineTestPlan.md) | Standard indexer operations (stake, provision, allocate, query, rewards) | 7 cycles, 22 tests | Yes | -| [IndexerTestGuide.md](https://github.com/graphprotocol/contracts/blob/main/packages/issuance/docs/testing/reo/IndexerTestGuide.md) | REO eligibility flows (renew, expire, deny, recover) | 5 sets, 8 tests | Yes | -| [ReoTestPlan.md](https://github.com/graphprotocol/contracts/blob/main/packages/issuance/docs/testing/reo/ReoTestPlan.md) | REO coordinator/governance operations | 8 cycles, 31 tests | Yes | -| [RewardsConditionsTestPlan.md](https://github.com/graphprotocol/contracts/blob/main/packages/issuance/docs/testing/reo/RewardsConditionsTestPlan.md) | Reclaim system, signal conditions, POI paths, observability | 7 cycles, 26 tests | Cycles 1-4, 6 | -| [SubgraphDenialTestPlan.md](https://github.com/graphprotocol/contracts/blob/main/packages/issuance/docs/testing/reo/SubgraphDenialTestPlan.md) | Subgraph denial: accumulator freeze, deferral, recovery | 6 cycles, 18 tests | Cycles 2, 3, 5, 6 | - -## Automation Layers - -Each layer builds on the previous. The goal is to move progressively from schema validation toward full operational testing. - -``` -Layer 0: Query Validation ← scripts validate queries parse correctly -Layer 1: State Observation ← scripts check network state matches expectations -Layer 2: Operational Lifecycle ← scripts drive state changes and verify outcomes -Layer 3: Timing-Dependent ← scripts manage epoch advancement and eligibility expiry -``` - -### Layer 0: Query Schema Validation - -**What**: Run every GraphQL verification query and `cast` command from the test plans against the live network. Check for schema errors, missing fields, invalid enum values. - -**Why**: Catches the kind of bugs we found manually — `unallocatedStake` vs `availableStake`, `"ProvisionThaw"` vs `Provision`, `indexingRewardAmount` on wrong entity type. These are silent failures that would block an indexer following the docs. - -**Speed**: Seconds. No state changes. Safe to run anytime. - -**Scripts**: - -- `scripts/test-baseline-queries.sh` — all 14 BaselineTestPlan queries -- `scripts/test-indexer-guide-queries.sh` — IndexerTestGuide queries + cast commands - -### Layer 1: State Observation - -**What**: After network startup, verify the expected state exists: indexer registered, provision created, allocations active, epoch progressing, subgraph synced. - -**Why**: Confirms the local network initialised correctly before running operational tests. Catches deployment regressions (e.g., contract upgrade breaks address book, indexer-agent fails to register). - -**Speed**: Seconds. Read-only. - -**Builds on**: Layer 0 queries, filtered to check specific values (non-zero stake, Active allocations, populated URL/geoHash). - -### Layer 2: Operational Lifecycle - -**What**: Execute the Cycle 7 end-to-end workflow: create allocation → send queries → close allocation → verify rewards and fees. Uses `cast send` for contract interactions and the indexer management API for allocation operations. - -**Why**: Validates that the full indexer operational cycle works, not just that queries parse. This is what an indexer actually does. - -**Speed**: Minutes. Requires epoch advancement between steps. - -**Builds on**: `advance-epoch.sh`, `query_gateway.sh`, `mine-block.sh`. - -### Layer 3: Timing-Dependent Flows - -**What**: Test eligibility expiry, thawing periods, and other time-dependent behaviour by advancing chain time and epochs programmatically. - -**Why**: These are the hardest tests to run manually — an indexer on testnet waits hours for epochs to advance. On local network we can cycle in seconds. - -**Covers**: - -- IndexerTestGuide Sets 2-4 (eligible → expire → ineligible → re-renew → full rewards) -- BaselineTestPlan 2.2 (unstake thawing), 3.3-3.4 (provision thawing) - -**Builds on**: `advance-epoch.sh`, `cast send` for eligibility renewal, `REO_ELIGIBILITY_PERIOD` from `.env`. - -## Local Network Advantages - -The local network can do things testnet can't: - -| Capability | Testnet | Local | -| -------------------------- | -------------------- | -------------------------------------- | -| Advance epoch | Wait ~110 min | `./scripts/advance-epoch.sh` (seconds) | -| Control eligibility period | Fixed by coordinator | `REO_ELIGIBILITY_PERIOD` in `.env` | -| Advance chain time | Wait | `evm_increaseTime` RPC | -| Reset state | Can't | `docker compose down -v && up` | -| Full log access | Partial | All containers, all levels | - -## Workflow Sequence - -For each test plan update or protocol upgrade: - -1. Start local network (`docker compose up -d`) -2. Run Layer 0 (`test-baseline-queries.sh`, `test-indexer-guide-queries.sh`) — catch schema issues immediately -3. Run Layer 1 (state observation) — confirm network initialised correctly -4. Run Layer 2 (operational lifecycle) — validate full cycle -5. Run Layer 3 (timing flows) — test eligibility and thawing -6. Fix any issues found, update test plans and scripts together - -## Related Documentation - -- [Eligibility Oracle Goal](../../eligibility-oracle/Goal.md) — REO local network integration -- [Eligibility Oracle Status](../../eligibility-oracle/Status.md) — REO implementation log diff --git a/docs/testing/reo/Status.md b/docs/testing/reo/Status.md deleted file mode 100644 index b0550feb..00000000 --- a/docs/testing/reo/Status.md +++ /dev/null @@ -1,206 +0,0 @@ -# Test Plan Automation - Status - -> Last updated: 2026-02-23 - -## Current Phase: Layers 0-3 complete + RewardsConditions & SubgraphDenial - -### Summary - -All test layers are implemented and passing. 43 Rust integration tests cover network state observation, allocation lifecycle, reward collection, eligibility lifecycle, query fee flow, rewards conditions (reclaim system, signal thresholds, POI paths), and subgraph denial (state management, accumulator freeze, recovery, edge cases). Infrastructure changes enable the reward pipeline (curation signal + issuance config) and speed up epoch advancement (1s EBO polling). - -## Layer Progress - -| Layer | Status | Implementation | -| -------------------------- | ------ | ------------------------------------------------------------------------------------------ | -| 0 - Query Validation | Done | `test-baseline-queries.sh`, `test-indexer-guide-queries.sh` | -| 1 - State Observation | Done | `test-baseline-state.sh` + Rust `network_state.rs` (6 tests) | -| 2 - Operational Lifecycle | Done | Rust `allocation_lifecycle.rs` (2 tests) | -| 3 - Timing-Dependent Flows | Done | Rust `eligibility.rs` (1 test), `reward_collection.rs` (1 test), `query_fees.rs` (2 tests) | - -### Rust Test Suite (43 tests) - -``` -tests/tests/ - network_state.rs 7 tests ~1s read-only state checks - stake_management.rs 2 tests ~2s stake add/remove - provision_management.rs 1 test ~20s provision add/thaw/deprovision - allocation_lifecycle.rs 3 tests ~38s create/close/query lifecycle - eligibility.rs 1 test ~100s eligible/ineligible/re-eligible cycle - reward_collection.rs 1 test ~54s collect(IndexingRewards) → stake increase - query_fees.rs 2 tests ~1s gateway receipts + escrow observability - reo_governance.rs 15 tests ~30s REO coordinator/governance operations - rewards_conditions.rs 6 tests ~TBD reclaim system, signal, POI paths, observability - subgraph_denial.rs 5 tests ~TBD denial state, accumulator freeze, recovery -``` - -Run with: `cd tests && cargo nextest run --no-capture` - -## Completed - -- [x] Manual validation of all 14 BaselineTestPlan GraphQL queries -- [x] Manual validation of all IndexerTestGuide GraphQL queries -- [x] Manual validation of all IndexerTestGuide `cast` commands -- [x] Fixed 3 bugs in BaselineTestPlan.md (pushed to `reo-testing` branch) -- [x] Fixed 1 bug in IndexerTestGuide.md (pushed to `reo-testing` branch) -- [x] Created Layer 0 bash scripts -- [x] Created Rust test crate with `TestNetwork` helper library -- [x] Network state observation tests (Layer 1 in Rust) -- [x] Allocation lifecycle tests (Layer 2) -- [x] Deterministic eligibility lifecycle tests (Layer 3) -- [x] Reward collection via `collect(IndexingRewards)` (Layer 3) -- [x] Query fee / Graph Tally receipt generation tests (Layer 3) -- [x] Enabled reward pipeline: curation signal + issuance config in deploy scripts -- [x] EBO polling interval reduced from 20s to 1s for faster tests - -## Bugs Found and Fixed - -### BaselineTestPlan.md (3 bugs) - -| Bug | Tests affected | Fix | -| ------------------------------------------------- | ----------------------- | ----------------------------------------------- | -| `unallocatedStake` field doesn't exist on Indexer | 2.1, 2.2, 3.2, 3.4, 6.1 | Changed to `availableStake` | -| `type: "ProvisionThaw"` invalid enum value | 3.3 | Changed to `type: Provision` (enum, not string) | -| `indexingRewardAmount` doesn't exist on Indexer | 6.1 | Changed to `rewardsEarned` | - -### IndexerTestGuide.md (1 bug) - -| Bug | Test affected | Fix | -| ------------------------------------------------------------------ | ------------- | -------------------------------------------- | -| `subgraphDeployment { id { id } }` invalid nested scalar selection | 1.1 | Changed to `subgraphDeployment { ipfsHash }` | - -### Infrastructure bugs found during test development - -| Bug | Impact | Fix | -| ----------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------- | -| No curation signal on any deployment | `accRewardsPerSignal = 0` — all rewards are zero regardless of issuance | Added `L2Curation.mint()` in `start-indexing/run.sh` | -| `issuancePerBlock` not configured | Default issuance too low for meaningful testing | Added `setIssuancePerBlock(100e18)` in `graph-contracts/run.sh` | -| `closeAllocation` returns 0 rewards when indexer ineligible | `RewardsDeniedDueToEligibility` event fires instead of `HorizonRewardsAssigned` | Tests renew eligibility before reward-dependent operations | -| `PaymentsEscrow.getBalance()` needs 3 args | Signature is `(payer, collector, receiver)` not `(payer, receiver)` | Fixed in `query_fees.rs` | -| EBO polling at 20s causes slow tests | Epoch sync takes ~2min per test with epoch advancement | Reduced `polling_interval_in_seconds` to 1 | - -## Key Technical Findings - -### Reward Pipeline Requirements - -For indexing rewards to flow, ALL of these must be true: - -1. `issuancePerBlock > 0` on RewardsManager (requires Governor = ACCOUNT1_SECRET) -2. Curation signal exists on the deployment (`signalledTokens > 0` via `L2Curation.mint()`) -3. Allocation spans multiple epochs -4. Indexer is eligible (if REO deployed) at the time of collect/close - -### collect() vs closeAllocation() - -`closeAllocation` calls `reclaimRewards()` which sends rewards to a reclaim address (or drops them). To route rewards to the indexer's stake, `SubgraphService.collect(indexer, PaymentTypes.IndexingRewards, data)` must be called BEFORE closing. - -However, `closeAllocation` via the management API does internally handle reward collection — the `indexingRewards` field in the close response is non-zero when the indexer is eligible and curation signal exists. - -The `collect_indexing_rewards` test directly calls `SubgraphService.collect()` as the indexer (RECEIVER_SECRET) and verifies the stake delta. - -### Eligibility Expiry During Mining - -Mining ~100 blocks for epoch advancement adds ~1200s of chain time (12s per block). With a 300s eligibility period, the indexer becomes ineligible mid-test. Tests must call `reo_renew_indexer()` before any reward-dependent operation. - -### Graph Tally Query Fee Pipeline - -The Graph Tally stack works end-to-end for receipt generation (20/20 gateway queries succeed). However: - -- Graph Tally escrow deposits are not observed (escrow balance = 0) -- Graph Tally subgraph shows 0 escrow accounts -- This is expected — the Graph Tally escrow manager processes asynchronously and may need longer running time - -## Gaps - -### ~~No signal on local network deployments~~ RESOLVED - -Fixed by adding `L2Curation.mint()` in `start-indexing/run.sh` and `setIssuancePerBlock(100e18)` in `graph-contracts/run.sh`. - -### Explorer UI operations not scriptable — [Task: explorer/Goal.md](../../explorer/Goal.md) - -Cycles 1-2 in BaselineTestPlan use Explorer UI for staking and delegation parameters. On local network these are done by `graph-contracts` during deployment. - -### ~~Test framework for Layers 2-3~~ RESOLVED - -Rust test crate implemented with `TestNetwork` helper library. See [TestFramework.md](../TestFramework.md) for the evaluation that led to this choice. - -### ~~Indexer CLI not available in devcontainer~~ RESOLVED - -Management API at `indexer-agent:7600` covers all operations via GraphQL. - -### Cold start validation pending - -Tests assume an already-running network. Full validation from `docker compose down -v && docker compose up -d` → test pass has not been confirmed yet. - ---- - -## Log - -### 2026-02-20 — Initial validation - -- Ran all BaselineTestPlan queries against local network subgraph (graph-node:8000) -- Found 3 schema bugs: `unallocatedStake`, `"ProvisionThaw"`, `indexingRewardAmount` on Indexer -- Fixed all 3 in BaselineTestPlan.md, committed to `reo-testing` branch -- Ran all IndexerTestGuide queries and cast commands -- Found 1 bug: invalid nested `{ id { id } }` selection on scalar -- Fixed in IndexerTestGuide.md, committed to `reo-testing` branch -- Created Layer 0 automation scripts -- Created Goal.md and Status.md for tracking - -### 2026-02-20 — Layer 1 and gap resolution - -- Built `test-baseline-state.sh`: 18 checks across indexer registration, provision, allocations, deployments, gateway, epoch, chain, and REO -- Investigated `graph indexer` CLI gap: CLI available via npx, but more importantly the indexer-agent management API (port 7600) exposes full GraphQL schema with all query and mutation operations -- Management API tested: `indexerRegistration`, `allocations`, `indexerDeployments`, `provisions`, `indexingRules` all work via curl -- This resolves the biggest gap for Layer 2: operational tests can use management API mutations (`createAllocation`, `closeAllocation`, `queueActions`, etc.) instead of the CLI - -### 2026-02-20 — Gap investigation and task docs - -- Investigated curation signal: L2Curation contract deployed, `mint()` available, ACCOUNT0 has GRT — straightforward to add to `start-indexing/run.sh` -- Investigated Graph Explorer: repo at `/git/edgeandnode/graph-explorer/`, Next.js app with Docker support, no backend API (all contract calls via Wagmi/Viem) -- Documented Explorer contract call reference: mapped UI components (SignalForm, DelegateTransactionContext, StakeForm) to equivalent `cast send` calls -- Evaluated test frameworks: Rust (cargo-nextest) recommended for Layers 2-3 given devcontainer tooling; bash retained for Layers 0-1 -- Created task docs: [CurationSignal.md](./CurationSignal.md), [explorer/Goal.md](../../explorer/Goal.md), [TestFramework.md](../TestFramework.md) - -### 2026-02-21 — Rust test crate and Layers 2-3 - -- Created Rust integration test crate (`tests/`) with `TestNetwork` helper -- Implemented `network_state.rs` (6 tests): indexer registration, provision, allocations, gateway, epoch, REO state -- Implemented `allocation_lifecycle.rs` (2 tests): create/close cycle, gateway query serving -- Implemented `eligibility.rs` (1 test): 3-phase lifecycle (eligible → ineligible → re-eligible) with deterministic contract calls -- All 9 tests passing - -### 2026-02-22 — Reward pipeline and expanded coverage - -- Discovered rewards were zero: `accRewardsPerSignal = 0` due to missing curation signal -- Fixed by adding `L2Curation.mint(1000 GRT)` in `start-indexing/run.sh` -- Set `issuancePerBlock = 100 GRT` in `graph-contracts/run.sh` (requires ACCOUNT1_SECRET as Governor) -- Reduced EBO polling from 20s to 1s — tests 3x faster (allocation_lifecycle 105s→38s, eligibility 277s→91s) -- Added `reward_collection.rs`: `collect(IndexingRewards)` increases stake by ~12,000 GRT -- Added `query_fees.rs`: gateway generates Graph Tally receipts (20/20), escrow state observable -- Found and fixed eligibility expiry during mining (300s period, ~1200s chain time in 2 epoch advances) -- Fixed `PaymentsEscrow.getBalance()` signature: 3 args (payer, collector, receiver) -- All 12 tests passing - -### 2026-02-23 — RewardsConditions and SubgraphDenial automation - -- Added `account1_secret` (Governor key) to `TestNetwork` for governance operations -- Added ~15 new `cast.rs` wrappers: reclaim config, signal threshold, denial state, accumulator reads, GRT balance, event filtering, governor operations -- Added `graphql.rs` helpers: `query_deployment_id()`, `query_deployments_with_signal()` -- Created `rewards_conditions.rs` (6 tests): - - `reclaim_configuration`: per-condition + default reclaim addresses, fallback routing, baseline balances (Cycle 1) - - `reclaim_unauthorized_reverts`: access control (Cycle 1.4) - - `below_minimum_signal_lifecycle`: threshold raise → freeze → reclaim → restore → resume (Cycle 2) - - `zero_allocated_tokens_lifecycle`: close all allocs → reclaim → new alloc baseline preserved (Cycle 3) - - `poi_normal_claim`: healthy close with event check (Cycle 4.1) - - `poi_allocation_too_young`: same-epoch close returns 0 (Cycle 4.4) - - `observability_accumulator_growth`: view function growth verification (Cycle 6.3) -- Created `subgraph_denial.rs` (5 tests): - - `denial_state_management`: deny, idempotent, unauthorized revert (Cycle 2) - - `accumulator_freeze_and_reclaim`: freeze verification + reclaim (Cycle 3) - - `denial_lifecycle`: full deny → verify → undeny → resume → close → rewards (Cycles 2+5) - - `edge_rapid_deny_undeny`: rapid toggle (Cycle 6.3) - - `edge_denial_vs_eligibility`: denial precedence over eligibility (Cycle 6.4) -- Updated `tests/README.md` with complete test mapping for all 5 test plans -- Updated reo-testing docs: removed DRAFT WIP, added local automation status -- Added indexer awareness section to IndexerTestGuide (denial, POI staleness, signal) -- 43 tests total (compile-checked, not yet run against live network) diff --git a/justfile b/justfile index 8f8bf9d4..4e91bd52 100644 --- a/justfile +++ b/justfile @@ -1,11 +1,35 @@ default: @just --list -# Bring the compose stack up in the background -up *args: - docker compose up -d {{args}} +# Resolve the active recipe (or specify one) into .env. `docker compose` +# picks .env up automatically — after this, bare `docker compose` commands +# work without --env-file. +# Recipe selection: $RECIPE → .recipe.local → .recipe → "baseline". +resolve recipe="": + ./scripts/resolve-recipe.sh {{recipe}} -# Tear the compose stack down +# List available recipes. +recipes: + @ls -1 recipes/*.json | sed 's,recipes/,,; s,\.json$,,' + +# Show the current active recipe selection (without resolving). +recipe-active: + @if [ -f .recipe.local ]; then \ + echo "from .recipe.local: $(cat .recipe.local)"; \ + elif [ -f .recipe ]; then \ + echo "from .recipe: $(cat .recipe)"; \ + else \ + echo "from default: baseline"; \ + fi + +# Bring the compose stack up. Resolves the active recipe first (writes +# .env), then `docker compose up -d --build`. Pass a recipe name as the +# first arg to override; remaining args forward to compose. +up recipe="" *args="": + ./scripts/resolve-recipe.sh {{recipe}} + docker compose up -d --build {{args}} + +# Tear the compose stack down. down *args: docker compose down {{args}} @@ -13,10 +37,30 @@ down *args: logs *services: docker compose logs -f {{services}} +# Rebuild and restart specific services (or all if no args). Useful after +# editing run.sh / Dockerfile in any container. +rebuild *services: + docker compose up -d --build {{services}} + # Connect the current container to the compose network so service hostnames resolve connect: ./scripts/connect-network.sh +# Capture local-network state for offline debugging. +# Default output dir: _dumps/. Pass an arg for a custom path. +dump-state *args: + ./scripts/dump-state.sh {{args}} + +# Snapshot the running stack for fast restore later. +# Default output: _snapshots/current/. Stack must be up + healthy + ready. +bake-snapshot *args: + ./scripts/bake-snapshot.sh {{args}} + +# Restore the stack from a previously-baked snapshot. Destructive on the +# named volumes — wipes current state. Default input: _snapshots/current/. +restore-snapshot *args: + ./scripts/restore-snapshot.sh {{args}} + # Mine N blocks (default 1), advancing time by 12s per block mine count="1": ./scripts/mine-block.sh {{count}} @@ -30,9 +74,38 @@ restart: docker compose down docker compose up -d -# Tear the stack down and wipe volumes — clean slate (run `up` to start fresh) +# Tear the stack down and wipe volumes — clean slate (run `up` to start fresh). +# Activates every defined profile during teardown so containers from inactive +# recipes (e.g. dipper/iisa left behind when switching baseline ↔ indexing-payments) +# are removed too — otherwise they hold volume references and the subsequent +# `volume rm` fails silently, leaving stale address books that break the next +# `up` in Phase 3 of graph-contracts (stale IssuanceAllocator etc.). +# Also force-removes leftover per-test compose stacks (`local-network-test-*`) +# from `cargo nextest` runs. reset: - docker compose down -v + -docker ps -a --filter "name=^local-network-test-" -q | xargs -r docker rm -f + -COMPOSE_PROFILES=$(docker compose config --profiles | paste -sd,) \ + docker compose down -v --remove-orphans + -docker volume ls -q --filter "name=^local-network_" | xargs -r docker volume rm + +# Stop containers whose service is no longer in the active profile set — +# e.g. dipper/iisa left running after `just up baseline` from indexing-payments. +# `docker compose up --remove-orphans` does NOT cover this (it only removes +# services missing from the compose file entirely). Use this after a recipe +# shrink, or use `just reset` for a full wipe. +stop-orphans: + #!/usr/bin/env bash + set -eu + active=$(docker compose config --services 2>/dev/null | sort) + running=$(docker compose ps --services --status=running 2>/dev/null | sort) + orphans=$(comm -23 <(echo "$running") <(echo "$active")) + if [ -z "$orphans" ]; then + echo "No orphan services running." + else + echo "Stopping out-of-profile services:" + echo "$orphans" | sed 's/^/ /' + echo "$orphans" | xargs docker compose stop + fi # Run integration tests (forwards args to tests/justfile) test *args: diff --git a/recipes/baseline.json b/recipes/baseline.json new file mode 100644 index 00000000..a02282bc --- /dev/null +++ b/recipes/baseline.json @@ -0,0 +1,15 @@ +{ + "description": "Default local-network stack: full GIP-0088 contract deployment (REO + IssuanceAllocator + RecurringAgreementManager on the audit-fix-3 ABI) with upstream stable image versions. No indexing-payments services. Mock REO wired by default for tests; REO-A still deployed and available. Profile set: block-oracle + explorer + rewards-eligibility.", + "fragments": [ + "base.env", + "services.env", + "accounts-role-named.env", + "mock-reo.env" + ], + "env": { + "COMPOSE_PROFILES": "block-oracle,explorer,rewards-eligibility" + }, + "compose_files": [ + "docker-compose.yaml" + ] +} diff --git a/recipes/indexing-payments.json b/recipes/indexing-payments.json new file mode 100644 index 00000000..5dabbf86 --- /dev/null +++ b/recipes/indexing-payments.json @@ -0,0 +1,15 @@ +{ + "description": "Baseline + WIP indexing-payments overlay: dipper, IISA, indexing-payments subgraph, and dips-fork indexer-rs (indexer-service + tap-agent). Layers indexing-payments.env on top of services.env. Profile set adds indexing-payments. Mock REO wired by default for tests; REO-A still deployed and available.", + "fragments": [ + "base.env", + "services.env", + "indexing-payments.env", + "accounts-role-named.env", + "mock-reo.env" + ], + "env": { "COMPOSE_PROFILES": "explorer,rewards-eligibility,indexing-payments" + }, + "compose_files": [ + "docker-compose.yaml" + ] +} diff --git a/scripts/bake-snapshot.sh b/scripts/bake-snapshot.sh new file mode 100755 index 00000000..e1452894 --- /dev/null +++ b/scripts/bake-snapshot.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# Snapshot the current local-network state into a directory so +# `restore-snapshot.sh` can later bring it back without a cold cold-start. +# +# Usage: +# scripts/bake-snapshot.sh [output-dir] (default: _snapshots/current) +# +# Stack must be up + healthy + ready before running. The script briefly +# stops services to capture consistent volume state, then restarts them. +# +# What's captured: +# - All compose-declared named volumes (chain-data, postgres-data, +# ipfs-data, config-local, iisa-scores, redpanda-data) as zstd +# tarballs. Each is copied via a throwaway alpine container to +# avoid platform-specific local-volume paths. +# - manifest.json: recipe, timestamp, git SHA + dirty flag, captured +# volume names, container image digests. +# - .env: snapshot of the active recipe's materialised env +# at capture time (so the same recipe restores cleanly). +# +# Future: fingerprinting + multi-snapshot cache. For now, single-slot +# under _snapshots/current/ — re-running bake overwrites. + +set -euo pipefail + +OUT=${1:-"_snapshots/current"} +TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) + +# Volumes we know about (compose-declared). Ordered roughly by importance. +VOLUMES=( + chain-data # anvil state — contracts, balances, storage + postgres-data # graph-node DBs + indexer-agent DB + ipfs-data # subgraph artifacts (wasm, schema) + config-local # contract address books + iisa-scores # IISA scoring persistence (indexing-payments only — empty otherwise) + redpanda-data # Kafka topic data +) + +# Sanity check: stack should be up + healthy + ready ------------------------- +log() { echo " $*" >&2; } + +ready_marker=$(docker ps -a --filter 'name=^ready$' --format '{{.Status}}' 2>/dev/null || true) +case "$ready_marker" in + Exited*) log "ready container exited cleanly — stack is bakeable" ;; + Up*) log "ready container still running — start-indexing may not have completed; baking anyway" ;; + *) + echo "warning: 'ready' container not found — can't confirm stack is fully bootstrapped." >&2 + echo " proceed only if you've manually verified all services are healthy." >&2 + ;; +esac + +mkdir -p "$OUT" + +# Resolve recipe + project -------------------------------------------------- +recipe="" +[ -f .recipe.local ] && recipe=$(head -n1 .recipe.local | tr -d '[:space:]') +[ -z "$recipe" ] && [ -f .recipe ] && recipe=$(head -n1 .recipe | tr -d '[:space:]') +[ -z "$recipe" ] && recipe="baseline" + +# Project name — compose uses the directory name by default. Capture it +# from a known container's labels rather than guessing. +project=$(docker inspect chain --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null || echo "local-network") + +git_sha=$(git rev-parse HEAD 2>/dev/null || echo "unknown") +git_dirty="false" +git diff-index --quiet HEAD -- 2>/dev/null || git_dirty="true" + +log "recipe=$recipe project=$project sha=$git_sha dirty=$git_dirty" + +# Stop services for consistent capture -------------------------------------- +log "stopping stack for consistent volume capture..." +docker compose --env-file .env stop 2>&1 | grep -E "Stopped|Error" >&2 || true + +# Capture each volume ------------------------------------------------------- +captured=() +mkdir -p "$OUT/volumes" +for v in "${VOLUMES[@]}"; do + prefixed="${project}_${v}" + if ! docker volume inspect "$prefixed" >/dev/null 2>&1; then + log "skip: volume $prefixed not present" + continue + fi + log "capturing $prefixed → volumes/${v}.tar.gz" + # Run tar+gzip inside an alpine container so we don't depend on host + # tooling (zstd in particular isn't always installed). Stream to stdout + # so we can write atomically via a .tmp suffix on the host. + docker run --rm \ + -v "$prefixed":/src:ro \ + alpine:3 \ + sh -c 'cd /src && tar -czf - .' \ + > "$OUT/volumes/${v}.tar.gz.tmp" + mv "$OUT/volumes/${v}.tar.gz.tmp" "$OUT/volumes/${v}.tar.gz" + captured+=("$v") +done + +# Capture image digests so a later restore can detect drift ----------------- +log "capturing image digests" +docker compose --env-file .env config --images 2>/dev/null \ + | sort -u > "$OUT/images.txt" || true + +# Snapshot the active resolved env ------------------------------------------ +cp .env "$OUT/.env" 2>/dev/null || true +[ -f .recipe.local ] && cp .recipe.local "$OUT/.recipe.local" +[ -f .recipe ] && cp .recipe "$OUT/.recipe" + +# Write manifest ------------------------------------------------------------ +{ + echo "{" + echo " \"baked_at\": \"$TS\"," + echo " \"recipe\": \"$recipe\"," + echo " \"project\": \"$project\"," + echo " \"git_sha\": \"$git_sha\"," + echo " \"git_dirty\": $git_dirty," + printf " \"volumes_captured\": [" + if [ ${#captured[@]} -gt 0 ]; then + printf '\n' + for i in "${!captured[@]}"; do + printf ' "%s"' "${captured[$i]}" + [ "$i" -lt $((${#captured[@]}-1)) ] && printf "," + printf '\n' + done + printf " " + fi + echo "]," + echo " \"images_file\": \"images.txt\"" + echo "}" +} > "$OUT/manifest.json" + +# Resume the stack ---------------------------------------------------------- +log "restarting stack..." +docker compose --env-file .env start 2>&1 | grep -E "Started|Error" >&2 || true + +size=$(du -sh "$OUT" | cut -f1) +echo "Baked snapshot ($size) → $OUT" >&2 +echo "$OUT" diff --git a/scripts/dipper-cli.sh b/scripts/dipper-cli.sh deleted file mode 100755 index 911049a4..00000000 --- a/scripts/dipper-cli.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# Wrapper script for dipper-cli that automatically sets required environment variables - -# Get the directory where this script is located -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -# Source the .env file from repo root -source "$SCRIPT_DIR/../.env" -[ -f "$SCRIPT_DIR/../.env.local" ] && source "$SCRIPT_DIR/../.env.local" - -# Set required environment variables -export INDEXING_SIGNING_KEY="${RECEIVER_SECRET}" -export INDEXING_SERVER_URL="http://${DIPPER_HOST:-localhost}:${DIPPER_ADMIN_RPC_PORT}/" - -# Change to dipper source directory -DIPPER_SOURCE="${DIPPER_SOURCE_ROOT:-}" -if [ -z "$DIPPER_SOURCE" ] || [ ! -d "$DIPPER_SOURCE" ]; then - echo "Error: Set DIPPER_SOURCE_ROOT to a local clone of edgeandnode/dipper." >&2 - exit 1 -fi -cd "$DIPPER_SOURCE" - -# Run dipper-cli with all passed arguments -cargo run --bin dipper-cli -- "$@" diff --git a/scripts/dump-state.sh b/scripts/dump-state.sh new file mode 100755 index 00000000..426067b4 --- /dev/null +++ b/scripts/dump-state.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# Capture local-network state to a directory for offline debugging. +# +# Usage: +# scripts/dump-state.sh [output-dir] +# +# Default output: _dumps/[-