From 6b80d5d3d9a6115360546ebd0bf39d9124b1af3a Mon Sep 17 00:00:00 2001 From: ByteYue Date: Tue, 14 Jul 2026 16:12:51 +0800 Subject: [PATCH] feat(oracle): add combined Binance and Polymarket E2E --- .agents/skills/gravity-oracle-demo/SKILL.md | 155 ++++++ .agents/workflows/build-and-test.md | 60 ++- .gitignore | 1 + Cargo.lock | 232 ++++----- bin/gravity_node/Cargo.toml | 3 +- bin/gravity_node/src/relayer.rs | 97 ++-- cluster/deploy.sh | 13 +- cluster/genesis.sh | 35 +- cluster/templates/validator.yaml.tpl | 3 + .../binance_price_feed/README.md | 200 ++++++++ .../binance_price_feed/cluster.toml | 27 + .../binance_price_feed/genesis.toml | 74 +++ .../binance_price_feed/hooks.py | 186 +++++++ .../binance_price_feed/mock_binance.py | 26 + .../binance_price_feed/relayer_config.json | 6 + .../test_binance_price_feed.py | 219 ++++++++ .../cluster_test_cases/oracle_demo/README.md | 92 ++++ .../oracle_demo/cluster.toml | 27 + .../oracle_demo/genesis.toml | 81 +++ .../cluster_test_cases/oracle_demo/hooks.py | 147 ++++++ .../oracle_demo/relayer_config.json | 7 + .../oracle_demo/test_oracle_demo.py | 452 +++++++++++++++++ .../polymarket_mock/README.md | 112 +++++ .../polymarket_mock/cluster.toml | 27 + .../polymarket_mock/genesis.toml | 78 +++ .../polymarket_mock/hooks.py | 65 +++ .../polymarket_mock/relayer_config.json | 5 + .../polymarket_mock/test_polymarket_mock.py | 255 ++++++++++ gravity_e2e/docs/MANUAL.md | 145 +++++- .../gravity_e2e/utils/mock_binance_index.py | 118 +++++ .../utils/mock_polymarket_polygon.py | 388 ++++++++++++++ .../gravity_e2e/utils/oracle_test_support.py | 476 ++++++++++++++++++ gravity_e2e/runner.py | 38 +- 33 files changed, 3684 insertions(+), 166 deletions(-) create mode 100644 .agents/skills/gravity-oracle-demo/SKILL.md create mode 100644 gravity_e2e/cluster_test_cases/binance_price_feed/README.md create mode 100644 gravity_e2e/cluster_test_cases/binance_price_feed/cluster.toml create mode 100644 gravity_e2e/cluster_test_cases/binance_price_feed/genesis.toml create mode 100644 gravity_e2e/cluster_test_cases/binance_price_feed/hooks.py create mode 100644 gravity_e2e/cluster_test_cases/binance_price_feed/mock_binance.py create mode 100644 gravity_e2e/cluster_test_cases/binance_price_feed/relayer_config.json create mode 100644 gravity_e2e/cluster_test_cases/binance_price_feed/test_binance_price_feed.py create mode 100644 gravity_e2e/cluster_test_cases/oracle_demo/README.md create mode 100644 gravity_e2e/cluster_test_cases/oracle_demo/cluster.toml create mode 100644 gravity_e2e/cluster_test_cases/oracle_demo/genesis.toml create mode 100644 gravity_e2e/cluster_test_cases/oracle_demo/hooks.py create mode 100644 gravity_e2e/cluster_test_cases/oracle_demo/relayer_config.json create mode 100644 gravity_e2e/cluster_test_cases/oracle_demo/test_oracle_demo.py create mode 100644 gravity_e2e/cluster_test_cases/polymarket_mock/README.md create mode 100644 gravity_e2e/cluster_test_cases/polymarket_mock/cluster.toml create mode 100644 gravity_e2e/cluster_test_cases/polymarket_mock/genesis.toml create mode 100644 gravity_e2e/cluster_test_cases/polymarket_mock/hooks.py create mode 100644 gravity_e2e/cluster_test_cases/polymarket_mock/relayer_config.json create mode 100644 gravity_e2e/cluster_test_cases/polymarket_mock/test_polymarket_mock.py create mode 100644 gravity_e2e/gravity_e2e/utils/mock_binance_index.py create mode 100644 gravity_e2e/gravity_e2e/utils/mock_polymarket_polygon.py create mode 100644 gravity_e2e/gravity_e2e/utils/oracle_test_support.py diff --git a/.agents/skills/gravity-oracle-demo/SKILL.md b/.agents/skills/gravity-oracle-demo/SKILL.md new file mode 100644 index 000000000..68ed81d21 --- /dev/null +++ b/.agents/skills/gravity-oracle-demo/SKILL.md @@ -0,0 +1,155 @@ +--- +name: gravity-oracle-demo +description: Use when the user asks how to run, demo, debug, or restart Gravity oracle tests, especially Binance index-kline price feeds, Polymarket settlement mirrors, or the combined frontend dashboard. +--- + +# Gravity Oracle Demo + +Use this skill for Gravity oracle demo and integration-test operations that span `gravity-sdk`, +`gravity-reth`, `gravity_chain_core_contracts`, and the demo web app. + +## Safety + +- Ask before outbound public API/RPC requests unless the user has already + approved that exact live run. +- Do not commit real RPC URLs, API keys, `.env` files, or absolute user-specific + paths in docs. +- Prefer deterministic mock E2E for CI and review. Use live mode only for demos + or explicitly approved validation. +- Do not stop a cluster unless the user asks or the running process was started + by the current task and is clearly part of cleanup. + +## Binance Price Feed Demo + +Default deterministic suite: + +```bash +cd gravity-sdk +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh binance_price_feed --force-init +``` + +Long-running frontend backend with the local mock: + +```bash +cd gravity-sdk +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh binance_price_feed \ + --force-init \ + --keep-running \ + --demo-config-out ../gravity_price_feed_demo_web/public/demo-config.json \ + --log-cli-level=INFO +``` + +Live Binance market-data demo: + +```bash +cd gravity-sdk +BINANCE_PRICE_FEED_MODE=live \ +BINANCE_PRICE_FEED_BASE_URL=https://testnet.binancefuture.com \ +BINANCE_PRICE_FEED_LAG_MINUTES=3 \ +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh binance_price_feed \ + --keep-running \ + --demo-config-out ../gravity_price_feed_demo_web/public/demo-config.json \ + --log-cli-level=INFO +``` + +Notes: + +- Production `https://fapi.binance.com` may return HTTP `451` from restricted + locations. For demos, `https://testnet.binancefuture.com` exposes the same + `indexPriceKlines` shape and worked for TSLAUSDT/NVDAUSDT. +- `indexPriceKlines` is public market data; this implementation does not need + `BINANCE_API_KEY` or `BINANCE_SECRET_KEY`. +- Live mode computes a recently closed 1-minute bucket, then continuous tasks + advance by delivery nonce: nonce `n` maps to bucket `start + (n - 1) * 1m`. +- The default live task uses `graceMs=120000` and a three-minute lag so the + first requested bucket is already beyond that grace period. +- A healthy demo shows `NativeOracle` nonce and resolver `roundId` increasing + every closed minute. + +Start or refresh the frontend: + +```bash +cd gravity_price_feed_demo_web +npm run dev +``` + +Open the URL printed by the dev server. The page reads `public/demo-config.json` +and proxies `/rpc` to the Gravity devnet. + +Stop the backend: + +```bash +cd gravity-sdk +bash cluster/stop.sh --config gravity_e2e/cluster_test_cases/binance_price_feed/cluster.toml +``` + +If mock mode is still running, also stop the local mock server: + +```bash +kill "$(cat gravity_e2e/cluster_test_cases/binance_price_feed/artifacts/mock_binance.pid)" +``` + +## Combined Price Feed + Polymarket Dashboard + +Use `oracle_demo` when the user wants one frontend showing both Binance price +feeds and a Polymarket mirror in the same local Gravity cluster. + +Run the deterministic local backend: + +```bash +cd gravity-sdk +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh oracle_demo \ + --force-init \ + --keep-running \ + --demo-config-out ../gravity_price_feed_demo_web/public/demo-config.json \ + --log-cli-level=INFO +``` + +Then start the dashboard: + +```bash +cd gravity_price_feed_demo_web +npm run dev +``` + +This suite sends no public internet requests. It starts: + +- a local Binance `indexPriceKlines` mock for `sourceType=3` +- a local Polygon JSON-RPC mock for a Fed-style binary CTF settlement +- one Gravity cluster that records both oracle lanes + +Stop it with: + +```bash +cd gravity-sdk +bash cluster/stop.sh --config gravity_e2e/cluster_test_cases/oracle_demo/cluster.toml +kill "$(cat gravity_e2e/cluster_test_cases/oracle_demo/artifacts/mock_binance.pid)" +``` + +## Polymarket Mirror Demo + +Run the offline settlement rail first: + +```bash +cd gravity-sdk +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh polymarket_mock --force-init +``` + +What it proves: + +- `sourceType=6` relayer reads a Polygon-like CTF `ConditionResolution` log. +- Unsupported-JWK consensus carries canonical settlement bytes to + `NativeOracle`. +- `PolymarketSettlementResolver` validates and stores the payout vector. +- A Gravity market contract settles and releases claims from that resolver. + +For real Polymarket mirrors, do not infer a market from a Polygon log alone. +Start from a reviewed manifest: slug/title/rules, `conditionId`, `questionId`, +CTF address, outcome labels, `slotToOutcome`, source block range, and hashes of +raw metadata snapshots. Then register exactly one `(sourceType=6, mirrorId)` per +reviewed CTF condition. diff --git a/.agents/workflows/build-and-test.md b/.agents/workflows/build-and-test.md index e03cf8f3a..2f85bfde2 100644 --- a/.agents/workflows/build-and-test.md +++ b/.agents/workflows/build-and-test.md @@ -131,7 +131,61 @@ Genesis artifacts are cached in `cluster_test_cases//artifacts/`. Use `-- --- -## 4. Bridge E2E Test Details +## 4. Polymarket Mock Oracle E2E + +Suite directory: `gravity_e2e/cluster_test_cases/polymarket_mock/` + +This suite is the SDK-side integration proof for a Polymarket-like Gravity +market. It does not hit Polygon. Instead, `hooks.py` starts +`MockPolymarketPolygon` on localhost, `relayer_config.json` maps the configured +Gravity oracle URI to that mock, and the test waits for the existing relayer plus +unsupported-JWK/oracle consensus path to write a `sourceType=6` payload into +`NativeOracle`. + +Run from the repository root: + +// turbo +```bash +PATH="$CONDA_PREFIX/bin:$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh polymarket_mock --force-init +``` + +If the shell already activated `gravity_e2e/.venv`, use: + +// turbo +```bash +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh polymarket_mock --force-init +``` + +Expected successful output includes: + +```text +Released mock Polymarket settlement: winning_slot= payout= +Polymarket match market resolved and claimed: marketId=1 winningSlot= totalPool=600000000000000000000 +PASSED +Suite polymarket_mock PASSED +All suites passed! +``` + +Product mapping: + +1. Gravity market creation stores the Polygon CTF reference. +2. The relayer reads the CTF `ConditionResolution` log from the configured URI. +3. Validators agree on canonical settlement bytes through the existing oracle + consensus path. +4. `NativeOracle` records the agreed payload. +5. Contract-side resolver logic validates the CTF metadata and stores the payout + vector. +6. The market contract settles and pays the winner. + +This is intentionally a settlement-rail PoC. Dynamic request discovery can be +layered on later with finalized request events, watermarks, deadlines, and typed +pending/unknown/expired states. + +--- + +## 5. Bridge E2E Test Details Suite directory: `gravity_e2e/cluster_test_cases/bridge/` @@ -162,7 +216,7 @@ Suite directory: `gravity_e2e/cluster_test_cases/bridge/` --- -## 5. Running Contract Unit Tests +## 6. Running Contract Unit Tests Working directory: `gravity_chain_core_contracts` @@ -179,4 +233,4 @@ forge test --match-path test/unit/integration/ConsensusEngineFlow.t.sol -vvv Run specific test function: ```bash forge test --match-test testBridgeFlow -vvv -``` \ No newline at end of file +``` diff --git a/.gitignore b/.gitignore index 184015286..a46bb93f8 100644 --- a/.gitignore +++ b/.gitignore @@ -112,6 +112,7 @@ lib/ # Hardhat (if ever used) artifacts/ +mock_polymarket_metadata.json cache/ typechain-types/ diff --git a/Cargo.lock b/Cargo.lock index db20a623e..bc66b38ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4608,7 +4608,7 @@ dependencies = [ "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -7901,7 +7901,7 @@ dependencies = [ [[package]] name = "gravity-precompiles" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "blst", @@ -7913,7 +7913,7 @@ dependencies = [ [[package]] name = "gravity-primitives" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" [[package]] name = "gravity-sdk" @@ -7927,7 +7927,7 @@ dependencies = [ [[package]] name = "gravity-storage" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "async-trait", @@ -8049,7 +8049,7 @@ dependencies = [ [[package]] name = "greth" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "async-trait", "gravity-primitives", @@ -11542,7 +11542,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.114", @@ -12768,7 +12768,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.114", @@ -13503,7 +13503,7 @@ checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "reth" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-rpc-types", "aquamarine", @@ -13549,7 +13549,7 @@ dependencies = [ [[package]] name = "reth-basic-payload-builder" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -13573,7 +13573,7 @@ dependencies = [ [[package]] name = "reth-chain-state" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -13602,7 +13602,7 @@ dependencies = [ [[package]] name = "reth-chainspec" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-chains", "alloy-consensus", @@ -13622,7 +13622,7 @@ dependencies = [ [[package]] name = "reth-cli" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-genesis", "clap 4.5.57", @@ -13636,7 +13636,7 @@ dependencies = [ [[package]] name = "reth-cli-commands" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-chains", "alloy-consensus", @@ -13713,7 +13713,7 @@ dependencies = [ [[package]] name = "reth-cli-runner" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "reth-tasks", "tokio", @@ -13723,7 +13723,7 @@ dependencies = [ [[package]] name = "reth-cli-util" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-eips", "alloy-primitives", @@ -13741,7 +13741,7 @@ dependencies = [ [[package]] name = "reth-codecs" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -13759,7 +13759,7 @@ dependencies = [ [[package]] name = "reth-codecs-derive" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "convert_case 0.7.1", "proc-macro2", @@ -13770,7 +13770,7 @@ dependencies = [ [[package]] name = "reth-config" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "eyre", "humantime-serde", @@ -13785,7 +13785,7 @@ dependencies = [ [[package]] name = "reth-consensus" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -13798,7 +13798,7 @@ dependencies = [ [[package]] name = "reth-consensus-common" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -13810,7 +13810,7 @@ dependencies = [ [[package]] name = "reth-consensus-debug-client" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -13836,7 +13836,7 @@ dependencies = [ [[package]] name = "reth-db" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "derive_more 2.1.1", @@ -13857,7 +13857,7 @@ dependencies = [ [[package]] name = "reth-db-api" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-genesis", @@ -13882,7 +13882,7 @@ dependencies = [ [[package]] name = "reth-db-common" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-genesis", @@ -13913,7 +13913,7 @@ dependencies = [ [[package]] name = "reth-db-models" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-eips", "alloy-primitives", @@ -13927,7 +13927,7 @@ dependencies = [ [[package]] name = "reth-discv4" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -13953,7 +13953,7 @@ dependencies = [ [[package]] name = "reth-discv5" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -13977,7 +13977,7 @@ dependencies = [ [[package]] name = "reth-dns-discovery" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "data-encoding", @@ -14001,7 +14001,7 @@ dependencies = [ [[package]] name = "reth-downloaders" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14031,7 +14031,7 @@ dependencies = [ [[package]] name = "reth-ecies" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "aes", "alloy-primitives", @@ -14062,7 +14062,7 @@ dependencies = [ [[package]] name = "reth-engine-local" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -14084,7 +14084,7 @@ dependencies = [ [[package]] name = "reth-engine-primitives" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14109,7 +14109,7 @@ dependencies = [ [[package]] name = "reth-engine-service" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "futures", "pin-project", @@ -14132,7 +14132,7 @@ dependencies = [ [[package]] name = "reth-engine-tree" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14184,7 +14184,7 @@ dependencies = [ [[package]] name = "reth-engine-util" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-rpc-types-engine", @@ -14212,7 +14212,7 @@ dependencies = [ [[package]] name = "reth-era" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14228,7 +14228,7 @@ dependencies = [ [[package]] name = "reth-era-downloader" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "bytes", @@ -14243,7 +14243,7 @@ dependencies = [ [[package]] name = "reth-era-utils" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -14265,7 +14265,7 @@ dependencies = [ [[package]] name = "reth-errors" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "reth-consensus", "reth-execution-errors", @@ -14276,7 +14276,7 @@ dependencies = [ [[package]] name = "reth-eth-wire" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-chains", "alloy-primitives", @@ -14304,7 +14304,7 @@ dependencies = [ [[package]] name = "reth-eth-wire-types" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-chains", "alloy-consensus", @@ -14325,7 +14325,7 @@ dependencies = [ [[package]] name = "reth-ethereum-cli" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "clap 4.5.57", "eyre", @@ -14347,7 +14347,7 @@ dependencies = [ [[package]] name = "reth-ethereum-consensus" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14363,7 +14363,7 @@ dependencies = [ [[package]] name = "reth-ethereum-engine-primitives" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-eips", "alloy-primitives", @@ -14381,7 +14381,7 @@ dependencies = [ [[package]] name = "reth-ethereum-forks" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-eip2124", "alloy-hardforks", @@ -14394,7 +14394,7 @@ dependencies = [ [[package]] name = "reth-ethereum-payload-builder" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14423,7 +14423,7 @@ dependencies = [ [[package]] name = "reth-ethereum-primitives" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14442,7 +14442,7 @@ dependencies = [ [[package]] name = "reth-etl" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "rayon", "reth-db-api", @@ -14452,7 +14452,7 @@ dependencies = [ [[package]] name = "reth-evm" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14475,7 +14475,7 @@ dependencies = [ [[package]] name = "reth-evm-ethereum" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14497,7 +14497,7 @@ dependencies = [ [[package]] name = "reth-execution-errors" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-evm", "alloy-primitives", @@ -14510,7 +14510,7 @@ dependencies = [ [[package]] name = "reth-execution-types" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14528,7 +14528,7 @@ dependencies = [ [[package]] name = "reth-exex" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14566,7 +14566,7 @@ dependencies = [ [[package]] name = "reth-exex-types" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-eips", "alloy-primitives", @@ -14580,7 +14580,7 @@ dependencies = [ [[package]] name = "reth-fs-util" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "serde", "serde_json", @@ -14590,7 +14590,7 @@ dependencies = [ [[package]] name = "reth-invalid-block-hooks" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -14617,7 +14617,7 @@ dependencies = [ [[package]] name = "reth-ipc" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "bytes", "futures", @@ -14637,7 +14637,7 @@ dependencies = [ [[package]] name = "reth-metrics" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "futures", "metrics", @@ -14649,7 +14649,7 @@ dependencies = [ [[package]] name = "reth-net-banlist" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", ] @@ -14657,7 +14657,7 @@ dependencies = [ [[package]] name = "reth-net-nat" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "futures-util", "if-addrs", @@ -14671,7 +14671,7 @@ dependencies = [ [[package]] name = "reth-network" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14726,7 +14726,7 @@ dependencies = [ [[package]] name = "reth-network-api" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -14751,7 +14751,7 @@ dependencies = [ [[package]] name = "reth-network-p2p" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14773,7 +14773,7 @@ dependencies = [ [[package]] name = "reth-network-peers" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -14788,7 +14788,7 @@ dependencies = [ [[package]] name = "reth-network-types" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-eip2124", "humantime-serde", @@ -14802,7 +14802,7 @@ dependencies = [ [[package]] name = "reth-nippy-jar" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "anyhow", "bincode", @@ -14819,7 +14819,7 @@ dependencies = [ [[package]] name = "reth-node-api" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-rpc-types-engine", "eyre", @@ -14843,7 +14843,7 @@ dependencies = [ [[package]] name = "reth-node-builder" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14912,7 +14912,7 @@ dependencies = [ [[package]] name = "reth-node-core" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14965,7 +14965,7 @@ dependencies = [ [[package]] name = "reth-node-ethereum" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-eips", "alloy-network", @@ -15003,7 +15003,7 @@ dependencies = [ [[package]] name = "reth-node-ethstats" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -15027,7 +15027,7 @@ dependencies = [ [[package]] name = "reth-node-events" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -15051,7 +15051,7 @@ dependencies = [ [[package]] name = "reth-node-metrics" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "eyre", "http 1.4.0", @@ -15072,7 +15072,7 @@ dependencies = [ [[package]] name = "reth-node-types" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "reth-chainspec", "reth-db-api", @@ -15084,7 +15084,7 @@ dependencies = [ [[package]] name = "reth-payload-builder" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -15105,7 +15105,7 @@ dependencies = [ [[package]] name = "reth-payload-builder-primitives" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "pin-project", "reth-payload-primitives", @@ -15117,7 +15117,7 @@ dependencies = [ [[package]] name = "reth-payload-primitives" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-eips", "alloy-primitives", @@ -15137,7 +15137,7 @@ dependencies = [ [[package]] name = "reth-payload-validator" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-rpc-types-engine", @@ -15147,7 +15147,7 @@ dependencies = [ [[package]] name = "reth-pipe-exec-layer-event-bus" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "reth-chain-state", @@ -15160,7 +15160,7 @@ dependencies = [ [[package]] name = "reth-pipe-exec-layer-ext-v2" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -15207,7 +15207,7 @@ dependencies = [ [[package]] name = "reth-pipe-exec-layer-relayer" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-network", "alloy-primitives", @@ -15231,7 +15231,7 @@ dependencies = [ [[package]] name = "reth-primitives" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "once_cell", @@ -15244,7 +15244,7 @@ dependencies = [ [[package]] name = "reth-primitives-traits" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -15274,7 +15274,7 @@ dependencies = [ [[package]] name = "reth-provider" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -15317,7 +15317,7 @@ dependencies = [ [[package]] name = "reth-prune" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -15345,7 +15345,7 @@ dependencies = [ [[package]] name = "reth-prune-types" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "derive_more 2.1.1", @@ -15358,7 +15358,7 @@ dependencies = [ [[package]] name = "reth-ress-protocol" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -15377,7 +15377,7 @@ dependencies = [ [[package]] name = "reth-ress-provider" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -15404,7 +15404,7 @@ dependencies = [ [[package]] name = "reth-revm" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "reth-primitives-traits", @@ -15417,7 +15417,7 @@ dependencies = [ [[package]] name = "reth-rpc" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -15497,7 +15497,7 @@ dependencies = [ [[package]] name = "reth-rpc-api" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-eips", "alloy-genesis", @@ -15525,7 +15525,7 @@ dependencies = [ [[package]] name = "reth-rpc-builder" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-network", "alloy-provider", @@ -15564,7 +15564,7 @@ dependencies = [ [[package]] name = "reth-rpc-convert" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-json-rpc", @@ -15585,7 +15585,7 @@ dependencies = [ [[package]] name = "reth-rpc-engine-api" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-eips", "alloy-primitives", @@ -15615,7 +15615,7 @@ dependencies = [ [[package]] name = "reth-rpc-eth-api" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -15659,7 +15659,7 @@ dependencies = [ [[package]] name = "reth-rpc-eth-types" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -15706,7 +15706,7 @@ dependencies = [ [[package]] name = "reth-rpc-layer" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-rpc-types-engine", "http 1.4.0", @@ -15720,7 +15720,7 @@ dependencies = [ [[package]] name = "reth-rpc-server-types" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-eips", "alloy-primitives", @@ -15736,7 +15736,7 @@ dependencies = [ [[package]] name = "reth-stages" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -15780,7 +15780,7 @@ dependencies = [ [[package]] name = "reth-stages-api" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-eips", "alloy-primitives", @@ -15807,7 +15807,7 @@ dependencies = [ [[package]] name = "reth-stages-types" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "bytes", @@ -15820,7 +15820,7 @@ dependencies = [ [[package]] name = "reth-static-file" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "parking_lot 0.12.5", @@ -15840,7 +15840,7 @@ dependencies = [ [[package]] name = "reth-static-file-types" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "clap 4.5.57", @@ -15852,7 +15852,7 @@ dependencies = [ [[package]] name = "reth-storage-api" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -15882,7 +15882,7 @@ dependencies = [ [[package]] name = "reth-storage-errors" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-eips", "alloy-primitives", @@ -15898,7 +15898,7 @@ dependencies = [ [[package]] name = "reth-tasks" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "auto_impl", "dyn-clone", @@ -15916,7 +15916,7 @@ dependencies = [ [[package]] name = "reth-tokio-util" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "tokio", "tokio-stream", @@ -15926,7 +15926,7 @@ dependencies = [ [[package]] name = "reth-tracing" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "clap 4.5.57", "eyre", @@ -15941,7 +15941,7 @@ dependencies = [ [[package]] name = "reth-transaction-pool" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -15983,7 +15983,7 @@ dependencies = [ [[package]] name = "reth-trie" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-eips", @@ -16007,7 +16007,7 @@ dependencies = [ [[package]] name = "reth-trie-common" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -16034,7 +16034,7 @@ dependencies = [ [[package]] name = "reth-trie-db" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -16053,7 +16053,7 @@ dependencies = [ [[package]] name = "reth-trie-parallel" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -16078,7 +16078,7 @@ dependencies = [ [[package]] name = "reth-trie-sparse" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -16097,7 +16097,7 @@ dependencies = [ [[package]] name = "reth-trie-sparse-parallel" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -16115,7 +16115,7 @@ dependencies = [ [[package]] name = "reth-zstd-compressors" version = "1.8.3" -source = "git+https://github.com/Galxe/gravity-reth?rev=b49b4864aeaa3c35c6871a77d7133bb9486edbf1#b49b4864aeaa3c35c6871a77d7133bb9486edbf1" +source = "git+https://github.com/Galxe/gravity-reth?branch=codex%2Fsports-score-oracle-poc#2b46a42f637f29075ee0c54313984ad23ad0f986" dependencies = [ "zstd", ] diff --git a/bin/gravity_node/Cargo.toml b/bin/gravity_node/Cargo.toml index 5f60c3a34..8e6475774 100644 --- a/bin/gravity_node/Cargo.toml +++ b/bin/gravity_node/Cargo.toml @@ -33,7 +33,8 @@ sha2.workspace = true bincode = "1.3" time = "0.3.36" anyhow = "1.0.87" -greth = { git = "https://github.com/Galxe/gravity-reth", rev = "b49b4864aeaa3c35c6871a77d7133bb9486edbf1" } +# Oracle integration branch; Cargo.lock pins the exact gravity-reth revision used by tests. +greth = { git = "https://github.com/Galxe/gravity-reth", branch = "codex/sports-score-oracle-poc" } reqwest = "0.12.9" alloy-primitives = { version = "=1.3.1", default-features = false, features = ["map-foldhash"] } alloy-eips = { version = "^1.0.37", default-features = false } diff --git a/bin/gravity_node/src/relayer.rs b/bin/gravity_node/src/relayer.rs index 0b84de20e..e4363b92c 100644 --- a/bin/gravity_node/src/relayer.rs +++ b/bin/gravity_node/src/relayer.rs @@ -9,7 +9,7 @@ use gaptos::api_types::{ relayer::{PollResult, Relayer}, ExecError, }; -use greth::reth_pipe_exec_layer_relayer::OracleRelayerManager; +use greth::reth_pipe_exec_layer_relayer::{parse_oracle_uri, OracleRelayerManager}; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; use tracing::{info, warn}; @@ -37,19 +37,17 @@ impl RelayerConfig { } } -/// Sanitize a URL for safe logging: keep only scheme://host[:port], redact path and query. -/// e.g. "https://mainnet.infura.io/v3/SECRET_KEY" → "https://mainnet.infura.io/***" +/// Keep only scheme and host for logging; redact userinfo, path, query, and fragment. fn sanitize_url(url: &str) -> String { - if let Some(scheme_end) = url.find("://") { - let scheme = &url[..scheme_end]; - let rest = &url[scheme_end + 3..]; - // Host ends at the first '/' or '?' or end of string - let host_end = rest.find('/').or_else(|| rest.find('?')).unwrap_or(rest.len()); - let host_port = &rest[..host_end]; - format!("{scheme}://{host_port}/***") - } else { - "***".to_string() - } + let Ok(parsed) = reqwest::Url::parse(url) else { + return "***".to_string(); + }; + let Some(host) = parsed.host_str() else { + return "***".to_string(); + }; + let host = if host.contains(':') { format!("[{host}]") } else { host.to_string() }; + let port = parsed.port().map(|value| format!(":{value}")).unwrap_or_default(); + format!("{}://{}{} /***", parsed.scheme(), host, port) } #[derive(Debug, Clone, Default)] @@ -100,7 +98,10 @@ impl RelayerWrapper { let config = config_path .and_then(|path| match RelayerConfig::from_file(&path) { Ok(cfg) => { - info!("Loaded relayer config from {:?}", path); + info!( + file = %path.file_name().and_then(|name| name.to_str()).unwrap_or(""), + "Loaded relayer config" + ); Some(cfg) } Err(e) => { @@ -155,25 +156,14 @@ impl RelayerWrapper { } } - /// Parse URI to extract source_type and source_id - /// URI format: gravity:////? + /// Parse only the source identity used to reconcile on-chain state. + /// + /// The reth relayer validates the task type and provider-specific parameters when the URI is + /// added. Keeping this helper limited to `(source_type, source_id)` avoids duplicating those + /// rules in the SDK wrapper. fn parse_source_from_uri(uri: &str) -> Option<(u32, u64)> { - if !uri.starts_with("gravity://") { - return None; - } - - let rest = &uri[10..]; // len("gravity://") = 10 - let parts: Vec<&str> = rest.split('/').collect(); - if parts.len() < 2 { - return None; - } - - let source_type: u32 = parts[0].parse().ok()?; - // Remove query string from source_id if present - let source_id_str = parts[1].split('?').next()?; - let source_id: u64 = source_id_str.parse().ok()?; - - Some((source_type, source_id)) + let task = parse_oracle_uri(uri).ok()?; + Some((task.source_type, task.source_id)) } /// Find oracle state for a URI by matching source_type and source_id @@ -271,11 +261,14 @@ impl Relayer for RelayerWrapper { // Get onchain state for this URI using source_type/source_id from URI let oracle_states = Self::get_oracle_source_states().await; - info!("Oracle states: {:?}", oracle_states); - let oracle_state = - Self::find_oracle_state_for_uri(uri, &oracle_states).ok_or_else(|| { + let oracle_state = Self::find_oracle_state_for_uri(uri, &oracle_states).ok_or_else(|| { + let available_sources = oracle_states + .iter() + .map(|state| format!("{}:{}", state.source_type, state.source_id)) + .collect::>() + .join(", "); ExecError::Other(format!( - "Oracle state not found for URI: {uri}. Available states: {oracle_states:?}" + "Oracle state not found for URI: {uri}. Available source identities: [{available_sources}]" )) })?; @@ -395,4 +388,36 @@ mod tests { ); assert!(fresh.updated, "a genuinely-new observation (8 > 7) must NOT be suppressed"); } + + #[test] + fn test_parse_price_feed_source_uri() { + let uri = "gravity://3/1001/price_feed?provider=binance_index_kline_v1"; + assert_eq!(RelayerWrapper::parse_source_from_uri(uri), Some((3, 1001))); + } + + #[test] + fn test_parse_blockchain_source_uri() { + let uri = "gravity://0/31337/events?contract=0x0000000000000000000000000000000000000001"; + assert_eq!(RelayerWrapper::parse_source_from_uri(uri), Some((0, 31337))); + } + + #[test] + fn test_parse_source_accepts_identity_only_uri() { + assert_eq!(RelayerWrapper::parse_source_from_uri("gravity://3/1001"), Some((3, 1001))); + } + + #[test] + fn test_parse_source_rejects_unroutable_uri() { + assert_eq!(RelayerWrapper::parse_source_from_uri("gravity://price/1001"), None); + assert_eq!(RelayerWrapper::parse_source_from_uri("https://oracle.example/3/1001"), None); + } + + #[test] + fn test_sanitize_url_removes_credentials_and_path() { + let sanitized = sanitize_url("https://user:secret@polygon.example:8443/v1/key?token=abc"); + assert_eq!(sanitized, "https://polygon.example:8443 /***"); + assert!(!sanitized.contains("user")); + assert!(!sanitized.contains("secret")); + assert!(!sanitized.contains("token")); + } } diff --git a/cluster/deploy.sh b/cluster/deploy.sh index 5e258758a..1a4b37422 100755 --- a/cluster/deploy.sh +++ b/cluster/deploy.sh @@ -432,9 +432,9 @@ done < <(jq -r '.env_vars | to_entries[] | .key, .value' "$reth_config") export RUST_BACKTRACE=1 pid=$( - env ${env_vars_array[*]} ${WORKSPACE}/bin/gravity_node node \ + nohup setsid env ${env_vars_array[*]} ${WORKSPACE}/bin/gravity_node node \ ${reth_args_array[*]} \ - > "${WORKSPACE}/logs/debug.log" 2>&1 & + > "${WORKSPACE}/logs/debug.log" 2>&1 < /dev/null & echo $! ) echo $pid > "${WORKSPACE}/script/node.pid" @@ -558,9 +558,9 @@ done < <(jq -r '.env_vars | to_entries[] | .key, .value' "$reth_config") export RUST_BACKTRACE=1 pid=$( - env ${env_vars_array[*]} ${WORKSPACE}/bin/gravity_node node \ + nohup setsid env ${env_vars_array[*]} ${WORKSPACE}/bin/gravity_node node \ ${reth_args_array[*]} \ - > "${WORKSPACE}/logs/debug.log" 2>&1 & + > "${WORKSPACE}/logs/debug.log" 2>&1 < /dev/null & echo $! ) echo $pid > "${WORKSPACE}/script/node.pid" @@ -689,9 +689,9 @@ done < <(jq -r '.env_vars | to_entries[] | .key, .value' "$reth_config") export RUST_BACKTRACE=1 pid=$( - env ${env_vars_array[*]} ${WORKSPACE}/bin/gravity_node node \ + nohup setsid env ${env_vars_array[*]} ${WORKSPACE}/bin/gravity_node node \ ${reth_args_array[*]} \ - > "${WORKSPACE}/logs/debug.log" 2>&1 & + > "${WORKSPACE}/logs/debug.log" 2>&1 < /dev/null & echo $! ) echo $pid > "${WORKSPACE}/script/node.pid" @@ -868,6 +868,7 @@ main() { export VFN_PORT=$(echo "$node" | jq -r '.vfn_port // "null"') export PUBLIC_PORT=$(echo "$node" | jq -r '.public_port // "null"') export RPC_PORT=$(echo "$node" | jq -r '.rpc_port') + export API_PORT=$(echo "$node" | jq -r '.api_port // 8080') export WS_PORT=$(echo "$node" | jq -r '.ws_port // "null"') export METRICS_PORT=$(echo "$node" | jq -r '.metrics_port') export INSPECTION_PORT=$(echo "$node" | jq -r '.inspection_port') diff --git a/cluster/genesis.sh b/cluster/genesis.sh index 4bb6a3013..440620ca2 100755 --- a/cluster/genesis.sh +++ b/cluster/genesis.sh @@ -36,10 +36,27 @@ main() { # Step 1: Clone/update external dependencies log_info "Step 1: Checking external dependencies..." + GENESIS_PATH=$(echo "$config_json" | jq -r '.dependencies.genesis_contracts.path // empty') GENESIS_REPO=$(echo "$config_json" | jq -r '.dependencies.genesis_contracts.repo // "https://github.com/Galxe/gravity_chain_core_contracts.git"') GENESIS_REF=$(echo "$config_json" | jq -r '.dependencies.genesis_contracts.ref // "main"') - - GENESIS_CONTRACT_DIR="$EXTERNAL_DIR/gravity_chain_core_contracts" + + GENESIS_CONTRACT_SOURCE_DIR="" + if [ -n "$GENESIS_PATH" ]; then + if [[ "$GENESIS_PATH" == /* ]]; then + GENESIS_CONTRACT_SOURCE_DIR="$GENESIS_PATH" + else + GENESIS_CONTRACT_SOURCE_DIR="$(cd "$(dirname "$GENESIS_CONFIG_FILE")" && realpath "$GENESIS_PATH")" + fi + if [ ! -d "$GENESIS_CONTRACT_SOURCE_DIR/.git" ]; then + log_error "Local genesis_contracts.path is not a git checkout: $GENESIS_CONTRACT_SOURCE_DIR" + exit 1 + fi + GENESIS_REPO="$GENESIS_CONTRACT_SOURCE_DIR" + GENESIS_CONTRACT_DIR="$EXTERNAL_DIR/gravity_chain_core_contracts_local" + log_info "Using local genesis contracts source: $GENESIS_CONTRACT_SOURCE_DIR" + else + GENESIS_CONTRACT_DIR="$EXTERNAL_DIR/gravity_chain_core_contracts" + fi if [ ! -d "$GENESIS_CONTRACT_DIR" ]; then log_warn "gravity_chain_core_contracts not found. Cloning from $GENESIS_REPO..." @@ -47,6 +64,13 @@ main() { git clone "$GENESIS_REPO" "$GENESIS_CONTRACT_DIR" fi + if [ -n "$GENESIS_CONTRACT_SOURCE_DIR" ]; then + ( + cd "$GENESIS_CONTRACT_DIR" + git remote set-url origin "$GENESIS_REPO" + ) + fi + # Checkout specified ref and pull latest (critical for branches to avoid stale bytecode) log_info "Checking out ref: $GENESIS_REF..." ( @@ -79,6 +103,13 @@ if p.exists(): " cd - ) + + if [ -n "$GENESIS_CONTRACT_SOURCE_DIR" ] \ + && [ ! -d "$GENESIS_CONTRACT_DIR/node_modules" ] \ + && [ -d "$GENESIS_CONTRACT_SOURCE_DIR/node_modules" ]; then + log_info "Reusing node_modules from local genesis contracts source..." + cp -a "$GENESIS_CONTRACT_SOURCE_DIR/node_modules" "$GENESIS_CONTRACT_DIR/node_modules" + fi # Install dependencies if missing if [ ! -d "$GENESIS_CONTRACT_DIR/node_modules" ]; then diff --git a/cluster/templates/validator.yaml.tpl b/cluster/templates/validator.yaml.tpl index 47f7418f9..ea5443d33 100644 --- a/cluster/templates/validator.yaml.tpl +++ b/cluster/templates/validator.yaml.tpl @@ -56,6 +56,9 @@ ${DISCOVERY_METHOD_FULLNODE_BLOCK} storage: dir: "${STORAGE_DIR}" +api: + address: 127.0.0.1:${API_PORT} + log_file_path: "${LOG_DIR}/consensus_log/validator.log" inspection_service: diff --git a/gravity_e2e/cluster_test_cases/binance_price_feed/README.md b/gravity_e2e/cluster_test_cases/binance_price_feed/README.md new file mode 100644 index 000000000..fc444e9c3 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/binance_price_feed/README.md @@ -0,0 +1,200 @@ +# Binance Index-Kline Price Feed Integration Test + +This suite proves the Gravity oracle path for stock-like price feeds using +Binance USD-M `indexPriceKlines` semantics: + +1. Governance deploys `PriceFeedResolver`. +2. Governance sets the `NativeOracle` default callback for `sourceType=3`. +3. Governance registers two `OracleTaskConfig` tasks: + - `feedId=1001`: `provider=binance_index_kline_v1&pair=NVDAUSDT`. + - `feedId=1002`: `provider=binance_index_kline_v1&pair=TSLAUSDT`. +4. Each task is a continuous 1-minute feed anchored at the same first bucket: + - `bucketStartMs=1783252500000` (`2026-07-05T11:55:00Z`). + - `continuous=true`, so delivery nonce `1` maps to that bucket. + - Delivery nonce `3` maps to `roundId=29720877` and + `resolvedAt=1783252679999`. +5. The suite uses a 30-second epoch so `aptos-jwk-consensus` rebuilds its + provider list after the governance update. +6. The pytest process runs a local mock Binance server for + `/fapi/v1/indexPriceKlines`. The mock validates `pair`, `interval`, + `startTime`, `endTime`, and `limit`, then returns deterministic kline rows. +7. `gravity-reth` discovers `sourceType=3` as a relayer-backed task, adds the + URI from local `relayer_config.json`, fetches the mock Binance kline through + the `provider=binance_index_kline_v1` adapter, and publishes canonical price + bytes through the unsupported-JWK consensus path. +8. `NativeOracle` records the payloads and calls `PriceFeedResolver`. +9. The test waits until `NativeOracle` reaches delivery nonce `3` for each + feed, then checks `priceRounds(feedId, 29720877)`. + +By default the suite does not call live Binance. It exercises the same +HTTP/JSON adapter path against a local deterministic mock so every validator +produces byte-identical payloads. For demos, set `BINANCE_PRICE_FEED_MODE=live` +to point the same tasks at Binance public market-data APIs. + +This is intentionally an epoch-config test, not a dynamic request watcher test. +The current relayer-backed JWK path rebuilds observers from on-chain config at +epoch boundaries. Per-request discovery inside an epoch still needs a separate +deterministic watcher. + +## Run + +Build `gravity_node` and `gravity_cli`, then run from the repository root: + +```bash +PATH="$CONDA_PREFIX/bin:$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh binance_price_feed --force-init +``` + +If you use a virtualenv instead of conda, activate it first and omit +`$CONDA_PREFIX/bin` from the `PATH` prefix: + +```bash +source gravity_e2e/.venv/bin/activate +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh binance_price_feed --force-init +``` + +## Run As A Live Demo Backend + +For a frontend demo, run the same suite in keep-running mode and write a runtime +config JSON into the frontend repository: + +```bash +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh binance_price_feed \ + --force-init \ + --keep-running \ + --demo-config-out ../gravity_price_feed_demo_web/public/demo-config.json \ + --log-cli-level=INFO +``` + +In this mode the runner: + +- Starts the Gravity cluster. +- Starts a long-running local Binance index-kline mock on port `18547`. +- Runs the e2e assertions. +- Leaves the cluster and mock running after success. +- Writes `public/demo-config.json` for the web app. + +## Run With Live Binance Market Data + +For a pure demo, use live mode. This sends outbound requests to Binance public +market-data APIs. `indexPriceKlines` does not require `BINANCE_API_KEY` or +`BINANCE_SECRET_KEY`. + +```bash +BINANCE_PRICE_FEED_MODE=live \ +BINANCE_PRICE_FEED_LAG_MINUTES=3 \ +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh binance_price_feed \ + --force-init \ + --keep-running \ + --demo-config-out ../gravity_price_feed_demo_web/public/demo-config.json \ + --log-cli-level=INFO +``` + +Live mode defaults to a 1-minute bucket that is at least two minutes past close +(`graceMs=120000`), generates a run-local relayer config under `artifacts/`, +registers matching task URIs, and uses: + +```text +GET https://fapi.binance.com/fapi/v1/indexPriceKlines + ?pair= + &interval=1m + &startTime= + &endTime= + &limit=1 +``` + +The live run still writes through the full Gravity path: +relayer fetch -> canonical bytes -> unsupported-JWK consensus -> `NativeOracle` +-> `PriceFeedResolver`. + +If the machine is blocked by Binance production Futures API with HTTP `451`, +use the official Futures testnet market-data host for demos: + +```bash +BINANCE_PRICE_FEED_MODE=live \ +BINANCE_PRICE_FEED_BASE_URL=https://testnet.binancefuture.com \ +BINANCE_PRICE_FEED_LAG_MINUTES=3 \ +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh binance_price_feed \ + --keep-running \ + --demo-config-out ../gravity_price_feed_demo_web/public/demo-config.json \ + --log-cli-level=INFO +``` + +The testnet host is still live Binance public market data, not the deterministic +local mock. A successful long-running demo should show `NativeOracle` delivery +nonces increasing as each closed 1-minute bucket is written on-chain. + +Start the web dashboard in the frontend repository: + +```bash +npm run dev +``` + +Then open the local URL printed by the dev server. The dashboard reads +`public/demo-config.json`, proxies `/rpc` to the live Gravity devnet, and should +show `provider.kind = live-binance`. + +Stop the demo backend with: + +```bash +bash cluster/stop.sh --config gravity_e2e/cluster_test_cases/binance_price_feed/cluster.toml +kill "$(cat gravity_e2e/cluster_test_cases/binance_price_feed/artifacts/mock_binance.pid)" +``` + +The `kill` command is only needed for mock mode. Live mode does not start the +local mock Binance server. + +## Expected Effect + +Successful logs include: + +```text +Binance price feed resolved: feedId=1001 deliveryNonce=3 roundId=29720877 price=19612645000 +Binance price feed resolved: feedId=1002 deliveryNonce=3 roundId=29720877 price=40117545000 +PASSED +Suite binance_price_feed PASSED +All suites passed! +``` + +The expected price math uses single-source weighted median aggregation over the +mock Binance kline close field: + +```text +NVDAUSDT nonce 3 = parse_fixed_decimal("196.12645000", 8) = 19612645000 +TSLAUSDT nonce 3 = parse_fixed_decimal("401.17545000", 8) = 40117545000 +``` + +All prices use 8 decimals. + +Note that `NativeOracle` delivery nonces are sequential (`1`, `2`, `3`, ...), +while resolver payload `roundId` values are time-derived Binance bucket IDs. +The relayer keeps those two concepts separate so long-running feeds can satisfy +`NativeOracle.latestNonce + 1` while still storing price rounds by market time. + +## Extending Toward a Polymarket-Like Gravity Product + +For Polymarket-style sports markets, keep the current split: + +- `sourceType=6`: mirror finalized Polygon CTF settlements into + `PolymarketSettlementResolver`. +- Market contracts consume resolver state and settle YES/NO or multi-outcome + markets. + +For price-index markets or perps, use this suite as the base: + +- `sourceType=3`: feed equity/crypto/index price rounds into + `PriceFeedResolver`. +- The downstream market or PerpDex contract decides how to use + `latestPrice(feedId)`. +- The relayer adapter should only fetch and canonicalize source observations. + Product-level BBO, mid-price, TWAP, risk, and weighting policy should live in + the consuming contract or in a separately versioned resolver policy. + +The production version should replace the local mock with a deterministic +provider policy: local provider allowlist, closed-bucket request schedule, +source timestamps, deadline/expiry, and payload hashes bound into the signed +oracle bytes. diff --git a/gravity_e2e/cluster_test_cases/binance_price_feed/cluster.toml b/gravity_e2e/cluster_test_cases/binance_price_feed/cluster.toml new file mode 100644 index 000000000..fa229a1f8 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/binance_price_feed/cluster.toml @@ -0,0 +1,27 @@ +# Gravity Cluster Configuration - Binance index-kline price feed integration test + +[cluster] +name = "gravity-devnet-binance-price-feed" +base_dir = "/tmp/gravity-cluster-binance-price-feed" + +[genesis_source] +genesis_path = "./artifacts/genesis.json" +waypoint_path = "./artifacts/waypoint.txt" + +[[nodes]] +id = "node1" +role = "genesis" +source = { project_path = "../" } +host = "127.0.0.1" +validator_port = 45382 +vfn_port = 45392 +rpc_port = 48755 +api_port = 48280 +metrics_port = 49203 +inspection_port = 40102 +https_port = 41124 +authrpc_port = 48775 +reth_p2p_port = 52226 + +[faucet_init] +num_accounts = 0 diff --git a/gravity_e2e/cluster_test_cases/binance_price_feed/genesis.toml b/gravity_e2e/cluster_test_cases/binance_price_feed/genesis.toml new file mode 100644 index 000000000..fa31c3ecb --- /dev/null +++ b/gravity_e2e/cluster_test_cases/binance_price_feed/genesis.toml @@ -0,0 +1,74 @@ +# Gravity Genesis Configuration - Binance index-kline price feed integration test + +[dependencies.genesis_contracts] +path = "../../../../gravity_chain_core_contracts" +ref = "codex/sports-score-oracle-poc" + +[[genesis_validators]] +id = "node1" +address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +host = "127.0.0.1" +validator_port = 45382 +vfn_port = 45392 +stake_amount = "2000000000000000000" +voting_power = "2000000000000000000" +consensus_pop = "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +[genesis] +chain_id = 1337 +# Keep the suite fast: task/callback changes are picked up when the JWK +# consensus epoch manager rebuilds providers for the next epoch. +epoch_interval_micros = 30000000 +major_version = 1 +consensus_config = "0x0301010a00000000000000280000000000000001010000000a000000000000000100010200000000000000000020000000000000" +execution_config = "0x00" +initial_locked_until_micros = 1798848000000000 + +governance_owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + +[genesis.faucet] +address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +balance = "0x2000000000000000000000000000000000000000000000000000000000000000" + +[genesis.validator_config] +minimum_bond = "1000000000000000000" +maximum_bond = "1000000000000000000000000" +unbonding_delay_micros = 604800000000 +allow_validator_set_change = true +voting_power_increase_limit_pct = 20 +max_validator_set_size = "100" +auto_evict_enabled = false +auto_evict_threshold_pct = 0 + +[genesis.staking_config] +minimum_stake = "1000000000000000000" +lockup_duration_micros = 86400000000 +unbonding_delay_micros = 86400000000 + +[genesis.governance_config] +min_voting_threshold = "1000000000000000000" +required_proposer_stake = "1000000000000000000" +voting_duration_micros = 5000000 + +[genesis.randomness_config] +variant = 1 +secrecy_threshold = 9223372036854775808 +reconstruction_threshold = 12297829382473033728 +fast_path_secrecy_threshold = 12297829382473033728 + +[genesis.oracle_config] +source_types = [1, 3] +callbacks = [ + "0x00000000000000000000000000000001625F4001", + "0x0000000000000000000000000000000000000000", +] + +[genesis.jwk_config] +issuers = ["0x68747470733a2f2f6163636f756e74732e676f6f676c652e636f6d"] + +[[genesis.jwk_config.jwks]] +kid = "f5f4c0ae6e6090a65ab0a694d6ba6f19d5d0b4e6" +kty = "RSA" +alg = "RS256" +e = "AQAB" +n = "2K7epoJWl_aBoYGpXmDBBiEnwQ0QdVRU1gsbGXNrEbrZEQdY5KjH5P5gZMq3d3KvT1j5KsD2tF_9jFMDLqV4VWDNJRLgSNJxhJuO_oLO2BXUSL9a7fLHxnZCUfJvT2K-O8AXjT3_ZM8UuL8d4jBn_fZLzdEI4MHrZLVSaHDvvKqL_mExQo6cFD-qyLZ-T6aHv2x8R7L_3X7E1nGMjKVVZMveQ_HMeXvnGxKf5yfEP0hIQlC_kFm4L_1kV1S0UPmMptZL2qI4VnXqmqI6TZJyE-3VXHgNn1Z1O_9QZlPC0fF0spLHf2S3nNqI0v3k2E7q3DkqxVf5xvn7q_X-gPqzVE9Jw" diff --git a/gravity_e2e/cluster_test_cases/binance_price_feed/hooks.py b/gravity_e2e/cluster_test_cases/binance_price_feed/hooks.py new file mode 100644 index 000000000..7b6442e44 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/binance_price_feed/hooks.py @@ -0,0 +1,186 @@ +"""Long-running helper hooks for the Binance price-feed demo suite.""" + +import json +import os +import signal +import subprocess +import sys +import time +import urllib.request +from pathlib import Path + +from gravity_e2e.utils.mock_binance_index import BUCKET_START_MS, INTERVAL_MS, MOCK_BINANCE_PORT + +DEFAULT_LIVE_BINANCE_BASE_URL = "https://fapi.binance.com" +DEFAULT_BINANCE_GRACE_MS = 120_000 +LIVE_MODE = "live" +MOCK_MODE = "mock" +NVDA_FEED_ID = 1001 +TSLA_FEED_ID = 1002 + + +def _pid_file(test_dir: Path) -> Path: + return test_dir / "artifacts" / "mock_binance.pid" + + +def _log_file(test_dir: Path) -> Path: + return test_dir / "artifacts" / "mock_binance.log" + + +def _mode(env: dict) -> str: + return env.get("BINANCE_PRICE_FEED_MODE", MOCK_MODE).strip().lower() + + +def _live_bucket_start_ms(env: dict) -> int: + configured = env.get("BINANCE_PRICE_FEED_BUCKET_START_MS") + if configured: + return int(configured) + grace_ms = int(env.get("BINANCE_PRICE_FEED_GRACE_MS", str(DEFAULT_BINANCE_GRACE_MS))) + minimum_lag = (grace_ms + INTERVAL_MS - 1) // INTERVAL_MS + 1 + lag_minutes = int(env.get("BINANCE_PRICE_FEED_LAG_MINUTES", str(minimum_lag))) + now_ms = int(time.time() * 1000) + return ((now_ms // INTERVAL_MS) - lag_minutes) * INTERVAL_MS + + +def _price_feed_uri( + feed_id: int, + pair: str, + bucket_start_ms: int, + *, + max_staleness_ms: int, + grace_ms: int, +) -> str: + return ( + f"gravity://3/{feed_id}/price_feed?" + f"provider=binance_index_kline_v1&pair={pair}&interval=1m&" + f"bucketStartMs={bucket_start_ms}&continuous=true&decimals=8&aggregationMode=2&" + f"minSourceCount=1&minTotalWeight=1&maxStaleness={max_staleness_ms}&graceMs={grace_ms}" + ) + + +def _write_live_relayer_config(test_dir: Path, env: dict): + base_url = env.get("BINANCE_PRICE_FEED_BASE_URL", DEFAULT_LIVE_BINANCE_BASE_URL) + bucket_start_ms = _live_bucket_start_ms(env) + max_staleness_ms = int(env.get("BINANCE_PRICE_FEED_MAX_STALENESS_MS", "3600000")) + grace_ms = int(env.get("BINANCE_PRICE_FEED_GRACE_MS", str(DEFAULT_BINANCE_GRACE_MS))) + env["BINANCE_PRICE_FEED_BUCKET_START_MS"] = str(bucket_start_ms) + env["BINANCE_PRICE_FEED_BASE_URL"] = base_url + env["BINANCE_PRICE_FEED_MAX_STALENESS_MS"] = str(max_staleness_ms) + env["BINANCE_PRICE_FEED_GRACE_MS"] = str(grace_ms) + + uris = [ + _price_feed_uri( + NVDA_FEED_ID, + "NVDAUSDT", + bucket_start_ms, + max_staleness_ms=max_staleness_ms, + grace_ms=grace_ms, + ), + _price_feed_uri( + TSLA_FEED_ID, + "TSLAUSDT", + bucket_start_ms, + max_staleness_ms=max_staleness_ms, + grace_ms=grace_ms, + ), + ] + relayer_config = {"uri_mappings": {uri: base_url for uri in uris}} + config_path = test_dir / "artifacts" / "relayer_config.live.json" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text(json.dumps(relayer_config, indent=2) + "\n") + env["RELAYER_CONFIG_TPL"] = str(config_path) + + +def _process_alive(pid: int) -> bool: + try: + os.kill(pid, 0) + return True + except OSError: + return False + + +def _read_pid(path: Path) -> int | None: + if not path.exists(): + return None + try: + return int(path.read_text().strip()) + except ValueError: + return None + + +def _wait_until_ready(timeout: int = 10): + start_time = BUCKET_START_MS + end_time = start_time + INTERVAL_MS - 1 + url = ( + f"http://127.0.0.1:{MOCK_BINANCE_PORT}/fapi/v1/indexPriceKlines" + f"?pair=NVDAUSDT&interval=1m&startTime={start_time}&endTime={end_time}&limit=1" + ) + deadline = time.monotonic() + timeout + last_error = None + while time.monotonic() < deadline: + try: + with urllib.request.urlopen(url, timeout=2) as resp: + json.loads(resp.read()) + return + except Exception as exc: + last_error = exc + time.sleep(0.25) + raise RuntimeError(f"mock Binance server did not become ready: {last_error}") + + +def pre_deploy(test_dir: Path, env: dict, pytest_args: list[str]): + if _mode(env) != LIVE_MODE: + return + _write_live_relayer_config(test_dir, env) + + +def pre_start(test_dir: Path, env: dict, pytest_args: list[str]): + if _mode(env) == LIVE_MODE: + return + + if env.get("GRAVITY_DEMO_KEEP_RUNNING") != "1": + return + + pid_path = _pid_file(test_dir) + pid = _read_pid(pid_path) + if pid is not None and _process_alive(pid): + env["BINANCE_PRICE_FEED_EXTERNAL_MOCK"] = "1" + _wait_until_ready() + return + + log_path = _log_file(test_dir) + log_path.parent.mkdir(parents=True, exist_ok=True) + with open(log_path, "ab") as log: + subprocess.Popen( + [ + sys.executable, + str(test_dir / "mock_binance.py"), + "--port", + str(MOCK_BINANCE_PORT), + "--pid-file", + str(pid_path), + ], + cwd=test_dir, + env=env, + stdout=log, + stderr=subprocess.STDOUT, + start_new_session=True, + ) + env["BINANCE_PRICE_FEED_EXTERNAL_MOCK"] = "1" + _wait_until_ready() + + +def post_stop(test_dir: Path, env: dict): + if _mode(env) == LIVE_MODE: + return + + pid = _read_pid(_pid_file(test_dir)) + if pid is None or not _process_alive(pid): + return + os.kill(pid, signal.SIGTERM) + deadline = time.monotonic() + 5 + while time.monotonic() < deadline: + if not _process_alive(pid): + return + time.sleep(0.25) + os.kill(pid, signal.SIGKILL) diff --git a/gravity_e2e/cluster_test_cases/binance_price_feed/mock_binance.py b/gravity_e2e/cluster_test_cases/binance_price_feed/mock_binance.py new file mode 100644 index 000000000..0be120d15 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/binance_price_feed/mock_binance.py @@ -0,0 +1,26 @@ +"""CLI wrapper for the shared deterministic Binance index-kline fixture.""" + +import argparse +import logging +from pathlib import Path +import sys + +E2E_ROOT = Path(__file__).resolve().parents[2] +if str(E2E_ROOT) not in sys.path: + sys.path.insert(0, str(E2E_ROOT)) + +from gravity_e2e.utils.mock_binance_index import MOCK_BINANCE_PORT, serve_forever + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--port", type=int, default=MOCK_BINANCE_PORT) + parser.add_argument("--pid-file", type=Path) + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") + serve_forever(args.port, args.pid_file) + + +if __name__ == "__main__": + main() diff --git a/gravity_e2e/cluster_test_cases/binance_price_feed/relayer_config.json b/gravity_e2e/cluster_test_cases/binance_price_feed/relayer_config.json new file mode 100644 index 000000000..2ed30ea51 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/binance_price_feed/relayer_config.json @@ -0,0 +1,6 @@ +{ + "uri_mappings": { + "gravity://3/1001/price_feed?provider=binance_index_kline_v1&pair=NVDAUSDT&interval=1m&bucketStartMs=1783252500000&continuous=true&decimals=8&aggregationMode=2&minSourceCount=1&minTotalWeight=1&maxStaleness=180000&graceMs=120000": "http://127.0.0.1:18547", + "gravity://3/1002/price_feed?provider=binance_index_kline_v1&pair=TSLAUSDT&interval=1m&bucketStartMs=1783252500000&continuous=true&decimals=8&aggregationMode=2&minSourceCount=1&minTotalWeight=1&maxStaleness=180000&graceMs=120000": "http://127.0.0.1:18547" + } +} diff --git a/gravity_e2e/cluster_test_cases/binance_price_feed/test_binance_price_feed.py b/gravity_e2e/cluster_test_cases/binance_price_feed/test_binance_price_feed.py new file mode 100644 index 000000000..fd07196cf --- /dev/null +++ b/gravity_e2e/cluster_test_cases/binance_price_feed/test_binance_price_feed.py @@ -0,0 +1,219 @@ +"""Binance closed index-kline price-feed E2E test.""" + +import json +import logging +import os +from pathlib import Path + +import pytest + +from gravity_e2e.cluster.manager import Cluster +from gravity_e2e.utils import oracle_test_support as support + +LOG = logging.getLogger(__name__) +SUITE_DIR = Path(__file__).resolve().parent + + +def _bucket_start_ms() -> int: + configured = os.environ.get("BINANCE_PRICE_FEED_BUCKET_START_MS") + return int(configured) if configured else support.BUCKET_START_MS + + +def _target_delivery_nonce() -> int: + return 1 if support.is_live_mode() else support.TARGET_DELIVERY_NONCE + + +def _assert_live_round_shape(round_data, round_id: int, resolved_at: int): + assert round_data[0] is True + assert round_data[1] == round_id + assert round_data[2] == resolved_at + assert round_data[3] == support.DECIMALS + assert round_data[4] == support.AGG_WEIGHTED_MEDIAN + assert round_data[5] == support.SOURCE_COUNT + assert round_data[6] == support.TOTAL_WEIGHT + assert round_data[7] > 0 + + +def _write_demo_config( + cluster: Cluster, + resolver_address: str, + chain_id: int, + binance_base_url: str, + observed_rounds: dict[int, tuple], + bucket_start_ms: int, + target_delivery_nonce: int, + target_round_id: int, + target_resolved_at: int, +): + output = os.environ.get("GRAVITY_DEMO_CONFIG_OUT") + if not output: + return + + live = support.is_live_mode() + provider = { + "kind": "live-binance" if live else "local-mock", + "name": "Binance USD-M indexPriceKlines" + if live + else "Local deterministic Binance index-kline mock", + "baseUrl": binance_base_url, + "endpoint": "/fapi/v1/indexPriceKlines", + "interval": "1m", + "live": live, + } + + def feed_config(feed_id: int, pair: str): + observed_price = int(observed_rounds[feed_id][7]) + return { + "feedId": feed_id, + "label": f"{pair[:-4]} {'Binance' if live else 'mock'} index", + "pair": pair, + "sourceType": support.SOURCE_TYPE_PRICE_FEED, + "decimals": support.DECIMALS, + "expectedDeliveryNonce": target_delivery_nonce, + "expectedRoundId": target_round_id, + "expectedResolvedAt": target_resolved_at, + "expectedPrice": str(observed_price), + "expectedDisplayPrice": support.format_price(observed_price), + } + + config = { + "version": 1, + "mode": "live-binance-index-kline" if live else "local-binance-index-kline", + "network": { + "chainId": chain_id, + "rpcUrl": cluster.get_node("node1").url, + "rpcProxyPath": "/rpc", + }, + "contracts": { + "priceFeedResolver": resolver_address, + "multiSourceOracleResolver": resolver_address, + "nativeOracle": support.NATIVE_ORACLE_ADDRESS, + "oracleTaskConfig": support.ORACLE_TASK_CONFIG_ADDRESS, + }, + "provider": provider, + "feeds": [ + feed_config(support.NVDA_FEED_ID, "NVDAUSDT"), + feed_config(support.TSLA_FEED_ID, "TSLAUSDT"), + ], + "demo": { + "providerMode": support.price_feed_mode(), + "bucketStartMs": bucket_start_ms, + "intervalMs": support.INTERVAL_MS, + "targetDeliveryNonce": target_delivery_nonce, + "targetRoundId": target_round_id, + "targetResolvedAt": target_resolved_at, + }, + } + + output_path = Path(output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(config, indent=2) + "\n") + LOG.info("Wrote price feed demo config to %s", output_path) + + +@pytest.mark.asyncio +async def test_binance_price_feed_resolves_nvda_and_tsla(cluster: Cluster): + assert await cluster.set_full_live(timeout=120), "Gravity node failed to become live" + assert await cluster.check_block_increasing(timeout=60), "Gravity chain is not producing blocks" + + node = cluster.get_node("node1") + assert node is not None, "node1 not found in cluster" + w3 = node.w3 + assert w3 is not None and w3.is_connected(), "node1 web3 not connected" + + required = [ + ("PriceFeedResolver.sol", "PriceFeedResolver"), + ("NativeOracle.sol", "NativeOracle"), + ("OracleTaskConfig.sol", "OracleTaskConfig"), + ] + contracts_out = support.ensure_contracts_out(SUITE_DIR, required, "Price feed") + resolver_artifact = support.load_artifact( + contracts_out, "PriceFeedResolver.sol", "PriceFeedResolver" + ) + native_artifact = support.load_artifact(contracts_out, "NativeOracle.sol", "NativeOracle") + task_artifact = support.load_artifact( + contracts_out, "OracleTaskConfig.sol", "OracleTaskConfig" + ) + + resolver = support.deploy_contract(w3, resolver_artifact, support.FAUCET_KEY) + native_oracle = w3.eth.contract( + address=support.NATIVE_ORACLE_ADDRESS, abi=native_artifact["abi"] + ) + task_config = w3.eth.contract( + address=support.ORACLE_TASK_CONFIG_ADDRESS, abi=task_artifact["abi"] + ) + pool_addr = support.pool0_voted_by_faucet(w3) + + bucket_start_ms = _bucket_start_ms() + target_nonce = _target_delivery_nonce() + target_round_id = support.round_id(bucket_start_ms, target_nonce) + target_resolved_at = support.round_end_ms(bucket_start_ms, target_nonce) + uris = { + support.NVDA_FEED_ID: support.price_feed_uri( + support.NVDA_FEED_ID, "NVDAUSDT", bucket_start_ms + ), + support.TSLA_FEED_ID: support.price_feed_uri( + support.TSLA_FEED_ID, "TSLAUSDT", bucket_start_ms + ), + } + + with support.server_context() as binance_base_url: + setup_data = [ + support.function_calldata( + native_oracle.functions.setDefaultCallback( + support.SOURCE_TYPE_PRICE_FEED, resolver.address + ) + ) + ] + for feed_id, uri in uris.items(): + setup_data.append( + support.function_calldata( + task_config.functions.setTask( + support.SOURCE_TYPE_PRICE_FEED, + feed_id, + support.TASK_PRICE_FEED, + uri.encode(), + ) + ) + ) + await support.execute_governance_proposal( + w3, + pool_addr, + [support.NATIVE_ORACLE_ADDRESS, support.ORACLE_TASK_CONFIG_ADDRESS, support.ORACLE_TASK_CONFIG_ADDRESS], + setup_data, + "binance-price-feed-e2e-setup", + ) + + observed_rounds = {} + expected = { + support.NVDA_FEED_ID: support.EXPECTED_NVDA_PRICE, + support.TSLA_FEED_ID: support.EXPECTED_TSLA_PRICE, + } + for feed_id, expected_price in expected.items(): + assert await support.poll_price_recorded(w3, feed_id, timeout=240) + latest_nonce = await support.wait_for_latest_nonce( + native_oracle, feed_id, target_nonce, timeout=240 + ) + assert latest_nonce >= target_nonce + stored = await support.wait_for_price_round( + resolver, feed_id, target_round_id, timeout=120 + ) + if support.is_live_mode(): + _assert_live_round_shape(stored, target_round_id, target_resolved_at) + else: + support.assert_price_round( + stored, expected_price, target_round_id, target_resolved_at + ) + observed_rounds[feed_id] = stored + + _write_demo_config( + cluster, + resolver.address, + w3.eth.chain_id, + binance_base_url, + observed_rounds, + bucket_start_ms, + target_nonce, + target_round_id, + target_resolved_at, + ) diff --git a/gravity_e2e/cluster_test_cases/oracle_demo/README.md b/gravity_e2e/cluster_test_cases/oracle_demo/README.md new file mode 100644 index 000000000..5fe9f9d0a --- /dev/null +++ b/gravity_e2e/cluster_test_cases/oracle_demo/README.md @@ -0,0 +1,92 @@ +# Combined Oracle Dashboard Demo + +This suite runs both production-candidate oracle paths in one local Gravity cluster so the frontend can +show both lanes at once: + +- `sourceType=3` Binance index-kline price feed with a deterministic local mock. +- `sourceType=6` Polymarket CTF settlement mirror with a deterministic local + Polygon JSON-RPC mock. + +No public Binance, Polygon, or Polymarket request is sent by this suite. + +## What It Proves + +1. Governance deploys/configures `PriceFeedResolver` for two price + feeds: `NVDAUSDT` and `TSLAUSDT`. +2. Governance deploys/configures `PolymarketSettlementResolver` and a + `PolymarketBinaryMarket` for a Fed-style YES/NO market. +3. The relayer fetches both mocked external sources and routes their canonical + bytes through the unsupported-JWK oracle consensus path. +4. `NativeOracle` records both `sourceType=3` and `sourceType=6` payloads. +5. The price resolver stores index rounds and the binary market settles from the + mirrored Polymarket payout vector. +6. The suite can write one `demo-config.json` that the web dashboard renders as + both a price-feed lane and a Polymarket-mirror lane. + +## Run + +From the repository root: + +```bash +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh oracle_demo --force-init +``` + +## Run As A Frontend Backend + +```bash +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh oracle_demo \ + --force-init \ + --keep-running \ + --demo-config-out ../gravity_price_feed_demo_web/public/demo-config.json \ + --log-cli-level=INFO +``` + +Then start the dashboard: + +```bash +cd ../gravity_price_feed_demo_web +npm run dev +``` + +Open the URL printed by the dev server. The dashboard uses `/rpc` to proxy to +the running Gravity devnet. + +## Stop + +```bash +bash cluster/stop.sh --config gravity_e2e/cluster_test_cases/oracle_demo/cluster.toml +kill "$(cat gravity_e2e/cluster_test_cases/oracle_demo/artifacts/mock_binance.pid)" +``` + +The mock Polygon server is owned by the e2e runner process; it exits when the +runner exits. That is fine for the dashboard because the Polymarket settlement +has already been written to `NativeOracle` before the suite passes. The Binance +mock stays alive in `--keep-running` mode so continuous price buckets can keep +advancing. + +## Expected Logs + +```text +Price feed resolved: feedId=1001 roundId=29720877 price=19612645000 +Price feed resolved: feedId=1002 roundId=29720877 price=40117545000 +Released mock binary Polymarket settlement: payout=[1, 0] +Combined oracle demo resolved: NVDA/TSLA roundId=29720877, polymarket marketId=1 YES claimable=300000000000000000000 +PASSED +``` + +## Product Mapping + +The Polymarket lane intentionally uses `PolymarketBinaryMarket` because this is +the cleanest contract shape for a Fed-rate YES/NO market or a single-match +"Team A wins?" mirror: + +```text +Polymarket CTF slot 0 -> Gravity YES +Polymarket CTF slot 1 -> Gravity NO +``` + +Do not assume this slot order for real Polymarket markets. Production mirrors +must use a reviewed manifest with frozen labels, rules, CTF token ids, and +`slotToOutcome`. diff --git a/gravity_e2e/cluster_test_cases/oracle_demo/cluster.toml b/gravity_e2e/cluster_test_cases/oracle_demo/cluster.toml new file mode 100644 index 000000000..c222f7a22 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/oracle_demo/cluster.toml @@ -0,0 +1,27 @@ +# Gravity Cluster Configuration - combined oracle dashboard demo + +[cluster] +name = "gravity-devnet-oracle-demo" +base_dir = "/tmp/gravity-cluster-oracle-demo" + +[genesis_source] +genesis_path = "./artifacts/genesis.json" +waypoint_path = "./artifacts/waypoint.txt" + +[[nodes]] +id = "node1" +role = "genesis" +source = { project_path = "../" } +host = "127.0.0.1" +validator_port = 45482 +vfn_port = 45492 +rpc_port = 48855 +api_port = 48380 +metrics_port = 49303 +inspection_port = 40202 +https_port = 41224 +authrpc_port = 48875 +reth_p2p_port = 52326 + +[faucet_init] +num_accounts = 0 diff --git a/gravity_e2e/cluster_test_cases/oracle_demo/genesis.toml b/gravity_e2e/cluster_test_cases/oracle_demo/genesis.toml new file mode 100644 index 000000000..4e40268d0 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/oracle_demo/genesis.toml @@ -0,0 +1,81 @@ +# Gravity Genesis Configuration - combined oracle dashboard demo + +[dependencies.genesis_contracts] +path = "../../../../gravity_chain_core_contracts" +ref = "codex/sports-score-oracle-poc" + +[[genesis_validators]] +id = "node1" +address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +host = "127.0.0.1" +validator_port = 45482 +vfn_port = 45492 +stake_amount = "2000000000000000000" +voting_power = "2000000000000000000" +consensus_pop = "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +[genesis] +chain_id = 1337 +# Keep the demo fast: governance-updated price feed tasks are picked up when +# the JWK consensus epoch manager rebuilds providers for the next epoch. +epoch_interval_micros = 30000000 +major_version = 1 +consensus_config = "0x0301010a00000000000000280000000000000001010000000a000000000000000100010200000000000000000020000000000000" +execution_config = "0x00" +initial_locked_until_micros = 1798848000000000 + +governance_owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + +[genesis.faucet] +address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +balance = "0x2000000000000000000000000000000000000000000000000000000000000000" + +[genesis.validator_config] +minimum_bond = "1000000000000000000" +maximum_bond = "1000000000000000000000000" +unbonding_delay_micros = 604800000000 +allow_validator_set_change = true +voting_power_increase_limit_pct = 20 +max_validator_set_size = "100" +auto_evict_enabled = false +auto_evict_threshold_pct = 0 + +[genesis.staking_config] +minimum_stake = "1000000000000000000" +lockup_duration_micros = 86400000000 +unbonding_delay_micros = 86400000000 + +[genesis.governance_config] +min_voting_threshold = "1000000000000000000" +required_proposer_stake = "1000000000000000000" +voting_duration_micros = 5000000 + +[genesis.randomness_config] +variant = 1 +secrecy_threshold = 9223372036854775808 +reconstruction_threshold = 12297829382473033728 +fast_path_secrecy_threshold = 12297829382473033728 + +[genesis.oracle_config] +source_types = [1, 3, 6] +callbacks = [ + "0x00000000000000000000000000000001625F4001", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", +] + +[[genesis.oracle_config.tasks]] +source_type = 6 +source_id = 7202626 +task_name = "polymarket_settlement" +config = "gravity://6/7202626/polymarket_settlement?ctf=0x4D97DCd97eC945f40cF65F87097ACe5EA0476045&fromBlock=89222200&condition=0xfed086f96be81a0d89ed776bedbd52d1c75bc47b49e6f0f791ddd009f52faf23&maxBlocksPerPoll=20" + +[genesis.jwk_config] +issuers = ["0x68747470733a2f2f6163636f756e74732e676f6f676c652e636f6d"] + +[[genesis.jwk_config.jwks]] +kid = "f5f4c0ae6e6090a65ab0a694d6ba6f19d5d0b4e6" +kty = "RSA" +alg = "RS256" +e = "AQAB" +n = "2K7epoJWl_aBoYGpXmDBBiEnwQ0QdVRU1gsbGXNrEbrZEQdY5KjH5P5gZMq3d3KvT1j5KsD2tF_9jFMDLqV4VWDNJRLgSNJxhJuO_oLO2BXUSL9a7fLHxnZCUfJvT2K-O8AXjT3_ZM8UuL8d4jBn_fZLzdEI4MHrZLVSaHDvvKqL_mExQo6cFD-qyLZ-T6aHv2x8R7L_3X7E1nGMjKVVZMveQ_HMeXvnGxKf5yfEP0hIQlC_kFm4L_1kV1S0UPmMptZL2qI4VnXqmqI6TZJyE-3VXHgNn1Z1O_9QZlPC0fF0spLHf2S3nNqI0v3k2E7q3DkqxVf5xvn7q_X-gPqzVE9Jw" diff --git a/gravity_e2e/cluster_test_cases/oracle_demo/hooks.py b/gravity_e2e/cluster_test_cases/oracle_demo/hooks.py new file mode 100644 index 000000000..78ce787be --- /dev/null +++ b/gravity_e2e/cluster_test_cases/oracle_demo/hooks.py @@ -0,0 +1,147 @@ +"""Hooks for the combined oracle dashboard demo suite.""" + +import json +import logging +import os +import signal +import subprocess +import sys +import time +import urllib.request +from pathlib import Path + +LOG = logging.getLogger(__name__) + +BINANCE_SUITE = Path(__file__).resolve().parent.parent / "binance_price_feed" +E2E_ROOT = Path(__file__).resolve().parents[2] + +if str(E2E_ROOT) not in sys.path: + sys.path.insert(0, str(E2E_ROOT)) + +from gravity_e2e.utils.mock_polymarket_polygon import MockPolymarketPolygon +from gravity_e2e.utils.mock_binance_index import BUCKET_START_MS, INTERVAL_MS, MOCK_BINANCE_PORT + +_poly_mock = None + + +def _pid_file(test_dir: Path) -> Path: + return test_dir / "artifacts" / "mock_binance.pid" + + +def _log_file(test_dir: Path) -> Path: + return test_dir / "artifacts" / "mock_binance.log" + + +def _process_alive(pid: int) -> bool: + try: + os.kill(pid, 0) + return True + except OSError: + return False + + +def _read_pid(path: Path) -> int | None: + if not path.exists(): + return None + try: + return int(path.read_text().strip()) + except ValueError: + return None + + +def _wait_for_binance_ready(timeout: int = 10): + start_time = BUCKET_START_MS + end_time = start_time + INTERVAL_MS - 1 + url = ( + f"http://127.0.0.1:{MOCK_BINANCE_PORT}/fapi/v1/indexPriceKlines" + f"?pair=NVDAUSDT&interval=1m&startTime={start_time}&endTime={end_time}&limit=1" + ) + deadline = time.monotonic() + timeout + last_error = None + while time.monotonic() < deadline: + try: + with urllib.request.urlopen(url, timeout=2) as resp: + json.loads(resp.read()) + return + except Exception as exc: + last_error = exc + time.sleep(0.25) + raise RuntimeError(f"mock Binance server did not become ready: {last_error}") + + +def _start_binance_mock(test_dir: Path, env: dict): + pid_path = _pid_file(test_dir) + pid = _read_pid(pid_path) + if pid is not None and _process_alive(pid): + env["BINANCE_PRICE_FEED_EXTERNAL_MOCK"] = "1" + _wait_for_binance_ready() + return + + log_path = _log_file(test_dir) + log_path.parent.mkdir(parents=True, exist_ok=True) + with open(log_path, "ab") as log: + subprocess.Popen( + [ + sys.executable, + str(BINANCE_SUITE / "mock_binance.py"), + "--port", + str(MOCK_BINANCE_PORT), + "--pid-file", + str(pid_path), + ], + cwd=test_dir, + env=env, + stdout=log, + stderr=subprocess.STDOUT, + start_new_session=True, + ) + env["BINANCE_PRICE_FEED_EXTERNAL_MOCK"] = "1" + _wait_for_binance_ready() + + +def _stop_binance_mock(test_dir: Path): + pid = _read_pid(_pid_file(test_dir)) + if pid is None or not _process_alive(pid): + return + os.kill(pid, signal.SIGTERM) + deadline = time.monotonic() + 5 + while time.monotonic() < deadline: + if not _process_alive(pid): + return + time.sleep(0.25) + os.kill(pid, signal.SIGKILL) + + +def pre_start(test_dir: Path, env: dict, pytest_args: list = None): + global _poly_mock + + LOG.info("[hook] Starting combined oracle demo mocks") + env["BINANCE_PRICE_FEED_MODE"] = "mock" + env.pop("BINANCE_PRICE_FEED_BASE_URL", None) + env.pop("BINANCE_PRICE_FEED_BUCKET_START_MS", None) + _start_binance_mock(test_dir, env) + + _poly_mock = MockPolymarketPolygon(port=8546) + _poly_mock.start() + metadata = { + "rpc_url": _poly_mock.rpc_url, + "winning_slot": None, + "payout_numerators": None, + "source_log": None, + } + (test_dir / "mock_polymarket_metadata.json").write_text(json.dumps(metadata, indent=2)) + + +def post_stop(test_dir: Path, env: dict): + global _poly_mock + + _stop_binance_mock(test_dir) + + if _poly_mock is not None: + LOG.info("[hook] Stopping MockPolymarketPolygon") + _poly_mock.stop() + _poly_mock = None + + metadata_path = test_dir / "mock_polymarket_metadata.json" + if metadata_path.exists(): + metadata_path.unlink() diff --git a/gravity_e2e/cluster_test_cases/oracle_demo/relayer_config.json b/gravity_e2e/cluster_test_cases/oracle_demo/relayer_config.json new file mode 100644 index 000000000..d5d8c48e9 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/oracle_demo/relayer_config.json @@ -0,0 +1,7 @@ +{ + "uri_mappings": { + "gravity://3/1001/price_feed?provider=binance_index_kline_v1&pair=NVDAUSDT&interval=1m&bucketStartMs=1783252500000&continuous=true&decimals=8&aggregationMode=2&minSourceCount=1&minTotalWeight=1&maxStaleness=180000&graceMs=120000": "http://127.0.0.1:18547", + "gravity://3/1002/price_feed?provider=binance_index_kline_v1&pair=TSLAUSDT&interval=1m&bucketStartMs=1783252500000&continuous=true&decimals=8&aggregationMode=2&minSourceCount=1&minTotalWeight=1&maxStaleness=180000&graceMs=120000": "http://127.0.0.1:18547", + "gravity://6/7202626/polymarket_settlement?ctf=0x4D97DCd97eC945f40cF65F87097ACe5EA0476045&fromBlock=89222200&condition=0xfed086f96be81a0d89ed776bedbd52d1c75bc47b49e6f0f791ddd009f52faf23&maxBlocksPerPoll=20": "http://localhost:8546" + } +} diff --git a/gravity_e2e/cluster_test_cases/oracle_demo/test_oracle_demo.py b/gravity_e2e/cluster_test_cases/oracle_demo/test_oracle_demo.py new file mode 100644 index 000000000..0f3ab5f59 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/oracle_demo/test_oracle_demo.py @@ -0,0 +1,452 @@ +"""Combined Binance price-feed and Polymarket mirror dashboard e2e.""" + +import asyncio +import json +import logging +import os +import time +import urllib.request +from pathlib import Path + +import pytest +from eth_abi import encode +from eth_account import Account +from web3 import Web3 + +from gravity_e2e.cluster.manager import Cluster +from gravity_e2e.utils import oracle_test_support as support +from gravity_e2e.utils.mock_polymarket_polygon import ( + CTF_ADDRESS, + FED_BINARY_BLOCK, + FED_BINARY_CONDITION_ID, + FED_BINARY_LOG_INDEX, + FED_BINARY_MARKET_ID, + FED_BINARY_QUESTION_ID, + FED_BINARY_TX_HASH, + POLYGON_CHAIN_ID, + UMA_ORACLE, +) + +LOG = logging.getLogger(__name__) + +SUITE_DIR = Path(__file__).resolve().parent + +SOURCE_TYPE_POLYMARKET_SETTLEMENT = 6 +SOURCE_TYPE_PRICE_FEED = 3 +SETTLEMENT_KIND_CTF_CONDITION_RESOLUTION = 1 +CALLBACK_GAS_LIMIT = 2_000_000 +STAKE_UNIT = 10**18 +USER_STARTING_BALANCE = 1_000 * STAKE_UNIT +NO_STAKE = 100 * STAKE_UNIT +YES_STAKE = 200 * STAKE_UNIT +TOTAL_BINARY_POOL = NO_STAKE + YES_STAKE +SPEC_HASH = Web3.keccak(text="Fed July binary Polymarket mirror demo") + +NATIVE_ORACLE_ADDRESS = support.NATIVE_ORACLE_ADDRESS +ORACLE_TASK_CONFIG_ADDRESS = support.ORACLE_TASK_CONFIG_ADDRESS +FAUCET_KEY = support.FAUCET_KEY + + +def _ensure_contracts_out() -> Path: + required = [ + ("PriceFeedResolver.sol", "PriceFeedResolver"), + ("OracleTaskConfig.sol", "OracleTaskConfig"), + ("NativeOracle.sol", "NativeOracle"), + ("PolymarketSettlementResolver.sol", "PolymarketSettlementResolver"), + ("PolymarketBinaryMarket.sol", "PolymarketBinaryMarket"), + ("MockGToken.sol", "MockGToken"), + ] + return support.ensure_contracts_out(SUITE_DIR, required, "Oracle demo") + + +def _mock_set_binary_winning_slot(rpc_url: str, winning_slot: int) -> dict: + payload = json.dumps( + { + "jsonrpc": "2.0", + "method": "mock_setBinaryWinningSlot", + "params": [winning_slot], + "id": 1, + } + ).encode() + req = urllib.request.Request( + rpc_url, + data=payload, + headers={"Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=5) as resp: + body = json.loads(resp.read()) + if "error" in body: + raise RuntimeError(f"mock_setBinaryWinningSlot failed: {body['error']}") + return body["result"] + + +async def _poll_polymarket_data_recorded(w3: Web3, timeout: int = 180): + deadline = time.monotonic() + timeout + filter_params = { + "fromBlock": 0, + "toBlock": "latest", + "address": NATIVE_ORACLE_ADDRESS, + "topics": [ + support.DATA_RECORDED_TOPIC0, + support.topic(SOURCE_TYPE_POLYMARKET_SETTLEMENT), + support.topic(FED_BINARY_MARKET_ID), + ], + } + while time.monotonic() < deadline: + logs = await asyncio.to_thread(w3.eth.get_logs, filter_params) + if logs: + return logs + await asyncio.sleep(2) + return [] + + +async def _wait_for_resolver_settlement(resolver, timeout: int = 120): + condition_id = bytes.fromhex(FED_BINARY_CONDITION_ID[2:]) + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + settlement = resolver.functions.getSettlement(FED_BINARY_MARKET_ID, condition_id).call() + if settlement[0]: + return settlement + await asyncio.sleep(2) + raise TimeoutError("binary Polymarket resolver settlement was not stored") + + +def _send_contract_tx(w3: Web3, contract, fn, sender_key: str, gas: int = 1_000_000) -> dict: + return support.send_contract_tx(w3, contract, fn, sender_key, gas=gas) + + +def _format_price(value: int) -> str: + return support.format_price(value) + + +def _write_demo_config( + cluster: Cluster, + chain_id: int, + price_resolver: str, + poly_resolver: str, + binary_market: str, + token: str, + market_id: int, + market_state: tuple, + release: dict, + observed_rounds: dict[int, tuple], + target_round_id: int, + target_resolved_at: int, + claimable_yes: int, +): + output = os.environ.get("GRAVITY_DEMO_CONFIG_OUT") + if not output: + return + + def feed_config(feed_id: int, pair: str, label: str): + observed = observed_rounds[feed_id] + observed_price = int(observed[7]) + return { + "feedId": feed_id, + "label": label, + "pair": pair, + "sourceType": SOURCE_TYPE_PRICE_FEED, + "decimals": support.DECIMALS, + "expectedDeliveryNonce": support.TARGET_DELIVERY_NONCE, + "expectedRoundId": target_round_id, + "expectedResolvedAt": target_resolved_at, + "expectedPrice": str(observed_price), + "expectedDisplayPrice": _format_price(observed_price), + } + + demo_config = { + "version": 2, + "mode": "local-combined-oracle-demo", + "network": { + "chainId": chain_id, + "rpcUrl": cluster.get_node("node1").url, + "rpcProxyPath": "/rpc", + }, + "contracts": { + "priceFeedResolver": price_resolver, + "multiSourceOracleResolver": price_resolver, + "polymarketSettlementResolver": poly_resolver, + "polymarketBinaryMarket": binary_market, + "collateral": token, + "nativeOracle": NATIVE_ORACLE_ADDRESS, + "oracleTaskConfig": ORACLE_TASK_CONFIG_ADDRESS, + }, + "provider": { + "kind": "local-mock", + "name": "Local deterministic Binance index-kline mock", + "baseUrl": f"http://127.0.0.1:{support.MOCK_BINANCE_PORT}", + "endpoint": "/fapi/v1/indexPriceKlines", + "interval": "1m", + "live": False, + }, + "feeds": [ + feed_config(support.NVDA_FEED_ID, "NVDAUSDT", "NVDA mock index"), + feed_config(support.TSLA_FEED_ID, "TSLAUSDT", "TSLA mock index"), + ], + "polymarket": { + "kind": "binary", + "title": "Will the Fed cut rates in July?", + "subtitle": "Local Polymarket CTF mirror fixture", + "sourceType": SOURCE_TYPE_POLYMARKET_SETTLEMENT, + "mirrorId": FED_BINARY_MARKET_ID, + "marketId": market_id, + "conditionId": FED_BINARY_CONDITION_ID, + "questionId": FED_BINARY_QUESTION_ID, + "ctf": CTF_ADDRESS, + "polygonChainId": POLYGON_CHAIN_ID, + "outcomeLabels": ["NO", "YES"], + "slotLabels": ["YES", "NO"], + "slotToOutcome": [1, 0], + "winningSlot": release["winning_slot"], + "winningOutcome": int(market_state[6]), + "payoutNumerators": release["payout_numerators"], + "settlementTxHash": FED_BINARY_TX_HASH, + "settlementBlock": FED_BINARY_BLOCK, + "settlementLogIndex": FED_BINARY_LOG_INDEX, + "totalPool": str(market_state[7]), + "yesAccount": { + "address": release["yesAccount"], + "claimable": str(claimable_yes), + }, + }, + "demo": { + "providerMode": "mock", + "bucketStartMs": support.BUCKET_START_MS, + "intervalMs": support.INTERVAL_MS, + "targetDeliveryNonce": support.TARGET_DELIVERY_NONCE, + "targetRoundId": target_round_id, + "targetResolvedAt": target_resolved_at, + }, + } + + output_path = Path(output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(demo_config, indent=2) + "\n") + LOG.info("Wrote combined oracle demo config to %s", output_path) + + +@pytest.mark.asyncio +async def test_combined_oracle_demo_resolves_price_and_polymarket(cluster: Cluster): + os.environ["BINANCE_PRICE_FEED_MODE"] = "mock" + os.environ.pop("BINANCE_PRICE_FEED_BASE_URL", None) + os.environ.pop("BINANCE_PRICE_FEED_BUCKET_START_MS", None) + + metadata_path = SUITE_DIR / "mock_polymarket_metadata.json" + metadata = json.loads(metadata_path.read_text()) + + LOG.info("Verifying gravity node is live") + assert await cluster.set_full_live(timeout=120), "Gravity node failed to become live" + assert await cluster.check_block_increasing(timeout=60), "Gravity chain is not producing blocks" + + node = cluster.get_node("node1") + assert node is not None, "node1 not found in cluster" + w3 = node.w3 + assert w3 is not None and w3.is_connected(), "node1 web3 not connected" + + contracts_out = _ensure_contracts_out() + price_resolver_artifact = support.load_artifact( + contracts_out, + "PriceFeedResolver.sol", + "PriceFeedResolver", + ) + poly_resolver_artifact = support.load_artifact( + contracts_out, + "PolymarketSettlementResolver.sol", + "PolymarketSettlementResolver", + ) + binary_market_artifact = support.load_artifact( + contracts_out, + "PolymarketBinaryMarket.sol", + "PolymarketBinaryMarket", + ) + token_artifact = support.load_artifact(contracts_out, "MockGToken.sol", "MockGToken") + native_artifact = support.load_artifact(contracts_out, "NativeOracle.sol", "NativeOracle") + task_artifact = support.load_artifact(contracts_out, "OracleTaskConfig.sol", "OracleTaskConfig") + + price_resolver = support.deploy_contract(w3, price_resolver_artifact, FAUCET_KEY) + poly_resolver = support.deploy_contract(w3, poly_resolver_artifact, FAUCET_KEY) + binary_market = support.deploy_contract(w3, binary_market_artifact, FAUCET_KEY) + token = support.deploy_contract(w3, token_artifact, FAUCET_KEY) + native_oracle = w3.eth.contract(address=NATIVE_ORACLE_ADDRESS, abi=native_artifact["abi"]) + task_config = w3.eth.contract(address=ORACLE_TASK_CONFIG_ADDRESS, abi=task_artifact["abi"]) + + pool_addr = support.pool0_voted_by_faucet(w3) + condition_id = bytes.fromhex(FED_BINARY_CONDITION_ID[2:]) + ctf = Web3.to_checksum_address(CTF_ADDRESS) + + now_ts = w3.eth.get_block("latest")["timestamp"] + closes_at = now_ts + 20 + oracle_deadline = now_ts + 180 + settlement_ref = ( + SOURCE_TYPE_POLYMARKET_SETTLEMENT, + FED_BINARY_MARKET_ID, + condition_id, + poly_resolver.address, + ctf, + POLYGON_CHAIN_ID, + 2, + [1, 0], + 0, + ) + create_params = ( + SPEC_HASH, + now_ts, + closes_at, + oracle_deadline, + token.address, + settlement_ref, + ) + + nvda_uri = support.price_feed_uri(support.NVDA_FEED_ID, "NVDAUSDT", support.BUCKET_START_MS) + tsla_uri = support.price_feed_uri(support.TSLA_FEED_ID, "TSLAUSDT", support.BUCKET_START_MS) + + setup_datas = [ + support.function_calldata( + native_oracle.functions.setDefaultCallback( + SOURCE_TYPE_PRICE_FEED, + price_resolver.address, + ) + ), + support.function_calldata( + task_config.functions.setTask( + SOURCE_TYPE_PRICE_FEED, + support.NVDA_FEED_ID, + support.TASK_PRICE_FEED, + nvda_uri.encode(), + ) + ), + support.function_calldata( + task_config.functions.setTask( + SOURCE_TYPE_PRICE_FEED, + support.TSLA_FEED_ID, + support.TASK_PRICE_FEED, + tsla_uri.encode(), + ) + ), + support.function_calldata( + native_oracle.functions.setCallback( + SOURCE_TYPE_POLYMARKET_SETTLEMENT, + FED_BINARY_MARKET_ID, + poly_resolver.address, + ) + ), + support.function_calldata( + poly_resolver.functions.registerMirror( + FED_BINARY_MARKET_ID, + POLYGON_CHAIN_ID, + ctf, + condition_id, + 2, + ) + ), + support.function_calldata(binary_market.functions.createMarket(create_params)), + ] + receipt = await support.execute_governance_proposal( + w3, + pool_addr, + [ + NATIVE_ORACLE_ADDRESS, + ORACLE_TASK_CONFIG_ADDRESS, + ORACLE_TASK_CONFIG_ADDRESS, + NATIVE_ORACLE_ADDRESS, + poly_resolver.address, + binary_market.address, + ], + setup_datas, + "combined-oracle-demo-setup", + gas=8_000_000, + ) + market_id = support.market_id_from_receipt(receipt) + + no_account = Account.create("oracle-demo-no") + yes_account = Account.create("oracle-demo-yes") + for account in [no_account, yes_account]: + support.send_tx(w3, account.address, b"", FAUCET_KEY, gas=21_000, value=STAKE_UNIT) + _send_contract_tx(w3, token, token.functions.mint(account.address, USER_STARTING_BALANCE), FAUCET_KEY) + _send_contract_tx(w3, token, token.functions.approve(binary_market.address, USER_STARTING_BALANCE), account.key) + + _send_contract_tx( + w3, + binary_market, + binary_market.functions.placeBet(market_id, 0, NO_STAKE), + no_account.key, + gas=1_500_000, + ) + _send_contract_tx( + w3, + binary_market, + binary_market.functions.placeBet(market_id, 1, YES_STAKE), + yes_account.key, + gas=1_500_000, + ) + assert binary_market.functions.getMarket(market_id).call()[7] == TOTAL_BINARY_POOL + + observed_rounds = {} + target_round_id = support.round_id(support.BUCKET_START_MS, support.TARGET_DELIVERY_NONCE) + target_resolved_at = support.round_end_ms(support.BUCKET_START_MS, support.TARGET_DELIVERY_NONCE) + for feed_id, expected_price in [ + (support.NVDA_FEED_ID, support.EXPECTED_NVDA_PRICE), + (support.TSLA_FEED_ID, support.EXPECTED_TSLA_PRICE), + ]: + logs = await support.poll_price_recorded(w3, feed_id, timeout=240) + assert logs, f"No sourceType=3 DataRecorded event observed for feedId={feed_id}" + latest_nonce = await support.wait_for_latest_nonce( + native_oracle, + feed_id, + support.TARGET_DELIVERY_NONCE, + timeout=240, + ) + assert latest_nonce >= support.TARGET_DELIVERY_NONCE + stored = await support.wait_for_price_round(price_resolver, feed_id, target_round_id, timeout=120) + support.assert_price_round(stored, expected_price, target_round_id, target_resolved_at) + observed_rounds[feed_id] = stored + LOG.info("Price feed resolved: feedId=%s roundId=%s price=%s", feed_id, target_round_id, int(stored[7])) + + await support.wait_for_chain_time(w3, closes_at) + _send_contract_tx(w3, binary_market, binary_market.functions.lockMarket(market_id), FAUCET_KEY) + + release = _mock_set_binary_winning_slot(metadata["rpc_url"], 0) + release["yesAccount"] = yes_account.address + metadata.update(release) + metadata_path.write_text(json.dumps(metadata, indent=2)) + LOG.info("Released mock binary Polymarket settlement: payout=%s", release["payout_numerators"]) + + logs = await _poll_polymarket_data_recorded(w3, timeout=180) + assert logs, "No Polymarket sourceType=6 DataRecorded event observed" + settlement = await _wait_for_resolver_settlement(poly_resolver, timeout=60) + assert settlement[0] is True + assert settlement[2] == POLYGON_CHAIN_ID + assert Web3.to_checksum_address(settlement[3]) == ctf + assert settlement[6] == 2 + assert settlement[9] == SETTLEMENT_KIND_CTF_CONDITION_RESOLUTION + assert poly_resolver.functions.getPayoutNumerators(FED_BINARY_MARKET_ID, condition_id).call() == [1, 0] + + _send_contract_tx(w3, binary_market, binary_market.functions.settleMarket(market_id), FAUCET_KEY, gas=2_000_000) + market_state = binary_market.functions.getMarket(market_id).call() + assert market_state[5] == 2, f"binary market did not settle: status={market_state[5]}" + assert market_state[6] == 1, f"winning outcome should be YES, got {market_state[6]}" + claimable_yes = binary_market.functions.claimable(market_id, yes_account.address).call() + assert claimable_yes == TOTAL_BINARY_POOL + + _write_demo_config( + cluster, + w3.eth.chain_id, + price_resolver.address, + poly_resolver.address, + binary_market.address, + token.address, + market_id, + market_state, + release, + observed_rounds, + target_round_id, + target_resolved_at, + claimable_yes, + ) + + LOG.info( + "Combined oracle demo resolved: NVDA/TSLA roundId=%s, polymarket marketId=%s YES claimable=%s", + target_round_id, + market_id, + claimable_yes, + ) diff --git a/gravity_e2e/cluster_test_cases/polymarket_mock/README.md b/gravity_e2e/cluster_test_cases/polymarket_mock/README.md new file mode 100644 index 000000000..5a5245c93 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/polymarket_mock/README.md @@ -0,0 +1,112 @@ +# Polymarket Mock Oracle E2E + +This suite exercises the current Polymarket-like oracle path end to end without +calling Polygon or any public API. It uses a local Polygon JSON-RPC mock, lets +the Gravity relayer read a CTF `ConditionResolution` log, sends the agreed bytes +through the existing unsupported-JWK/oracle consensus path, and settles a +Gravity match market contract from the resulting oracle payload. + +## What It Proves + +- A Gravity oracle task can point at a Polygon-like source URI and be remapped to + a local source in E2E via `relayer_config.json`. +- The source payload can travel through the relayer and unsupported-JWK consensus + path into `NativeOracle`. +- Contract-side resolver logic can parse the agreed bytes, store the final + Polymarket CTF payout vector, and expose it to a market contract. +- A match-market style contract can create a market, accept bets, lock, wait for + oracle resolution, settle the winning outcome, and pay the winner. + +## Local Topology + +```mermaid +flowchart LR + A["MockPolymarketPolygon
localhost:8546"] --> B["gravity-reth
Polymarket settlement source"] + B --> C["gravity-sdk relayer
URI mapping"] + C --> D["unsupported-JWK consensus
oracle bytes quorum"] + D --> E["NativeOracle
DataRecorded sourceType=6"] + E --> F["PolymarketSettlementResolver"] + F --> G["PolymarketMatchMarket
settle + claim"] +``` + +## How To Run + +From the repository root, build the quick-release binaries first: + +```bash +RUSTFLAGS="--cfg tokio_unstable" cargo build -p gravity_node --profile quick-release +RUSTFLAGS="--cfg tokio_unstable" cargo build -p gravity_cli --profile quick-release +``` + +Then run the suite with a Python environment that has the E2E dependencies +installed. A virtualenv is fine; a conda environment is also fine as long as its +`python3` is first in `PATH`. + +```bash +PATH="$CONDA_PREFIX/bin:$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh polymarket_mock --force-init +``` + +If you are using a virtualenv instead of conda: + +```bash +source gravity_e2e/.venv/bin/activate +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh polymarket_mock --force-init +``` + +Use `--force-init` when changing the suite config or the contract branch, because +the genesis artifacts are cached under this suite. + +## Expected Result + +A successful run should include logs like: + +```text +MockPolymarketPolygon: prepared winning_slot= payout= visible=True +Released mock Polymarket settlement: winning_slot= payout= +Polymarket match market resolved and claimed: marketId=1 winningSlot= totalPool=600000000000000000000 +PASSED +Suite polymarket_mock PASSED +All suites passed! +``` + +The winning slot is random by default. To make the test deterministic while +debugging, set `POLYMARKET_MOCK_WINNING_SLOT` to `0`, `1`, or `2`. + +## How This Maps To A Polymarket-Like Gravity Product + +The current implementation keeps dynamic market discovery out of scope and focuses on the +settlement rail: + +1. A market is created on Gravity with a Polymarket CTF reference + (`conditionId`, CTF address, Polygon chain id, outcome count, and accepted + outcome slots). +2. The Gravity oracle task watches a Polygon-like CTF `ConditionResolution` log. + In this E2E test, `relayer_config.json` maps that URI to the local mock. +3. Validators agree on the canonical settlement bytes through the existing + unsupported-JWK/oracle consensus path. +4. `NativeOracle` emits/stores the agreed payload for `sourceType=6`. +5. `PolymarketSettlementResolver` validates the payload against the market's + expected CTF metadata and stores the payout vector. +6. `PolymarketMatchMarket` uses the resolver result to settle the market and + release funds. + +For a production Polymarket-like flow, the same settlement rail can be reused +while adding a request-discovery layer later: + +- Static task: configure known CTF condition ids in genesis or governance. +- Direct binary mirror: use `PolymarketBinaryMarket` for one reviewed + two-outcome Polymarket condition such as a Fed-rate YES/NO market or a + single-match "Team A wins?" market. +- Direct 3-way mirror: use `PolymarketMatchMarket` for one reviewed + three-outcome CTF condition. +- Near-term dynamic flow: create Gravity markets with explicit Polygon CTF + references, and let a deterministic watcher discover finalized request events. +- Longer-term product flow: add provider allowlists, finality gates, request + deadlines, and typed `Pending` / `Unknown` / `Expired` settlement states before + validators sign any payload. + +This keeps high-risk external fetch logic outside consensus-critical code until +the request watcher design is ready, while still proving that Gravity can consume +Polygon/Polymarket settlement data once a canonical source payload is available. diff --git a/gravity_e2e/cluster_test_cases/polymarket_mock/cluster.toml b/gravity_e2e/cluster_test_cases/polymarket_mock/cluster.toml new file mode 100644 index 000000000..1f7579476 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/polymarket_mock/cluster.toml @@ -0,0 +1,27 @@ +# Gravity Cluster Configuration - Polymarket mirror integration test + +[cluster] +name = "gravity-devnet-polymarket-mock" +base_dir = "/tmp/gravity-cluster-polymarket-mock" + +[genesis_source] +genesis_path = "./artifacts/genesis.json" +waypoint_path = "./artifacts/waypoint.txt" + +[[nodes]] +id = "node1" +role = "genesis" +source = { project_path = "../" } +host = "127.0.0.1" +validator_port = 45282 +vfn_port = 45292 +rpc_port = 48655 +api_port = 48180 +metrics_port = 49103 +inspection_port = 40002 +https_port = 41024 +authrpc_port = 48675 +reth_p2p_port = 52126 + +[faucet_init] +num_accounts = 0 diff --git a/gravity_e2e/cluster_test_cases/polymarket_mock/genesis.toml b/gravity_e2e/cluster_test_cases/polymarket_mock/genesis.toml new file mode 100644 index 000000000..c9c57bc0a --- /dev/null +++ b/gravity_e2e/cluster_test_cases/polymarket_mock/genesis.toml @@ -0,0 +1,78 @@ +# Gravity Genesis Configuration - Polymarket mirror integration test + +[dependencies.genesis_contracts] +path = "../../../../gravity_chain_core_contracts" +ref = "codex/sports-score-oracle-poc" + +[[genesis_validators]] +id = "node1" +address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +host = "127.0.0.1" +validator_port = 45282 +vfn_port = 45292 +stake_amount = "2000000000000000000" +voting_power = "2000000000000000000" +consensus_pop = "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +[genesis] +chain_id = 1337 +epoch_interval_micros = 7200000000 +major_version = 1 +consensus_config = "0x0301010a00000000000000280000000000000001010000000a000000000000000100010200000000000000000020000000000000" +execution_config = "0x00" +initial_locked_until_micros = 1798848000000000 + +governance_owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + +[genesis.faucet] +address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +balance = "0x2000000000000000000000000000000000000000000000000000000000000000" + +[genesis.validator_config] +minimum_bond = "1000000000000000000" +maximum_bond = "1000000000000000000000000" +unbonding_delay_micros = 604800000000 +allow_validator_set_change = true +voting_power_increase_limit_pct = 20 +max_validator_set_size = "100" +auto_evict_enabled = false +auto_evict_threshold_pct = 0 + +[genesis.staking_config] +minimum_stake = "1000000000000000000" +lockup_duration_micros = 86400000000 +unbonding_delay_micros = 86400000000 + +[genesis.governance_config] +min_voting_threshold = "1000000000000000000" +required_proposer_stake = "1000000000000000000" +voting_duration_micros = 5000000 + +[genesis.randomness_config] +variant = 1 +secrecy_threshold = 9223372036854775808 +reconstruction_threshold = 12297829382473033728 +fast_path_secrecy_threshold = 12297829382473033728 + +[genesis.oracle_config] +source_types = [1, 6] +callbacks = [ + "0x00000000000000000000000000000001625F4001", + "0x0000000000000000000000000000000000000000", +] + +[[genesis.oracle_config.tasks]] +source_type = 6 +source_id = 1897398 +task_name = "polymarket_settlement" +config = "gravity://6/1897398/polymarket_settlement?ctf=0x4D97DCd97eC945f40cF65F87097ACe5EA0476045&fromBlock=89222200&condition=0x2afe86f96be81a0d89ed776bedbd52d1c75bc47b49e6f0f791ddd009f52faf23&maxBlocksPerPoll=20" + +[genesis.jwk_config] +issuers = ["0x68747470733a2f2f6163636f756e74732e676f6f676c652e636f6d"] + +[[genesis.jwk_config.jwks]] +kid = "f5f4c0ae6e6090a65ab0a694d6ba6f19d5d0b4e6" +kty = "RSA" +alg = "RS256" +e = "AQAB" +n = "2K7epoJWl_aBoYGpXmDBBiEnwQ0QdVRU1gsbGXNrEbrZEQdY5KjH5P5gZMq3d3KvT1j5KsD2tF_9jFMDLqV4VWDNJRLgSNJxhJuO_oLO2BXUSL9a7fLHxnZCUfJvT2K-O8AXjT3_ZM8UuL8d4jBn_fZLzdEI4MHrZLVSaHDvvKqL_mExQo6cFD-qyLZ-T6aHv2x8R7L_3X7E1nGMjKVVZMveQ_HMeXvnGxKf5yfEP0hIQlC_kFm4L_1kV1S0UPmMptZL2qI4VnXqmqI6TZJyE-3VXHgNn1Z1O_9QZlPC0fF0spLHf2S3nNqI0v3k2E7q3DkqxVf5xvn7q_X-gPqzVE9Jw" diff --git a/gravity_e2e/cluster_test_cases/polymarket_mock/hooks.py b/gravity_e2e/cluster_test_cases/polymarket_mock/hooks.py new file mode 100644 index 000000000..356a614f4 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/polymarket_mock/hooks.py @@ -0,0 +1,65 @@ +"""Hooks for the Polymarket mirror integration suite.""" + +import json +import logging +import sys +from pathlib import Path + +LOG = logging.getLogger(__name__) + +_mock = None +_METADATA_FILE = "mock_polymarket_metadata.json" + + +def pre_start(test_dir: Path, env: dict, pytest_args: list = None): + global _mock + + e2e_root = str(Path(__file__).resolve().parent.parent.parent) + if e2e_root not in sys.path: + sys.path.insert(0, e2e_root) + + from gravity_e2e.utils.mock_polymarket_polygon import ( + CTF_ADDRESS, + MATCH_BLOCK, + MATCH_CONDITION_ID, + MATCH_LOG_INDEX, + MATCH_MARKET_ID, + MATCH_QUESTION_ID, + MATCH_TX_HASH, + MockPolymarketPolygon, + ) + + LOG.info("[hook] Starting MockPolymarketPolygon on port 8546 with hidden settlement") + _mock = MockPolymarketPolygon(port=8546) + _mock.start() + + metadata = { + "port": 8546, + "rpc_url": _mock.rpc_url, + "ctf": CTF_ADDRESS, + "market_id": MATCH_MARKET_ID, + "condition_id": MATCH_CONDITION_ID, + "question_id": MATCH_QUESTION_ID, + "tx_hash": MATCH_TX_HASH, + "block": MATCH_BLOCK, + "log_index": MATCH_LOG_INDEX, + "winning_slot": None, + "payout_numerators": None, + "source_log": None, + } + metadata_path = test_dir / _METADATA_FILE + metadata_path.write_text(json.dumps(metadata, indent=2)) + LOG.info("[hook] Wrote Polymarket mock metadata to %s", metadata_path) + + +def post_stop(test_dir: Path, env: dict): + global _mock + + if _mock is not None: + LOG.info("[hook] Stopping MockPolymarketPolygon") + _mock.stop() + _mock = None + + metadata_path = test_dir / _METADATA_FILE + if metadata_path.exists(): + metadata_path.unlink() diff --git a/gravity_e2e/cluster_test_cases/polymarket_mock/relayer_config.json b/gravity_e2e/cluster_test_cases/polymarket_mock/relayer_config.json new file mode 100644 index 000000000..6d7e9ac79 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/polymarket_mock/relayer_config.json @@ -0,0 +1,5 @@ +{ + "uri_mappings": { + "gravity://6/1897398/polymarket_settlement?ctf=0x4D97DCd97eC945f40cF65F87097ACe5EA0476045&fromBlock=89222200&condition=0x2afe86f96be81a0d89ed776bedbd52d1c75bc47b49e6f0f791ddd009f52faf23&maxBlocksPerPoll=20": "http://localhost:8546" + } +} diff --git a/gravity_e2e/cluster_test_cases/polymarket_mock/test_polymarket_mock.py b/gravity_e2e/cluster_test_cases/polymarket_mock/test_polymarket_mock.py new file mode 100644 index 000000000..7e5a54794 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/polymarket_mock/test_polymarket_mock.py @@ -0,0 +1,255 @@ +"""Polymarket three-way match mirror E2E test.""" + +import asyncio +import json +import logging +import os +import secrets +import time +import urllib.request +from pathlib import Path + +import pytest +from eth_account import Account +from web3 import Web3 + +from gravity_e2e.cluster.manager import Cluster +from gravity_e2e.utils import oracle_test_support as support +from gravity_e2e.utils.mock_polymarket_polygon import MATCH_MARKET_ID + +LOG = logging.getLogger(__name__) +SUITE_DIR = Path(__file__).resolve().parent + +SOURCE_TYPE_POLYMARKET_SETTLEMENT = 6 +POLYGON_CHAIN_ID = 137 +MATCH_OUTCOME_COUNT = 3 +SETTLEMENT_KIND_CTF_CONDITION_RESOLUTION = 1 +USER_STARTING_BALANCE = 1_000 * support.STAKE_UNIT +BET_AMOUNTS = [100 * support.STAKE_UNIT, 200 * support.STAKE_UNIT, 300 * support.STAKE_UNIT] +TOTAL_POOL = sum(BET_AMOUNTS) + + +def _mock_set_winning_slot(rpc_url: str, winning_slot: int) -> dict: + payload = json.dumps( + { + "jsonrpc": "2.0", + "method": "mock_setWinningSlot", + "params": [winning_slot], + "id": 1, + } + ).encode() + request = urllib.request.Request( + rpc_url, data=payload, headers={"Content-Type": "application/json"} + ) + with urllib.request.urlopen(request, timeout=5) as response: + body = json.loads(response.read()) + if "error" in body: + raise RuntimeError(f"mock_setWinningSlot failed: {body['error']}") + return body["result"] + + +async def _poll_data_recorded(w3: Web3, timeout: int = 120): + deadline = time.monotonic() + timeout + params = { + "fromBlock": 0, + "toBlock": "latest", + "address": support.NATIVE_ORACLE_ADDRESS, + "topics": [ + support.DATA_RECORDED_TOPIC0, + support.topic(SOURCE_TYPE_POLYMARKET_SETTLEMENT), + support.topic(MATCH_MARKET_ID), + ], + } + while time.monotonic() < deadline: + logs = await asyncio.to_thread(w3.eth.get_logs, params) + if logs: + return logs + await asyncio.sleep(2) + return [] + + +async def _wait_for_resolver_settlement( + resolver, mirror_id: int, condition_id: bytes, timeout: int = 120 +): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + settlement = resolver.functions.getSettlement(mirror_id, condition_id).call() + if settlement[0]: + return settlement + await asyncio.sleep(2) + raise TimeoutError("resolver settlement was not stored") + + +@pytest.mark.asyncio +async def test_polymarket_match_market_mock_resolves_random_score(cluster: Cluster): + metadata_path = SUITE_DIR / "mock_polymarket_metadata.json" + metadata = json.loads(metadata_path.read_text()) + + assert await cluster.set_full_live(timeout=120), "Gravity node failed to become live" + assert await cluster.check_block_increasing(timeout=60), "Gravity chain is not producing blocks" + + node = cluster.get_node("node1") + assert node is not None, "node1 not found in cluster" + w3 = node.w3 + assert w3 is not None and w3.is_connected(), "node1 web3 not connected" + + required = [ + ("PolymarketSettlementResolver.sol", "PolymarketSettlementResolver"), + ("PolymarketMatchMarket.sol", "PolymarketMatchMarket"), + ("MockGToken.sol", "MockGToken"), + ("NativeOracle.sol", "NativeOracle"), + ] + contracts_out = support.ensure_contracts_out(SUITE_DIR, required, "Polymarket") + resolver_artifact = support.load_artifact( + contracts_out, "PolymarketSettlementResolver.sol", "PolymarketSettlementResolver" + ) + market_artifact = support.load_artifact( + contracts_out, "PolymarketMatchMarket.sol", "PolymarketMatchMarket" + ) + token_artifact = support.load_artifact(contracts_out, "MockGToken.sol", "MockGToken") + native_artifact = support.load_artifact(contracts_out, "NativeOracle.sol", "NativeOracle") + + resolver = support.deploy_contract(w3, resolver_artifact, support.FAUCET_KEY) + market = support.deploy_contract(w3, market_artifact, support.FAUCET_KEY) + token = support.deploy_contract(w3, token_artifact, support.FAUCET_KEY) + native_oracle = w3.eth.contract( + address=support.NATIVE_ORACLE_ADDRESS, abi=native_artifact["abi"] + ) + + condition_id = bytes.fromhex(metadata["condition_id"][2:]) + ctf = Web3.to_checksum_address(metadata["ctf"]) + pool_addr = support.pool0_voted_by_faucet(w3) + + now_ts = w3.eth.get_block("latest")["timestamp"] + closes_at = now_ts + 120 + settlement_ref = ( + SOURCE_TYPE_POLYMARKET_SETTLEMENT, + MATCH_MARKET_ID, + condition_id, + resolver.address, + ctf, + POLYGON_CHAIN_ID, + MATCH_OUTCOME_COUNT, + [0, 1, 2], + 0, + ) + create_params = ( + Web3.keccak(text="Portugal vs Colombia random e2e match market"), + now_ts, + closes_at, + now_ts + 300, + token.address, + settlement_ref, + ) + + setup_data = [ + support.function_calldata( + native_oracle.functions.setCallback( + SOURCE_TYPE_POLYMARKET_SETTLEMENT, MATCH_MARKET_ID, resolver.address + ) + ), + support.function_calldata( + resolver.functions.registerMirror( + MATCH_MARKET_ID, + POLYGON_CHAIN_ID, + ctf, + condition_id, + MATCH_OUTCOME_COUNT, + ) + ), + support.function_calldata(market.functions.createMarket(create_params)), + ] + receipt = await support.execute_governance_proposal( + w3, + pool_addr, + [support.NATIVE_ORACLE_ADDRESS, resolver.address, market.address], + setup_data, + "polymarket-match-market-e2e-setup", + ) + market_id = support.market_id_from_receipt(receipt) + + bettors = [Account.create(f"polymarket-bettor-{i}") for i in range(MATCH_OUTCOME_COUNT)] + for account in bettors: + support.send_tx( + w3, account.address, b"", support.FAUCET_KEY, gas=21_000, value=support.STAKE_UNIT + ) + support.send_contract_tx( + w3, + token, + token.functions.mint(account.address, USER_STARTING_BALANCE), + support.FAUCET_KEY, + ) + support.send_contract_tx( + w3, + token, + token.functions.approve(market.address, USER_STARTING_BALANCE), + account.key, + ) + + for outcome, (account, amount) in enumerate(zip(bettors, BET_AMOUNTS)): + support.send_contract_tx( + w3, + market, + market.functions.placeBet(market_id, outcome, amount), + account.key, + gas=1_500_000, + ) + assert market.functions.getMarket(market_id).call()[8] == TOTAL_POOL + + await support.wait_for_chain_time(w3, closes_at) + support.send_contract_tx( + w3, market, market.functions.lockMarket(market_id), support.FAUCET_KEY + ) + + forced_slot = os.environ.get("POLYMARKET_MOCK_WINNING_SLOT") + winning_slot = int(forced_slot) if forced_slot is not None else secrets.randbelow(MATCH_OUTCOME_COUNT) + assert 0 <= winning_slot < MATCH_OUTCOME_COUNT + release = _mock_set_winning_slot(metadata["rpc_url"], winning_slot) + metadata.update(release) + metadata_path.write_text(json.dumps(metadata, indent=2)) + + logs = await _poll_data_recorded(w3, timeout=180) + assert logs, "No Polymarket sourceType=6 DataRecorded event observed" + assert support.topic_hex(logs[0]["topics"][1]) == support.topic( + SOURCE_TYPE_POLYMARKET_SETTLEMENT + ) + assert support.topic_hex(logs[0]["topics"][2]) == support.topic(MATCH_MARKET_ID) + + settlement = await _wait_for_resolver_settlement( + resolver, MATCH_MARKET_ID, condition_id, timeout=60 + ) + assert settlement[0] is True + assert settlement[2] == POLYGON_CHAIN_ID + assert Web3.to_checksum_address(settlement[3]) == ctf + assert settlement[6] == MATCH_OUTCOME_COUNT + assert settlement[9] == SETTLEMENT_KIND_CTF_CONDITION_RESOLUTION + assert resolver.functions.getPayoutNumerators(MATCH_MARKET_ID, condition_id).call() == release[ + "payout_numerators" + ] + + support.send_contract_tx( + w3, market, market.functions.settleMarket(market_id), support.FAUCET_KEY, gas=2_000_000 + ) + final_market = market.functions.getMarket(market_id).call() + assert final_market[5] == 2 + assert final_market[7] == winning_slot + + winner = bettors[winning_slot] + assert market.functions.claimable(market_id, winner.address).call() == TOTAL_POOL + balance_before = token.functions.balanceOf(winner.address).call() + support.send_contract_tx( + w3, market, market.functions.claim(market_id), winner.key, gas=1_500_000 + ) + assert token.functions.balanceOf(winner.address).call() - balance_before == TOTAL_POOL + assert market.functions.claimable(market_id, winner.address).call() == 0 + + for outcome, account in enumerate(bettors): + if outcome != winning_slot: + assert market.functions.claimable(market_id, account.address).call() == 0 + + LOG.info( + "Polymarket match market resolved: marketId=%s winningSlot=%s totalPool=%s", + market_id, + winning_slot, + TOTAL_POOL, + ) diff --git a/gravity_e2e/docs/MANUAL.md b/gravity_e2e/docs/MANUAL.md index 8971df391..ec4beffff 100644 --- a/gravity_e2e/docs/MANUAL.md +++ b/gravity_e2e/docs/MANUAL.md @@ -79,7 +79,150 @@ You can control the Python logging level by passing standard pytest flags to the ./gravity_e2e/run_test.sh --log-cli-level=DEBUG ``` -### 5. Docker Runner (CI) +### 5. Polymarket Mock Oracle Suite +The `polymarket_mock` suite is a local-only integration test for a Polymarket-like settlement +flow on Gravity. It starts a local Polygon JSON-RPC mock, maps the oracle task +URI to that mock, waits for the relayer and unsupported-JWK/oracle consensus path +to publish `sourceType=6` bytes into `NativeOracle`, and then settles a +match-market contract from the resolver result. + +Run it from the repository root after building `gravity_node` and `gravity_cli`: +```bash +PATH="$CONDA_PREFIX/bin:$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh polymarket_mock --force-init +``` + +If you use a virtualenv instead of conda, activate it first and omit +`$CONDA_PREFIX/bin` from the `PATH` prefix: +```bash +source gravity_e2e/.venv/bin/activate +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh polymarket_mock --force-init +``` + +Expected success logs include: +```text +Released mock Polymarket settlement: winning_slot= payout= +Polymarket match market resolved and claimed: marketId=1 winningSlot= totalPool=600000000000000000000 +PASSED +Suite polymarket_mock PASSED +All suites passed! +``` + +The suite README at `gravity_e2e/cluster_test_cases/polymarket_mock/README.md` +describes the local topology, what the test proves, and how this can evolve into +a production Polymarket-like Gravity product. + +### 6. Binance Index-Kline Price Feed Suite +The `binance_price_feed` suite is a local-only integration test for continuous stock-like +price feed rounds on Gravity. It registers deterministic +`provider=binance_index_kline_v1&continuous=true` `sourceType=3` tasks for +`NVDAUSDT` and `TSLAUSDT` through governance, starts a local mock Binance +`/fapi/v1/indexPriceKlines` server, waits for the next short test epoch so +`aptos-jwk-consensus` rebuilds relayer-backed observers, then waits for the +unsupported-JWK/oracle consensus path to publish at least three price rounds +into `NativeOracle` and checks `PriceFeedResolver.priceRounds`. + +The suite intentionally does not call live Binance. The local mock validates the +same closed-bucket request shape that the real Binance adapter uses: +`pair`, `interval`, `startTime`, `endTime`, and `limit=1`. Live Binance testnet +fetching is covered separately by the ignored `gravity-reth` relayer smoke test. + +This suite proves the epoch-config path for long-running feeds. It does not +implement intra-epoch dynamic request discovery; that still needs a deterministic +request watcher if Gravity wants one-off requests such as a future match score. + +Run it from the repository root after building `gravity_node` and `gravity_cli`: +```bash +PATH="$CONDA_PREFIX/bin:$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh binance_price_feed --force-init +``` + +To keep the price-feed backend alive for the web demo, run: +```bash +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh binance_price_feed \ + --force-init \ + --keep-running \ + --demo-config-out ../gravity_price_feed_demo_web/public/demo-config.json \ + --log-cli-level=INFO +``` + +The keep-running mode leaves the Gravity node and the local Binance kline mock +running after the suite passes. The web app reads the generated +`public/demo-config.json` to discover the local RPC URL, resolver address, feed +IDs, expected nonce, and expected round. + +For a live Binance demo, set `BINANCE_PRICE_FEED_MODE=live`. This mode is for +manual demos and sends outbound requests to Binance public market-data APIs. It +does not use `BINANCE_API_KEY` or `BINANCE_SECRET_KEY`. + +```bash +BINANCE_PRICE_FEED_MODE=live \ +BINANCE_PRICE_FEED_LAG_MINUTES=3 \ +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh binance_price_feed \ + --force-init \ + --keep-running \ + --demo-config-out ../gravity_price_feed_demo_web/public/demo-config.json \ + --log-cli-level=INFO +``` + +Live mode computes a 1-minute bucket that is at least two minutes past close, +generates a run-local relayer config, registers matching +`provider=binance_index_kline_v1` tasks, and then keeps the local chain running +so the frontend can display ongoing resolver updates. + +Expected success logs include: +```text +Binance price feed resolved: feedId=1001 deliveryNonce=3 roundId=29720877 price=19612645000 +Binance price feed resolved: feedId=1002 deliveryNonce=3 roundId=29720877 price=40117545000 +PASSED +Suite binance_price_feed PASSED +All suites passed! +``` + +This proves the contract-facing path a Gravity PerpDex or price-index market +would consume: `sourceType=3` data is recorded by `NativeOracle`, callbacked into +`PriceFeedResolver`, and exposed as `latestPrice(feedId)`. BBO, mid +price, TWAP, risk, and market-specific weighting should be decided by the +downstream product contract or by a separately versioned resolver policy. + +### 7. Combined Binance + Polymarket Suite + +The `oracle_demo` suite is the review and dashboard gate for both current +oracle products. One local Gravity cluster receives continuous Binance +index-kline rounds and a finalized Polymarket-style CTF settlement through the +same unsupported-JWK consensus path. + +Run it without public network traffic: + +```bash +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh oracle_demo --force-init +``` + +Keep the backend alive and write frontend discovery metadata: + +```bash +PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" \ + ./gravity_e2e/run_test.sh oracle_demo \ + --force-init \ + --keep-running \ + --demo-config-out ../gravity_price_feed_demo_web/public/demo-config.json \ + --log-cli-level=INFO +``` + +The suite directly deploys `PriceFeedResolver`, +`PolymarketSettlementResolver`, and `PolymarketBinaryMarket`. Shared deployment, +governance, polling, and deterministic provider helpers live under +`gravity_e2e/gravity_e2e/utils/`; product tests should reuse them instead of +importing another pytest module. + +Expected terminal evidence includes two resolved price feeds, one released CTF +payout vector, one settled Gravity market, and `Suite oracle_demo PASSED`. + +### 8. Docker Runner (CI) The `run_docker.sh` script runs the full pipeline inside Docker. It accepts the same arguments as `run_test.sh`: ```bash # Run all suites in Docker diff --git a/gravity_e2e/gravity_e2e/utils/mock_binance_index.py b/gravity_e2e/gravity_e2e/utils/mock_binance_index.py new file mode 100644 index 000000000..c916ba0d8 --- /dev/null +++ b/gravity_e2e/gravity_e2e/utils/mock_binance_index.py @@ -0,0 +1,118 @@ +"""Deterministic local Binance index-kline fixture shared by oracle E2E suites.""" + +from contextlib import contextmanager +import json +import logging +import os +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from urllib.parse import parse_qs, urlparse + +LOG = logging.getLogger(__name__) + +BUCKET_START_MS = 1_783_252_500_000 +INTERVAL_MS = 60_000 +DECIMALS = 8 +MOCK_BINANCE_PORT = 18547 +SUPPORTED_PAIRS = {"NVDAUSDT", "TSLAUSDT"} + + +def format_price(scaled_price: int) -> str: + whole = scaled_price // 10**DECIMALS + fraction = scaled_price % 10**DECIMALS + return f"{whole}.{fraction:0{DECIMALS}d}" + + +def mock_close_price(pair: str, bucket_index: int) -> str: + base_prices = {"NVDAUSDT": 19_592_645_000, "TSLAUSDT": 40_067_545_000} + increments = {"NVDAUSDT": 10_000_000, "TSLAUSDT": 25_000_000} + return format_price(base_prices[pair] + bucket_index * increments[pair]) + + +class MockBinanceIndexKlineHandler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def do_GET(self): + parsed = urlparse(self.path) + if parsed.path != "/fapi/v1/indexPriceKlines": + self.send_error(404) + return + + params = parse_qs(parsed.query) + pair = params.get("pair", [""])[0] + try: + start_time = int(params.get("startTime", ["-1"])[0]) + end_time = int(params.get("endTime", ["-1"])[0]) + except ValueError: + self.send_error(400) + return + if ( + params.get("interval", [""])[0] != "1m" + or params.get("limit", [""])[0] != "1" + or start_time < BUCKET_START_MS + or (start_time - BUCKET_START_MS) % INTERVAL_MS != 0 + or end_time != start_time + INTERVAL_MS - 1 + or pair not in SUPPORTED_PAIRS + ): + self.send_error(400) + return + + bucket_index = (start_time - BUCKET_START_MS) // INTERVAL_MS + close_px = mock_close_price(pair, bucket_index) + response = [ + [ + start_time, + close_px, + close_px, + close_px, + close_px, + "0", + end_time, + "0", + 60, + "0", + "0", + "0", + ] + ] + payload = json.dumps(response).encode() + self.send_response(200) + self.send_header("content-type", "application/json") + self.send_header("content-length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def log_message(self, fmt: str, *args): + LOG.debug("mock Binance index kline: " + fmt, *args) + + +class ReusableThreadingHTTPServer(ThreadingHTTPServer): + allow_reuse_address = True + + +@contextmanager +def mock_binance_index_kline_server(port: int = MOCK_BINANCE_PORT): + server = ReusableThreadingHTTPServer(("127.0.0.1", port), MockBinanceIndexKlineHandler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + yield f"http://127.0.0.1:{port}" + finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) + + +def serve_forever(port: int, pid_file: Path | None = None): + if pid_file is not None: + pid_file.parent.mkdir(parents=True, exist_ok=True) + pid_file.write_text(f"{os.getpid()}\n") + server = ReusableThreadingHTTPServer(("127.0.0.1", port), MockBinanceIndexKlineHandler) + LOG.info("Mock Binance index kline server listening on 127.0.0.1:%s", port) + try: + server.serve_forever() + finally: + server.server_close() + if pid_file is not None: + pid_file.unlink(missing_ok=True) diff --git a/gravity_e2e/gravity_e2e/utils/mock_polymarket_polygon.py b/gravity_e2e/gravity_e2e/utils/mock_polymarket_polygon.py new file mode 100644 index 000000000..6aa40f503 --- /dev/null +++ b/gravity_e2e/gravity_e2e/utils/mock_polymarket_polygon.py @@ -0,0 +1,388 @@ +""" +MockPolymarketPolygon - deterministic local Polygon JSON-RPC fixture for oracle E2E tests. + +The mock only implements the Polygon methods used by the gravity-reth +Polymarket settlement source: + - eth_chainId / net_version + - eth_blockNumber + - eth_getBlockByNumber("finalized" | "latest" | hex) + - eth_getLogs + +It preloads CTF ConditionResolution logs so the e2e cluster can exercise the +relayer + UnsupportedJWK consensus + NativeOracle path without a real Polygon +RPC endpoint or secrets. +""" + +import json +import logging +import threading +import time +from http.server import BaseHTTPRequestHandler, HTTPServer +from typing import Any, Dict, List, Optional + +LOG = logging.getLogger(__name__) + +POLYGON_CHAIN_ID = 137 +CTF_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045" +UMA_ORACLE = "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296" +MATCH_MARKET_ID = 1_897_398 +MATCH_CONDITION_ID = "0x2afe86f96be81a0d89ed776bedbd52d1c75bc47b49e6f0f791ddd009f52faf23" +MATCH_QUESTION_ID = "0x49a5e94a4b5a400dcd720ca1875fcd49ba55c303e43bf091bc175df72f74f501" +MATCH_TX_HASH = "0x97828bf9110f78c07f1ad5cff5415875b67b3fe032e19ee6aa2317355861aab2" +MATCH_BLOCK = 89_222_209 +MATCH_LOG_INDEX = 2_077 + +FED_BINARY_MARKET_ID = 7_202_626 +FED_BINARY_CONDITION_ID = "0xfed086f96be81a0d89ed776bedbd52d1c75bc47b49e6f0f791ddd009f52faf23" +FED_BINARY_QUESTION_ID = "0xfed5e94a4b5a400dcd720ca1875fcd49ba55c303e43bf091bc175df72f74f501" +FED_BINARY_TX_HASH = "0xfed28bf9110f78c07f1ad5cff5415875b67b3fe032e19ee6aa2317355861aab2" +FED_BINARY_BLOCK = 89_222_209 +FED_BINARY_LOG_INDEX = 2_078 + +# Backward-compatible aliases used by the original match-market test. +DRAW_MARKET_ID = MATCH_MARKET_ID +DRAW_CONDITION_ID = MATCH_CONDITION_ID +DRAW_QUESTION_ID = MATCH_QUESTION_ID +DRAW_TX_HASH = MATCH_TX_HASH +DRAW_BLOCK = MATCH_BLOCK +DRAW_LOG_INDEX = MATCH_LOG_INDEX + +CONDITION_RESOLUTION_TOPIC0 = "0xb44d84d3289691f71497564b85d4233648d9dbae8cbdbb4329f301c3a0185894" + + +def _to_hex(value: int) -> str: + return hex(value) + + +def _pad_topic(value: int) -> str: + return "0x" + value.to_bytes(32, "big").hex() + + +def _uint256(value: int) -> bytes: + return value.to_bytes(32, "big") + + +def _address_topic(address: str) -> str: + return "0x" + bytes.fromhex(address.replace("0x", "")).rjust(32, b"\x00").hex() + + +def _fake_hash(seed: int) -> str: + return "0x" + seed.to_bytes(32, "big").hex() + + +def generate_condition_resolution_log( + block_number: int = MATCH_BLOCK, + log_index: int = MATCH_LOG_INDEX, + payout_numerators: Optional[List[int]] = None, + condition_id: str = MATCH_CONDITION_ID, + question_id: str = MATCH_QUESTION_ID, + tx_hash: str = MATCH_TX_HASH, +) -> Dict[str, Any]: + """Generate a CTF ConditionResolution log for a configured mock market.""" + payouts = payout_numerators or [0, 1, 0] + data = _uint256(len(payouts)) + _uint256(64) + _uint256(len(payouts)) + data += b"".join(_uint256(payout) for payout in payouts) + return { + "address": CTF_ADDRESS.lower(), + "topics": [ + CONDITION_RESOLUTION_TOPIC0, + condition_id.lower(), + _address_topic(UMA_ORACLE).lower(), + question_id.lower(), + ], + "data": "0x" + data.hex(), + "blockNumber": _to_hex(block_number), + "blockHash": _fake_hash(block_number + 0x100), + "transactionHash": tx_hash, + "transactionIndex": "0x0", + "logIndex": _to_hex(log_index), + "removed": False, + } + + +class MockPolymarketPolygon: + def __init__(self, port: int = 8546, chain_id: int = POLYGON_CHAIN_ID): + self.port = port + self.chain_id = chain_id + self.current_block = MATCH_BLOCK - 1 + self._logs: Dict[int, List[Dict[str, Any]]] = {} + self._server: Optional[HTTPServer] = None + self._thread: Optional[threading.Thread] = None + + @property + def rpc_url(self) -> str: + return f"http://localhost:{self.port}" + + @property + def is_running(self) -> bool: + return self._thread is not None and self._thread.is_alive() + + def preload_draw_resolution(self) -> Dict[str, Any]: + log = self.preload_match_resolution(1, visible=True) + LOG.info( + "MockPolymarketPolygon: preloaded Draw settlement at block=%s logIndex=%s", + MATCH_BLOCK, + MATCH_LOG_INDEX, + ) + return log + + def preload_match_resolution(self, winning_slot: int, visible: bool = False) -> Dict[str, Any]: + if winning_slot < 0 or winning_slot > 2: + raise ValueError(f"winning_slot must be 0, 1, or 2; got {winning_slot}") + payouts = [0, 0, 0] + payouts[winning_slot] = 1 + log = generate_condition_resolution_log(payout_numerators=payouts) + self._logs[MATCH_BLOCK] = [log] + self.current_block = MATCH_BLOCK if visible else MATCH_BLOCK - 1 + LOG.info( + "MockPolymarketPolygon: prepared winning_slot=%s payout=%s visible=%s", + winning_slot, + payouts, + visible, + ) + return log + + def release_match_resolution(self, winning_slot: int) -> Dict[str, Any]: + if winning_slot < 0 or winning_slot > 2: + raise ValueError(f"winning_slot must be 0, 1, or 2; got {winning_slot}") + payouts = [0, 0, 0] + payouts[winning_slot] = 1 + log = self.preload_match_resolution(winning_slot, visible=True) + return { + "market_id": MATCH_MARKET_ID, + "condition_id": MATCH_CONDITION_ID, + "question_id": MATCH_QUESTION_ID, + "ctf": CTF_ADDRESS, + "oracle": UMA_ORACLE, + "tx_hash": MATCH_TX_HASH, + "block": MATCH_BLOCK, + "log_index": MATCH_LOG_INDEX, + "winning_slot": winning_slot, + "payout_numerators": payouts, + "source_log": log, + } + + def preload_binary_resolution(self, winning_slot: int, visible: bool = False) -> Dict[str, Any]: + if winning_slot < 0 or winning_slot > 1: + raise ValueError(f"winning_slot must be 0 or 1; got {winning_slot}") + payouts = [0, 0] + payouts[winning_slot] = 1 + log = generate_condition_resolution_log( + block_number=FED_BINARY_BLOCK, + log_index=FED_BINARY_LOG_INDEX, + payout_numerators=payouts, + condition_id=FED_BINARY_CONDITION_ID, + question_id=FED_BINARY_QUESTION_ID, + tx_hash=FED_BINARY_TX_HASH, + ) + self._logs[FED_BINARY_BLOCK] = [log] + self.current_block = FED_BINARY_BLOCK if visible else FED_BINARY_BLOCK - 1 + LOG.info( + "MockPolymarketPolygon: prepared binary winning_slot=%s payout=%s visible=%s", + winning_slot, + payouts, + visible, + ) + return log + + def release_binary_resolution(self, winning_slot: int) -> Dict[str, Any]: + if winning_slot < 0 or winning_slot > 1: + raise ValueError(f"winning_slot must be 0 or 1; got {winning_slot}") + payouts = [0, 0] + payouts[winning_slot] = 1 + log = self.preload_binary_resolution(winning_slot, visible=True) + return { + "market_id": FED_BINARY_MARKET_ID, + "condition_id": FED_BINARY_CONDITION_ID, + "question_id": FED_BINARY_QUESTION_ID, + "ctf": CTF_ADDRESS, + "oracle": UMA_ORACLE, + "tx_hash": FED_BINARY_TX_HASH, + "block": FED_BINARY_BLOCK, + "log_index": FED_BINARY_LOG_INDEX, + "winning_slot": winning_slot, + "payout_numerators": payouts, + "source_log": log, + } + + def handle_request(self, body: dict) -> dict: + method = body.get("method", "") + params = body.get("params", []) + req_id = body.get("id", 1) + + try: + if method == "eth_getBlockByNumber": + result = self._handle_get_block_by_number(params) + elif method == "eth_getLogs": + result = self._handle_get_logs(params) + elif method == "eth_chainId": + result = _to_hex(self.chain_id) + elif method == "net_version": + result = str(self.chain_id) + elif method == "eth_blockNumber": + result = _to_hex(self.current_block) + elif method == "mock_setWinningSlot": + winning_slot = params[0] if params else 1 + if isinstance(winning_slot, str): + winning_slot = int(winning_slot, 16) if winning_slot.startswith("0x") else int(winning_slot) + result = self.release_match_resolution(int(winning_slot)) + elif method == "mock_setBinaryWinningSlot": + winning_slot = params[0] if params else 0 + if isinstance(winning_slot, str): + winning_slot = int(winning_slot, 16) if winning_slot.startswith("0x") else int(winning_slot) + result = self.release_binary_resolution(int(winning_slot)) + else: + LOG.debug("MockPolymarketPolygon: unsupported method '%s'", method) + result = None + return {"jsonrpc": "2.0", "id": req_id, "result": result} + except Exception as exc: + LOG.error("MockPolymarketPolygon: error handling %s: %s", method, exc) + return { + "jsonrpc": "2.0", + "id": req_id, + "error": {"code": -32603, "message": str(exc)}, + } + + def _handle_get_block_by_number(self, params: list) -> Optional[dict]: + if not params: + return None + + block_tag = params[0] + if block_tag in ("finalized", "latest", "safe", "pending"): + block_num = self.current_block + elif block_tag == "earliest": + block_num = 0 + elif isinstance(block_tag, str) and block_tag.startswith("0x"): + block_num = int(block_tag, 16) + else: + block_num = int(block_tag) + + if block_num > self.current_block: + return None + + return { + "number": _to_hex(block_num), + "hash": _fake_hash(block_num + 0x100), + "parentHash": _fake_hash(block_num + 0x0FF), + "timestamp": _to_hex(1_700_000_000 + block_num), + "gasLimit": _to_hex(100_000_000), + "gasUsed": "0x0", + "miner": "0x" + "00" * 20, + "difficulty": "0x0", + "totalDifficulty": "0x0", + "size": "0x100", + "nonce": "0x0000000000000000", + "extraData": "0x", + "logsBloom": "0x" + "00" * 256, + "transactionsRoot": "0x" + "00" * 32, + "stateRoot": "0x" + "00" * 32, + "receiptsRoot": "0x" + "00" * 32, + "sha3Uncles": "0x" + "00" * 32, + "uncles": [], + "transactions": [], + "baseFeePerGas": "0x0", + "mixHash": "0x" + "00" * 32, + } + + def _handle_get_logs(self, params: list) -> list: + if not params: + return [] + filter_obj = params[0] + from_block = self._parse_block_tag(filter_obj.get("fromBlock", "0x0")) + to_block = self._parse_block_tag(filter_obj.get("toBlock", _to_hex(self.current_block))) + filter_address = filter_obj.get("address", "").lower() + filter_topics = filter_obj.get("topics", []) + + results = [] + for block_num in range(from_block, to_block + 1): + for log in self._logs.get(block_num, []): + if filter_address and log["address"] != filter_address: + continue + if not self._topics_match(log["topics"], filter_topics): + continue + results.append(log) + return results + + def _parse_block_tag(self, tag) -> int: + if isinstance(tag, int): + return tag + if tag in ("latest", "finalized", "safe", "pending"): + return self.current_block + if tag == "earliest": + return 0 + if isinstance(tag, str) and tag.startswith("0x"): + return int(tag, 16) + return int(tag) + + @staticmethod + def _topics_match(log_topics: list, filter_topics: list) -> bool: + for idx, filter_topic in enumerate(filter_topics): + if filter_topic is None: + continue + if idx >= len(log_topics): + return False + if isinstance(filter_topic, list): + allowed = [topic.lower() for topic in filter_topic] + if log_topics[idx].lower() not in allowed: + return False + elif log_topics[idx].lower() != filter_topic.lower(): + return False + return True + + def start(self) -> None: + if self.is_running: + self.stop() + + mock = self + + class Handler(BaseHTTPRequestHandler): + def do_POST(self): + content_len = int(self.headers.get("Content-Length", 0)) + body_bytes = self.rfile.read(content_len) + try: + body = json.loads(body_bytes) + except json.JSONDecodeError: + self.send_error(400, "Invalid JSON") + return + + if isinstance(body, list): + response_body = json.dumps([mock.handle_request(req) for req in body]) + else: + response_body = json.dumps(mock.handle_request(body)) + + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(response_body.encode()) + + def log_message(self, fmt, *args): + pass + + self._server = HTTPServer(("127.0.0.1", self.port), Handler) + self.port = self._server.server_port + self._thread = threading.Thread(target=self._server.serve_forever, daemon=True) + self._thread.start() + + import socket + + deadline = time.time() + 5 + while time.time() < deadline: + try: + with socket.create_connection(("127.0.0.1", self.port), timeout=1): + break + except (ConnectionRefusedError, OSError): + time.sleep(0.1) + + LOG.info("MockPolymarketPolygon running at %s", self.rpc_url) + + def stop(self) -> None: + server = self._server + thread = self._thread + if server is not None: + LOG.info("MockPolymarketPolygon: shutting down") + self._server = None + self._thread = None + server.shutdown() + server.server_close() + if thread is not None: + thread.join(timeout=1) diff --git a/gravity_e2e/gravity_e2e/utils/oracle_test_support.py b/gravity_e2e/gravity_e2e/utils/oracle_test_support.py new file mode 100644 index 000000000..052ae9725 --- /dev/null +++ b/gravity_e2e/gravity_e2e/utils/oracle_test_support.py @@ -0,0 +1,476 @@ +"""Shared contract, governance, and price-feed helpers for oracle E2E suites.""" + +import asyncio +from contextlib import nullcontext +import json +import os +import shutil +import subprocess +import time +from pathlib import Path +from typing import Optional + +import pytest +from eth_abi import encode +from eth_account import Account +from web3 import Web3 + +from gravity_e2e.utils.mock_binance_index import ( + BUCKET_START_MS, + DECIMALS, + INTERVAL_MS, + MOCK_BINANCE_PORT, + format_price, + mock_binance_index_kline_server, +) + +try: + import tomllib +except ImportError: + import tomli as tomllib + + +NATIVE_ORACLE_ADDRESS = Web3.to_checksum_address( + "0x00000000000000000000000000000001625F4000" +) +ORACLE_TASK_CONFIG_ADDRESS = Web3.to_checksum_address( + "0x00000000000000000000000000000001625F1009" +) +GOVERNANCE_ADDRESS = Web3.to_checksum_address( + "0x00000000000000000000000000000001625F3000" +) +STAKING_ADDRESS = Web3.to_checksum_address( + "0x00000000000000000000000000000001625F2000" +) + +FAUCET_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +FAUCET_ADDR = Web3.to_checksum_address("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266") + +SOURCE_TYPE_PRICE_FEED = 3 +TASK_PRICE_FEED = Web3.keccak(text="price_feed") +NVDA_FEED_ID = 1001 +TSLA_FEED_ID = 1002 +TARGET_DELIVERY_NONCE = 3 +AGG_WEIGHTED_MEDIAN = 2 +SOURCE_COUNT = 1 +TOTAL_WEIGHT = 1 +EXPECTED_NVDA_PRICE = 19_612_645_000 +EXPECTED_TSLA_PRICE = 40_117_545_000 +DEFAULT_LIVE_BINANCE_BASE_URL = "https://fapi.binance.com" +DEFAULT_BINANCE_GRACE_MS = 120_000 +MODE_MOCK = "mock" +MODE_LIVE = "live" + +DATA_RECORDED_TOPIC0 = Web3.keccak( + text="DataRecorded(uint32,uint256,uint128,uint256)" +).hex() +MARKET_CREATED_TOPIC0 = Web3.keccak(text="MarketCreated(uint256,bytes32,uint256)") +PROPOSAL_CREATED_TOPIC0 = Web3.keccak( + text="ProposalCreated(uint64,address,address,bytes32,string)" +) + +SEL_OWNER = Web3.keccak(text="owner()")[:4] +SEL_ADD_EXECUTOR = Web3.keccak(text="addExecutor(address)")[:4] +SEL_IS_EXECUTOR = Web3.keccak(text="isExecutor(address)")[:4] +SEL_GET_POOL = Web3.keccak(text="getPool(uint256)")[:4] +SEL_GET_POOL_VOTER = Web3.keccak(text="getPoolVoter(address)")[:4] +SEL_GET_POOL_VOTING_POWER_NOW = Web3.keccak(text="getPoolVotingPowerNow(address)")[:4] +SEL_CREATE_PROPOSAL = Web3.keccak(text="createProposal(address,address[],bytes[],string)")[:4] +SEL_VOTE = Web3.keccak(text="vote(address,uint64,uint128,bool)")[:4] +SEL_RESOLVE = Web3.keccak(text="resolve(uint64)")[:4] +SEL_EXECUTE = Web3.keccak(text="execute(uint64,address[],bytes[])")[:4] +SEL_GET_PROPOSAL_STATE = Web3.keccak(text="getProposalState(uint64)")[:4] + +MAX_UINT128 = (1 << 128) - 1 +PROPOSAL_STATE_SUCCEEDED = 1 +VOTING_DURATION_SECS = 5 +STAKE_UNIT = 10**18 + + +def topic(value: int) -> str: + return "0x" + value.to_bytes(32, "big").hex() + + +def topic_hex(value) -> str: + result = value.hex() + return result if result.startswith("0x") else f"0x{result}" + + +def as_bytes(data) -> bytes: + if isinstance(data, bytes): + return data + if isinstance(data, str): + return bytes.fromhex(data[2:] if data.startswith("0x") else data) + return bytes(data) + + +def function_calldata(fn) -> bytes: + return as_bytes(fn._encode_transaction_data()) + + +def send_tx( + w3: Web3, + to: Optional[str], + data, + sender_key: str, + gas: int = 1_000_000, + value: int = 0, +) -> dict: + sender = Account.from_key(sender_key) + tx = { + "data": data, + "gas": gas, + "gasPrice": w3.eth.gas_price, + "nonce": w3.eth.get_transaction_count(sender.address), + "chainId": w3.eth.chain_id, + "value": value, + } + if to is not None: + tx["to"] = to + signed = sender.sign_transaction(tx) + tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) + receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=90) + assert receipt["status"] == 1, f"transaction failed: {receipt}" + return receipt + + +def send_contract_tx(w3: Web3, contract, fn, sender_key: str, gas: int = 1_000_000) -> dict: + return send_tx(w3, contract.address, function_calldata(fn), sender_key, gas=gas) + + +def artifact_file(out_dir: Path, source_name: str, contract_name: str) -> Path: + direct = out_dir / source_name / f"{contract_name}.json" + if direct.exists(): + return direct + raise FileNotFoundError( + f"missing {source_name}/{contract_name}.json under contracts out dir" + ) + + +def contracts_repo_from_genesis(suite_dir: Path) -> Path | None: + data = tomllib.loads((suite_dir / "genesis.toml").read_text()) + rel = data.get("dependencies", {}).get("genesis_contracts", {}).get("path") + return (suite_dir / rel).resolve() if rel else None + + +def required_contract_sources( + contracts_repo: Path, required: list[tuple[str, str]] +) -> list[Path]: + source_names = dict.fromkeys(source for source, _ in required) + source_roots = [contracts_repo / name for name in ("src", "test", "script")] + paths = [] + + for source_name in source_names: + matches = [ + path + for root in source_roots + if root.is_dir() + for path in root.rglob(source_name) + ] + if len(matches) != 1: + relative_matches = [str(path.relative_to(contracts_repo)) for path in matches] + raise FileNotFoundError( + f"expected exactly one source file named {source_name}; " + f"found {relative_matches}" + ) + paths.append(matches[0].relative_to(contracts_repo)) + + return paths + + +def ensure_contracts_out( + suite_dir: Path, + required: list[tuple[str, str]], + error_label: str = "Oracle", +) -> Path: + sdk_root = suite_dir.parents[2] + contracts_repo = contracts_repo_from_genesis(suite_dir) + if contracts_repo is not None and shutil.which("forge"): + sources = required_contract_sources(contracts_repo, required) + subprocess.run( + ["forge", "build", *(str(source) for source in sources)], + cwd=contracts_repo, + check=True, + ) + out_dir = contracts_repo / "out" + for source, name in required: + artifact_file(out_dir, source, name) + return out_dir + + candidates = [] + candidates.extend( + [ + sdk_root / "external" / "gravity_chain_core_contracts_local" / "out", + sdk_root / "external" / "gravity_chain_core_contracts" / "out", + ] + ) + + for out_dir in candidates: + if not out_dir.exists(): + continue + try: + for source, name in required: + artifact_file(out_dir, source, name) + return out_dir + except FileNotFoundError: + pass + + raise FileNotFoundError( + f"{error_label} contract artifacts not found; run forge build in contracts repo" + ) + + +def load_artifact(out_dir: Path, source_name: str, contract_name: str) -> dict: + return json.loads(artifact_file(out_dir, source_name, contract_name).read_text()) + + +def deploy_contract( + w3: Web3, + artifact: dict, + sender_key: str, + args=None, + gas: int = 8_000_000, +): + sender = Account.from_key(sender_key) + factory = w3.eth.contract(abi=artifact["abi"], bytecode=_bytecode(artifact)) + tx = factory.constructor(*(args or [])).build_transaction( + { + "from": sender.address, + "gas": gas, + "gasPrice": w3.eth.gas_price, + "nonce": w3.eth.get_transaction_count(sender.address), + "chainId": w3.eth.chain_id, + } + ) + signed = sender.sign_transaction(tx) + tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) + receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120) + assert receipt["status"] == 1, f"deploy failed: {receipt}" + return w3.eth.contract(address=receipt["contractAddress"], abi=artifact["abi"]) + + +def _bytecode(artifact: dict) -> str: + bytecode = artifact["bytecode"] + if isinstance(bytecode, dict): + bytecode = bytecode["object"] + return bytecode if bytecode.startswith("0x") else f"0x{bytecode}" + + +def _call(w3: Web3, to: str, data: bytes) -> bytes: + return w3.eth.call({"to": to, "data": data}) + + +def _decode_address(raw: bytes) -> str: + return Web3.to_checksum_address("0x" + raw[-20:].hex()) + + +def _ensure_faucet_executor(w3: Web3): + owner = _decode_address(_call(w3, GOVERNANCE_ADDRESS, SEL_OWNER)) + assert owner == FAUCET_ADDR, f"Governance.owner expected faucet, got {owner}" + is_executor = bool( + int.from_bytes( + _call(w3, GOVERNANCE_ADDRESS, SEL_IS_EXECUTOR + encode(["address"], [FAUCET_ADDR])), + "big", + ) + ) + if not is_executor: + send_tx( + w3, + GOVERNANCE_ADDRESS, + SEL_ADD_EXECUTOR + encode(["address"], [FAUCET_ADDR]), + FAUCET_KEY, + ) + + +async def execute_governance_proposal( + w3: Web3, + pool_addr: str, + targets: list[str], + datas: list[bytes], + description: str, + gas: int = 5_000_000, +) -> dict: + _ensure_faucet_executor(w3) + create_data = SEL_CREATE_PROPOSAL + encode( + ["address", "address[]", "bytes[]", "string"], + [pool_addr, targets, datas, description], + ) + try: + w3.eth.call({"from": FAUCET_ADDR, "to": GOVERNANCE_ADDRESS, "data": create_data, "gas": gas}) + except Exception as exc: + pytest.fail(f"createProposal would revert: {exc!r}") + receipt = send_tx(w3, GOVERNANCE_ADDRESS, create_data, FAUCET_KEY, gas=gas) + + proposal_id = next( + ( + int.from_bytes(log["topics"][1], "big") + for log in receipt["logs"] + if log["topics"] and bytes(log["topics"][0]) == bytes(PROPOSAL_CREATED_TOPIC0) + ), + None, + ) + assert proposal_id is not None, "ProposalCreated event not found" + + vote_data = SEL_VOTE + encode( + ["address", "uint64", "uint128", "bool"], + [pool_addr, proposal_id, MAX_UINT128, True], + ) + send_tx(w3, GOVERNANCE_ADDRESS, vote_data, FAUCET_KEY) + vote_block = w3.eth.block_number + + await asyncio.sleep(VOTING_DURATION_SECS + 2) + deadline = time.monotonic() + 30 + while time.monotonic() < deadline and w3.eth.block_number < vote_block + 3: + await asyncio.sleep(1) + assert w3.eth.block_number >= vote_block + 3, "chain did not advance past vote block" + + resolve_data = SEL_RESOLVE + encode(["uint64"], [proposal_id]) + send_tx(w3, GOVERNANCE_ADDRESS, resolve_data, FAUCET_KEY) + state = int.from_bytes( + _call(w3, GOVERNANCE_ADDRESS, SEL_GET_PROPOSAL_STATE + encode(["uint64"], [proposal_id])), + "big", + ) + assert state == PROPOSAL_STATE_SUCCEEDED, f"proposal state is not SUCCEEDED: {state}" + + execute_data = SEL_EXECUTE + encode( + ["uint64", "address[]", "bytes[]"], [proposal_id, targets, datas] + ) + return send_tx(w3, GOVERNANCE_ADDRESS, execute_data, FAUCET_KEY, gas=gas) + + +def pool0_voted_by_faucet(w3: Web3) -> str: + pool_addr = _decode_address(_call(w3, STAKING_ADDRESS, SEL_GET_POOL + encode(["uint256"], [0]))) + voter_addr = _decode_address( + _call(w3, STAKING_ADDRESS, SEL_GET_POOL_VOTER + encode(["address"], [pool_addr])) + ) + assert voter_addr == FAUCET_ADDR, f"pool[0].voter expected faucet, got {voter_addr}" + voting_power = int.from_bytes( + _call( + w3, + STAKING_ADDRESS, + SEL_GET_POOL_VOTING_POWER_NOW + encode(["address"], [pool_addr]), + ), + "big", + ) + assert voting_power >= STAKE_UNIT, f"pool[0] voting power too low: {voting_power}" + return pool_addr + + +def market_id_from_receipt(receipt: dict) -> int: + for log in receipt["logs"]: + if log["topics"] and bytes(log["topics"][0]) == bytes(MARKET_CREATED_TOPIC0): + return int.from_bytes(log["topics"][1], "big") + raise AssertionError("MarketCreated event not found") + + +async def wait_for_chain_time(w3: Web3, target_ts: int, timeout: Optional[int] = None): + latest = w3.eth.get_block("latest") + if timeout is None: + timeout = max(30, target_ts - latest["timestamp"] + 30) + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + latest = w3.eth.get_block("latest") + if latest["timestamp"] >= target_ts: + return latest + await asyncio.sleep(1) + raise TimeoutError(f"chain timestamp did not reach {target_ts}") + + +async def poll_price_recorded(w3: Web3, feed_id: int, timeout: int = 180): + deadline = time.monotonic() + timeout + filter_params = { + "fromBlock": 0, + "toBlock": "latest", + "address": NATIVE_ORACLE_ADDRESS, + "topics": [DATA_RECORDED_TOPIC0, topic(SOURCE_TYPE_PRICE_FEED), topic(feed_id)], + } + while time.monotonic() < deadline: + logs = await asyncio.to_thread(w3.eth.get_logs, filter_params) + if logs: + return logs + await asyncio.sleep(2) + return [] + + +async def wait_for_latest_nonce(native_oracle, feed_id: int, target_nonce: int, timeout: int = 240): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + nonce = native_oracle.functions.getLatestNonce(SOURCE_TYPE_PRICE_FEED, feed_id).call() + if nonce >= target_nonce: + return nonce + await asyncio.sleep(2) + raise TimeoutError(f"latest nonce for feedId={feed_id} did not reach {target_nonce}") + + +async def wait_for_price_round(resolver, feed_id: int, round_id: int, timeout: int = 120): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + stored = resolver.functions.priceRounds(feed_id, round_id).call() + if stored[0]: + return stored + await asyncio.sleep(2) + raise TimeoutError(f"priceRounds({feed_id}, {round_id}) was not stored") + + +def assert_price_round(latest, expected_price: int, round_id: int, resolved_at: int): + assert latest[0] is True + assert latest[1] == round_id + assert latest[2] == resolved_at + assert latest[3] == DECIMALS + assert latest[4] == AGG_WEIGHTED_MEDIAN + assert latest[5] == SOURCE_COUNT + assert latest[6] == TOTAL_WEIGHT + assert latest[7] == expected_price + + +def price_feed_mode() -> str: + return os.environ.get("BINANCE_PRICE_FEED_MODE", MODE_MOCK).strip().lower() + + +def is_live_mode() -> bool: + return price_feed_mode() == MODE_LIVE + + +def binance_base_url() -> str: + if is_live_mode(): + return os.environ.get("BINANCE_PRICE_FEED_BASE_URL", DEFAULT_LIVE_BINANCE_BASE_URL) + return f"http://127.0.0.1:{MOCK_BINANCE_PORT}" + + +def max_staleness_ms() -> int: + default = "3600000" if is_live_mode() else "180000" + return int(os.environ.get("BINANCE_PRICE_FEED_MAX_STALENESS_MS", default)) + + +def binance_grace_ms() -> int: + return int(os.environ.get("BINANCE_PRICE_FEED_GRACE_MS", str(DEFAULT_BINANCE_GRACE_MS))) + + +def round_start_ms(first_bucket_start_ms: int, delivery_nonce: int) -> int: + return first_bucket_start_ms + (delivery_nonce - 1) * INTERVAL_MS + + +def round_id(first_bucket_start_ms: int, delivery_nonce: int) -> int: + return round_start_ms(first_bucket_start_ms, delivery_nonce) // INTERVAL_MS + + +def round_end_ms(first_bucket_start_ms: int, delivery_nonce: int) -> int: + return round_start_ms(first_bucket_start_ms, delivery_nonce) + INTERVAL_MS - 1 + + +def price_feed_uri(feed_id: int, pair: str, bucket_start_ms: int) -> str: + return ( + f"gravity://3/{feed_id}/price_feed?" + f"provider=binance_index_kline_v1&pair={pair}&interval=1m&" + f"bucketStartMs={bucket_start_ms}&continuous=true&decimals=8&aggregationMode=2&" + f"minSourceCount=1&minTotalWeight=1&maxStaleness={max_staleness_ms()}&" + f"graceMs={binance_grace_ms()}" + ) + + +def server_context(): + if is_live_mode(): + return nullcontext(binance_base_url()) + if os.environ.get("BINANCE_PRICE_FEED_EXTERNAL_MOCK") == "1": + return nullcontext(f"http://127.0.0.1:{MOCK_BINANCE_PORT}") + return mock_binance_index_kline_server(MOCK_BINANCE_PORT) diff --git a/gravity_e2e/runner.py b/gravity_e2e/runner.py index a20dbeb5d..696ab4e71 100644 --- a/gravity_e2e/runner.py +++ b/gravity_e2e/runner.py @@ -195,9 +195,11 @@ def verify_nodes_alive(cluster_config: Path, env: dict): def run_test_suite( test_dir: Path, no_cleanup: bool = False, + keep_running: bool = False, pytest_args: list = None, force_init: bool = False, resume: bool = False, + demo_config_out: Path | None = None, ): """ Run tests in a specific directory. @@ -216,6 +218,10 @@ def run_test_suite( env = os.environ.copy() ensure_local_no_proxy(env) env["GRAVITY_ARTIFACTS_DIR"] = str(suite_artifacts_dir) + if keep_running: + env["GRAVITY_DEMO_KEEP_RUNNING"] = "1" + if demo_config_out is not None: + env["GRAVITY_DEMO_CONFIG_OUT"] = str(demo_config_out.resolve()) # Set genesis config path if it exists if genesis_config.exists(): @@ -328,6 +334,10 @@ def run_test_suite( env[env_var] = str(tpl_path) logger.info(f"Using custom reth config template: {env_var}={tpl_path}") + if hooks and hasattr(hooks, "pre_deploy"): + logger.info(f"Running pre_deploy hook from {hooks_path}") + hooks.pre_deploy(test_dir, env, pytest_args or []) + run_command( ["bash", str(deploy_script), str(cluster_config)], cwd=CLUSTER_SCRIPTS_DIR, @@ -390,6 +400,13 @@ def run_test_suite( # 5. Teardown if resume: logger.info("♻️ Reuse-cluster mode: skipping teardown") + elif keep_running and success: + logger.warning("Demo keep-running mode enabled; cluster left running.") + logger.warning(f"Cluster config: {cluster_config}") + logger.warning( + "Stop it with: " + f"bash {CLUSTER_SCRIPTS_DIR / 'stop.sh'} --config {cluster_config}" + ) elif no_cleanup and not success: logger.warning( f"Test failed and --no-cleanup set. Cluster left running using config: {cluster_config}" @@ -411,7 +428,13 @@ def run_test_suite( # MockAnvil runs as a daemon thread in this process — it dies # when we exit anyway, but explicit cleanup is cleaner and # prevents the relayer from crashing on connection loss. - if not resume and hooks and hasattr(hooks, "post_stop"): + should_run_post_stop = ( + not resume + and not (keep_running and success) + and hooks + and hasattr(hooks, "post_stop") + ) + if should_run_post_stop: logger.info("Running post_stop hook...") try: hooks.post_stop(test_dir, env) @@ -426,6 +449,17 @@ def main(): action="store_true", help="Leave cluster running if tests fail (for debugging)", ) + parser.add_argument( + "--keep-running", + action="store_true", + help="Leave the cluster and demo helper processes running after a successful suite.", + ) + parser.add_argument( + "--demo-config-out", + type=Path, + default=None, + help="Path where a suite may write frontend-readable demo runtime config.", + ) parser.add_argument( "--force-init", action="store_true", @@ -519,9 +553,11 @@ def main(): run_test_suite( test_dir, no_cleanup=args.no_cleanup, + keep_running=args.keep_running, pytest_args=pytest_args, force_init=args.force_init, resume=args.resume, + demo_config_out=args.demo_config_out, ) except Exception: logger.exception(f"Suite {test_dir.name} failed with exception:")