From cebf75dbabfd8ff71bb2897a58d1c3dac2b8988d Mon Sep 17 00:00:00 2001 From: ByteYue Date: Tue, 4 Aug 2026 01:54:49 +0800 Subject: [PATCH 01/12] test(oracle): add manual live soak suite Add one opt-in four-validator suite that discovers live Binance Futures testnet index-price buckets and a finalized Polygon Polymarket settlement before deployment, activates both tasks through governance at an epoch boundary, and verifies JWK quorum through execution-layer callbacks. The suite monitors replica convergence, monotonic source progress, exact Binance closed-bucket prices, relayer quorum, immutable Polymarket settlement, callback counts, and node4 restart recovery. Generated source mappings and metadata are ignored and removed on teardown, including deployed relayer configs. Validated with: - 3-minute live smoke: 5 bucket advances, 6 price callbacks, 1 settlement callback - 30-minute burn-in: 1,802.6s, 119 samples, 32 advances (24 required), 76.11s max price gap, 33 price callbacks, 1 settlement callback - 3-minute restart probe: node4 recovered in 11.17s, 4 bucket advances, 5 price callbacks, 1 settlement callback The documented 24-hour run restarts node4 halfway and requires at least 80% of expected minute advances. --- .gitignore | 1 + .../oracle_live_soak/README.md | 134 +++ .../oracle_live_soak/cluster.toml | 73 ++ .../oracle_live_soak/genesis.toml | 105 ++ .../oracle_live_soak/hooks.py | 359 +++++++ .../oracle_live_soak/test_oracle_live_soak.py | 978 ++++++++++++++++++ .../gravity_e2e/utils/oracle_test_support.py | 8 +- gravity_e2e/runner.py | 6 +- 8 files changed, 1660 insertions(+), 4 deletions(-) create mode 100644 gravity_e2e/cluster_test_cases/oracle_live_soak/README.md create mode 100644 gravity_e2e/cluster_test_cases/oracle_live_soak/cluster.toml create mode 100644 gravity_e2e/cluster_test_cases/oracle_live_soak/genesis.toml create mode 100644 gravity_e2e/cluster_test_cases/oracle_live_soak/hooks.py create mode 100644 gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py diff --git a/.gitignore b/.gitignore index 18401528..7a466181 100644 --- a/.gitignore +++ b/.gitignore @@ -182,4 +182,5 @@ __pycache__/ sentinel.toml sentinel_state.json gravity_e2e/cluster_test_cases/bridge/mock_anvil_metadata.json +gravity_e2e/cluster_test_cases/oracle_live_soak/artifacts/ bench_reports/ diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md new file mode 100644 index 00000000..f7ce3a47 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md @@ -0,0 +1,134 @@ +# Wave 7 Manual Live Oracle Soak + +This manual, non-gating suite runs four equal-power Gravity validators against: + +- one continuous Binance `NVDAUSDT` closed index-price kline feed (`sourceType=3`); +- one recently closed binary Polymarket market whose CTF resolution is already + finalized on Polygon (`sourceType=6`). + +The suite discovers the live inputs before deployment, then submits one Gravity +governance proposal that registers both tasks and their resolver callbacks. It +proves that observers remain absent in proposal epoch `E`, start in `E+1`, and +reach a three-of-four JWK quorum before the soak timer begins. + +This is operational evidence, not merge-gating CI. The deterministic Binance +and Polymarket suites remain the reproducible CI coverage. + +## What Every Heartbeat Checks + +- all four Gravity RPC replicas can serve one common snapshot block; +- each replica returns the same NativeOracle progress and resolver state; +- Binance delivery nonce, source position, and resolver round never regress; +- the latest resolver value maps to the exact closed one-minute bucket; +- the Binance feed and every Gravity node stay within the configured stall + budget; +- at least three relayers have checkpointed the committed Binance nonce; +- the finalized Polymarket settlement remains immutable at nonce `1`. + +At completion, the suite fetches the exact final Binance bucket again and +compares its close with the onchain value. It also counts NativeOracle callback +successes: the Binance count must equal its final delivery nonce, while the +Polymarket settlement must have exactly one successful delivery. + +For runs of at least one hour, `node4` restarts halfway through by default. The +suite requires its chain and relayer checkpoints to catch up without nonce +regression or a duplicate Polymarket delivery. + +## Prerequisites + +- Current quick-release `gravity_node` and `gravity_cli` binaries. +- Foundry `forge` for the resolver artifacts. +- Approved outbound access to Binance Futures testnet, Polymarket Gamma, and + Polygon. +- `POLYGON_RPC_URL` or `POLYGON_QUICKNONE_HTTP_URL` set to an HTTPS Polygon RPC. + +The Binance Futures testnet `indexPriceKlines` endpoint used here is public and +does not require an API key. It exercises a live HTTP source and the complete +validator consensus path, but its values are testnet index data and must be +labeled that way in a demo. Do not put RPC credentials in committed files. The +generated relayer mapping and source metadata live under the ignored suite +`artifacts/` directory and are removed during normal teardown. + +The runner performs a global local `gravity_node` cleanup before and after the +suite. Do not run this beside another local Gravity cluster you need to keep. + +## Build + +From the SDK repository root: + +```bash +make MODE=quick-release gravity_node gravity_cli +export PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" +``` + +Export the private Polygon RPC without printing it. For a standard shell-safe +environment file, either load it normally or extract only the required entry: + +```bash +export POLYGON_QUICKNONE_HTTP_URL="$( + sed -n 's/^POLYGON_QUICKNONE_HTTP_URL=//p' /path/to/private.env +)" +``` + +## Required Gate: 30-Minute Burn-In + +Run this first. Validator restart is intentionally disabled for the burn-in; +restart recovery belongs to the 24-hour run. + +```bash +ORACLE_SOAK_DURATION_SECONDS=1800 \ +ORACLE_SOAK_POLL_SECONDS=15 \ +ORACLE_SOAK_STALL_TIMEOUT_SECONDS=360 \ +ORACLE_SOAK_MIN_ADVANCES=24 \ +ORACLE_SOAK_RESTART_AFTER_SECONDS=0 \ + ./gravity_e2e/run_test.sh \ + oracle_live_soak \ + --force-init \ + --log-cli-level=INFO +``` + +Do not start the 24-hour run unless this command exits successfully and the +summary reports `status: passed`. + +## 24-Hour Soak + +Run from a terminal multiplexer or another session that will stay alive: + +```bash +ORACLE_SOAK_DURATION_SECONDS=86400 \ +ORACLE_SOAK_POLL_SECONDS=15 \ +ORACLE_SOAK_STALL_TIMEOUT_SECONDS=360 \ + ./gravity_e2e/run_test.sh \ + oracle_live_soak \ + --force-init \ + --log-cli-level=INFO +``` + +With the defaults, `node4` restarts after 12 hours. The pytest process is the +monitor, performs final assertions, writes the report, and tears down the +cluster. Do not terminate it after merely seeing the first onchain update. + +## Controls + +| Variable | Default | Meaning | +| --- | ---: | --- | +| `ORACLE_SOAK_DURATION_SECONDS` | `86400` | Monitored soak duration | +| `ORACLE_SOAK_POLL_SECONDS` | `15` | Onchain heartbeat interval | +| `ORACLE_SOAK_STALL_TIMEOUT_SECONDS` | `360` | Maximum feed or chain stall | +| `ORACLE_SOAK_MIN_ADVANCES` | 80% of expected minutes | Required Binance nonce advances | +| `ORACLE_SOAK_RESTART_AFTER_SECONDS` | Halfway for runs >= 1 hour | Restart time; `0` disables | +| `ORACLE_SOAK_RESTART_NODE` | `node4` | Validator selected for restart | +| `BINANCE_PRICE_FEED_BASE_URL` | `https://testnet.binancefuture.com` | Binance Futures testnet base URL | +| `BINANCE_PRICE_FEED_GRACE_MS` | `120000` | Closed-bucket safety delay | +| `POLYMARKET_GAMMA_URL` | `https://gamma-api.polymarket.com` | Market discovery API | + +## Evidence + +Runtime evidence remains local and ignored by Git: + +- `artifacts/oracle_live_soak_heartbeat.jsonl`: one bounded checkpoint per poll; +- `artifacts/oracle_live_soak_summary.json`: final PASS/FAIL report and restart + recovery time. + +The summary is the acceptance artifact. A process that is still running has +not yet passed the soak. diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/cluster.toml b/gravity_e2e/cluster_test_cases/oracle_live_soak/cluster.toml new file mode 100644 index 00000000..08ea537f --- /dev/null +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/cluster.toml @@ -0,0 +1,73 @@ +# Four-validator live Binance and Polymarket soak test. +# Keep fixed listeners below Linux's default ephemeral source-port range. + +[cluster] +name = "gravity-devnet-oracle-live-soak" +base_dir = "/tmp/gravity-cluster-oracle-live-soak" + +[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 = 26680 +vfn_port = 26690 +rpc_port = 26700 +api_port = 26710 +metrics_port = 26720 +inspection_port = 26730 +https_port = 26740 +authrpc_port = 26750 +reth_p2p_port = 26760 + +[[nodes]] +id = "node2" +role = "genesis" +source = { project_path = "../" } +host = "127.0.0.1" +validator_port = 26681 +vfn_port = 26691 +rpc_port = 26701 +api_port = 26711 +metrics_port = 26721 +inspection_port = 26731 +https_port = 26741 +authrpc_port = 26751 +reth_p2p_port = 26761 + +[[nodes]] +id = "node3" +role = "genesis" +source = { project_path = "../" } +host = "127.0.0.1" +validator_port = 26682 +vfn_port = 26692 +rpc_port = 26702 +api_port = 26712 +metrics_port = 26722 +inspection_port = 26732 +https_port = 26742 +authrpc_port = 26752 +reth_p2p_port = 26762 + +[[nodes]] +id = "node4" +role = "genesis" +source = { project_path = "../" } +host = "127.0.0.1" +validator_port = 26683 +vfn_port = 26693 +rpc_port = 26703 +api_port = 26713 +metrics_port = 26723 +inspection_port = 26733 +https_port = 26743 +authrpc_port = 26753 +reth_p2p_port = 26763 + +[faucet_init] +num_accounts = 0 diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/genesis.toml b/gravity_e2e/cluster_test_cases/oracle_live_soak/genesis.toml new file mode 100644 index 00000000..29581c70 --- /dev/null +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/genesis.toml @@ -0,0 +1,105 @@ +# Source types 3 and 6 are enabled at genesis, while their concrete tasks are +# registered by governance during the test. + +[dependencies.genesis_contracts] +repo = "https://github.com/Galxe/gravity_chain_core_contracts.git" +ref = "f447687bf6f6f9efabd92626b588aeda94148f88" + +# Equal voting power means a JWK QC requires matching signatures from at least +# three of the four validators. +[[genesis_validators]] +id = "node1" +address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +host = "127.0.0.1" +validator_port = 26680 +vfn_port = 26690 +stake_amount = "2000000000000000000" +voting_power = "2000000000000000000" +consensus_pop = "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +[[genesis_validators]] +id = "node2" +address = "0x7b254Bd44F6CE45e00a912b2460D47F3Be56fAD7" +host = "127.0.0.1" +validator_port = 26681 +vfn_port = 26691 +stake_amount = "2000000000000000000" +voting_power = "2000000000000000000" +consensus_pop = "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +[[genesis_validators]] +id = "node3" +address = "0x9B2C25E77a97d3e84DC0Cb7F83fb676ddC4F24b9" +host = "127.0.0.1" +validator_port = 26682 +vfn_port = 26692 +stake_amount = "2000000000000000000" +voting_power = "2000000000000000000" +consensus_pop = "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +[[genesis_validators]] +id = "node4" +address = "0x18c23753385ce7A60B15d171302E48b6AFf0BDC5" +host = "127.0.0.1" +validator_port = 26683 +vfn_port = 26693 +stake_amount = "2000000000000000000" +voting_power = "2000000000000000000" +consensus_pop = "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +[genesis] +chain_id = 1337 +epoch_interval_micros = 60000000 +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.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_live_soak/hooks.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/hooks.py new file mode 100644 index 00000000..02e7e08a --- /dev/null +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/hooks.py @@ -0,0 +1,359 @@ +"""Prepare live Binance and Polygon inputs before deploying the soak cluster.""" + +import ipaddress +import json +import logging +import os +from pathlib import Path +import time +from urllib.parse import urlencode, urlparse +from urllib.request import Request, urlopen + +try: + import tomllib +except ImportError: + import tomli as tomllib + + +LOG = logging.getLogger(__name__) + +INTERVAL_MS = 60_000 +DEFAULT_GRACE_MS = 120_000 +DEFAULT_BINANCE_BASE_URL = "https://testnet.binancefuture.com" +DEFAULT_GAMMA_URL = "https://gamma-api.polymarket.com" +NVDA_FEED_ID = 1001 +NVDA_PAIR = "NVDAUSDT" + +POLYGON_CHAIN_ID = 137 +CTF_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045" +CONDITION_RESOLUTION_TOPIC0 = ( + "0xb44d84d3289691f71497564b85d4233648d9dbae8cbdbb4329f301c3a0185894" +) +SCAN_CHUNK = 2_000 +MAX_SCAN_BLOCKS = 100_000 + +_METADATA_FILE = "oracle_live_soak_metadata.json" +_RELAYER_FILE = "relayer_config.live.json" +_HEARTBEAT_FILE = "oracle_live_soak_heartbeat.jsonl" +_SUMMARY_FILE = "oracle_live_soak_summary.json" + + +def _require_public_https_url(value: str, name: str) -> str: + parsed = urlparse(value) + if parsed.scheme != "https" or not parsed.hostname: + raise RuntimeError(f"{name} must be an https URL with a hostname") + hostname = parsed.hostname.lower() + if hostname == "localhost" or hostname.endswith(".localhost"): + raise RuntimeError(f"{name} must not use a loopback host") + try: + address = ipaddress.ip_address(hostname) + except ValueError: + address = None + if address is not None and not address.is_global: + raise RuntimeError(f"{name} must not use a private or loopback address") + return value.rstrip("/") + + +def _read_json(request: Request) -> object: + with urlopen(request, timeout=30) as response: + return json.loads(response.read()) + + +def _rpc(rpc_url: str, method: str, params: list): + payload = json.dumps( + {"jsonrpc": "2.0", "id": 1, "method": method, "params": params} + ).encode() + body = _read_json( + Request( + rpc_url, + data=payload, + headers={"Content-Type": "application/json"}, + ) + ) + if not isinstance(body, dict) or "result" not in body: + detail = body.get("error") if isinstance(body, dict) else body + raise RuntimeError(f"Polygon {method} failed: {detail}") + return body["result"] + + +def _gamma_markets(base_url: str) -> list[dict]: + query = urlencode( + { + "closed": "true", + "limit": 100, + "order": "closedTime", + "ascending": "false", + } + ) + markets = _read_json( + Request( + f"{base_url}/markets?{query}", + headers={ + "Accept": "application/json", + "User-Agent": "gravity-oracle-e2e/1.0", + }, + ) + ) + if not isinstance(markets, list): + raise RuntimeError("Polymarket Gamma response is not a market list") + return markets + + +def _parse_list(value) -> list: + if isinstance(value, str): + value = json.loads(value) + return list(value or []) + + +def _binary_markets(base_url: str) -> dict[str, dict]: + result = {} + for market in _gamma_markets(base_url): + condition_id = market.get("conditionId") + outcomes = _parse_list(market.get("outcomes")) + if ( + isinstance(condition_id, str) + and len(condition_id) == 66 + and condition_id.startswith("0x") + and len(outcomes) == 2 + and market.get("id") is not None + ): + result[condition_id.lower()] = {**market, "outcomes": outcomes} + if not result: + raise RuntimeError("Gamma returned no recently closed binary markets") + return result + + +def _decode_resolution(log: dict) -> tuple[int, list[int]]: + data = bytes.fromhex(log["data"].removeprefix("0x")) + if len(data) < 96: + raise ValueError("ConditionResolution data is too short") + outcome_count = int.from_bytes(data[0:32], "big") + offset = int.from_bytes(data[32:64], "big") + if offset + 32 > len(data): + raise ValueError("ConditionResolution payout offset is invalid") + array_len = int.from_bytes(data[offset : offset + 32], "big") + end = offset + 32 + array_len * 32 + if end > len(data): + raise ValueError("ConditionResolution payouts exceed encoded data") + payouts = [ + int.from_bytes( + data[offset + 32 + index * 32 : offset + 64 + index * 32], + "big", + ) + for index in range(array_len) + ] + if outcome_count != array_len: + raise ValueError("ConditionResolution outcome count does not match payouts") + return outcome_count, payouts + + +def _discover_market(rpc_url: str, gamma_url: str) -> dict: + chain_id = int(_rpc(rpc_url, "eth_chainId", []), 16) + if chain_id != POLYGON_CHAIN_ID: + raise RuntimeError(f"Polygon RPC chainId must be 137, got {chain_id}") + finalized = _rpc(rpc_url, "eth_getBlockByNumber", ["finalized", False]) + if not finalized: + raise RuntimeError("Polygon RPC did not return a finalized block") + finalized_block = int(finalized["number"], 16) + markets = _binary_markets(gamma_url) + + scanned = 0 + end = finalized_block + while end >= 0 and scanned < MAX_SCAN_BLOCKS: + start = max(0, end - SCAN_CHUNK + 1) + logs = _rpc( + rpc_url, + "eth_getLogs", + [ + { + "fromBlock": hex(start), + "toBlock": hex(end), + "address": CTF_ADDRESS, + "topics": [CONDITION_RESOLUTION_TOPIC0], + } + ], + ) + logs.sort( + key=lambda item: ( + int(item["blockNumber"], 16), + int(item["logIndex"], 16), + ), + reverse=True, + ) + for log in logs: + if log.get("removed") or len(log.get("topics", [])) < 4: + continue + condition_id = log["topics"][1].lower() + market = markets.get(condition_id) + if market is None: + continue + outcome_count, payouts = _decode_resolution(log) + if outcome_count != 2 or sum(value > 0 for value in payouts) != 1: + continue + receipt = _rpc( + rpc_url, + "eth_getTransactionReceipt", + [log["transactionHash"]], + ) + if not receipt or int(receipt["status"], 16) != 1: + continue + + block_number = int(log["blockNumber"], 16) + mirror_id = int(market["id"]) + task_uri = ( + f"gravity://6/{mirror_id}/polymarket_settlement?" + f"ctf={CTF_ADDRESS}&condition={condition_id}&" + f"fromBlock={max(0, block_number - 1)}&chainId=137&" + "maxBlocksPerPoll=100" + ) + return { + "mirrorId": mirror_id, + "question": market.get("question"), + "outcomes": market["outcomes"], + "conditionId": condition_id, + "questionId": log["topics"][3], + "oracle": "0x" + log["topics"][2][-40:], + "transactionHash": log["transactionHash"], + "blockNumber": block_number, + "finalizedBlock": finalized_block, + "logIndex": int(log["logIndex"], 16), + "outcomeSlotCount": outcome_count, + "payoutNumerators": payouts, + "winningSlot": next( + index for index, value in enumerate(payouts) if value > 0 + ), + "taskUri": task_uri, + } + + scanned += end - start + 1 + if start == 0: + break + end = start - 1 + + raise RuntimeError( + "no recently closed binary Polymarket market had a finalized CTF " + f"resolution in the last {MAX_SCAN_BLOCKS} Polygon blocks" + ) + + +def _bucket_start_ms(env: dict) -> int: + configured = env.get("BINANCE_PRICE_FEED_BUCKET_START_MS") + if configured: + bucket_start_ms = int(configured) + else: + grace_ms = int(env.get("BINANCE_PRICE_FEED_GRACE_MS", DEFAULT_GRACE_MS)) + minimum_lag = (grace_ms + INTERVAL_MS - 1) // INTERVAL_MS + 1 + lag_minutes = int( + env.get("BINANCE_PRICE_FEED_LAG_MINUTES", minimum_lag) + ) + bucket_start_ms = ( + int(time.time() * 1000) // INTERVAL_MS - lag_minutes + ) * INTERVAL_MS + if bucket_start_ms <= 0 or bucket_start_ms % INTERVAL_MS != 0: + raise RuntimeError( + "BINANCE_PRICE_FEED_BUCKET_START_MS must be a positive aligned minute" + ) + return bucket_start_ms + + +def _price_uri(bucket_start_ms: int, grace_ms: int) -> str: + return ( + f"gravity://3/{NVDA_FEED_ID}/price_feed?" + f"provider=binance_index_kline_v1&pair={NVDA_PAIR}&interval=1m&" + f"bucketStartMs={bucket_start_ms}&decimals=8&graceMs={grace_ms}" + ) + + +def pre_deploy(test_dir: Path, env: dict, pytest_args: list[str]): + binance_base_url = _require_public_https_url( + env.get("BINANCE_PRICE_FEED_BASE_URL", DEFAULT_BINANCE_BASE_URL), + "BINANCE_PRICE_FEED_BASE_URL", + ) + gamma_url = _require_public_https_url( + env.get("POLYMARKET_GAMMA_URL", DEFAULT_GAMMA_URL), + "POLYMARKET_GAMMA_URL", + ) + polygon_rpc_url = env.get("POLYGON_RPC_URL") or env.get( + "POLYGON_QUICKNONE_HTTP_URL" + ) + if not polygon_rpc_url: + raise RuntimeError( + "POLYGON_RPC_URL or POLYGON_QUICKNONE_HTTP_URL is required" + ) + polygon_rpc_url = _require_public_https_url( + polygon_rpc_url, "Polygon RPC URL" + ) + + grace_ms = int(env.get("BINANCE_PRICE_FEED_GRACE_MS", DEFAULT_GRACE_MS)) + if grace_ms < 0: + raise RuntimeError("BINANCE_PRICE_FEED_GRACE_MS must be non-negative") + bucket_start_ms = _bucket_start_ms(env) + price_uri = _price_uri(bucket_start_ms, grace_ms) + market = _discover_market(polygon_rpc_url, gamma_url) + + artifacts = test_dir / "artifacts" + artifacts.mkdir(parents=True, exist_ok=True) + for name in (_HEARTBEAT_FILE, _SUMMARY_FILE): + (artifacts / name).unlink(missing_ok=True) + + relayer_path = artifacts / _RELAYER_FILE + relayer_path.write_text( + json.dumps( + { + "uri_mappings": { + price_uri: binance_base_url, + market["taskUri"]: polygon_rpc_url, + } + }, + indent=2, + ) + + "\n" + ) + metadata_path = artifacts / _METADATA_FILE + metadata_path.write_text( + json.dumps( + { + "binance": { + "baseUrl": binance_base_url, + "feedId": NVDA_FEED_ID, + "pair": NVDA_PAIR, + "bucketStartMs": bucket_start_ms, + "graceMs": grace_ms, + "taskUri": price_uri, + }, + "polymarket": market, + }, + indent=2, + ensure_ascii=False, + ) + + "\n" + ) + env["RELAYER_CONFIG_TPL"] = str(relayer_path) + LOG.info( + "Prepared live Oracle inputs: Binance anchor=%s; Polymarket " + "mirrorId=%s block=%s finalized=%s", + bucket_start_ms, + market["mirrorId"], + market["blockNumber"], + market["finalizedBlock"], + ) + + +def post_stop(test_dir: Path, env: dict): + artifacts = test_dir / "artifacts" + for name in (_METADATA_FILE, _RELAYER_FILE): + (artifacts / name).unlink(missing_ok=True) + + cluster_config = Path( + env.get("GRAVITY_CLUSTER_CONFIG", test_dir / "cluster.toml") + ) + try: + with cluster_config.open("rb") as config_file: + config = tomllib.load(config_file) + base_dir = Path(config["cluster"]["base_dir"]) + for node in config.get("nodes", []): + (base_dir / node["id"] / "config" / "relayer_config.json").unlink( + missing_ok=True + ) + except (KeyError, OSError, tomllib.TOMLDecodeError) as error: + LOG.warning("Could not remove deployed live relayer configs: %s", error) diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py new file mode 100644 index 00000000..12101bfb --- /dev/null +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py @@ -0,0 +1,978 @@ +"""Manual four-validator soak against live Binance and Polygon data.""" + +import asyncio +from dataclasses import dataclass +from decimal import Decimal +import json +import logging +import os +from pathlib import Path +import time +from urllib.parse import urlencode +from urllib.request import urlopen + +import pytest +from web3 import Web3 + +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 + +INTERVAL_MS = 60_000 +DECIMALS = 8 +SOURCE_TYPE_POLYMARKET = 6 +POLYGON_CHAIN_ID = 137 +CTF_ADDRESS = Web3.to_checksum_address( + "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045" +) +POLYMARKET_TASK = Web3.keccak(text="polymarket_settlement") +RECONFIGURATION_ADDRESS = Web3.to_checksum_address( + "0x00000000000000000000000000000001625F2003" +) +SEL_CURRENT_EPOCH = Web3.keccak(text="currentEpoch()")[:4] +SEL_REMAINING_TIME = Web3.keccak(text="getRemainingTimeSeconds()")[:4] +CALLBACK_SUCCESS_TOPIC0 = Web3.keccak( + text="CallbackSuccess(uint32,uint256,uint128,address)" +).hex() + +DEFAULT_SOAK_SECONDS = 24 * 60 * 60 +DEFAULT_POLL_SECONDS = 15 +DEFAULT_STALL_SECONDS = 6 * 60 +EPOCH_TIMEOUT_SECONDS = 180 +ORACLE_TIMEOUT_SECONDS = 360 +MIN_PROPOSAL_WINDOW_SECONDS = 40 +QUORUM_VALIDATORS = 3 + +_HEARTBEAT_FILE = "oracle_live_soak_heartbeat.jsonl" +_SUMMARY_FILE = "oracle_live_soak_summary.json" + + +@dataclass(frozen=True) +class SoakSettings: + duration_seconds: int + poll_seconds: int + stall_seconds: int + minimum_advances: int + restart_after_seconds: int + restart_node: str + + +def _env_int(name: str, default: int, minimum: int = 0) -> int: + value = int(os.environ.get(name, str(default))) + if value < minimum: + raise ValueError(f"{name} must be at least {minimum}") + return value + + +def _soak_settings() -> SoakSettings: + duration = _env_int( + "ORACLE_SOAK_DURATION_SECONDS", DEFAULT_SOAK_SECONDS, minimum=1 + ) + poll_seconds = _env_int( + "ORACLE_SOAK_POLL_SECONDS", DEFAULT_POLL_SECONDS, minimum=1 + ) + stall_seconds = _env_int( + "ORACLE_SOAK_STALL_TIMEOUT_SECONDS", + DEFAULT_STALL_SECONDS, + minimum=poll_seconds * 2, + ) + configured_minimum = os.environ.get("ORACLE_SOAK_MIN_ADVANCES") + minimum_advances = ( + max(1, (duration // 60) * 8 // 10) + if configured_minimum is None and duration >= 120 + else int(configured_minimum or 0) + ) + if minimum_advances < 0: + raise ValueError("ORACLE_SOAK_MIN_ADVANCES must be non-negative") + + configured_restart = os.environ.get("ORACLE_SOAK_RESTART_AFTER_SECONDS") + restart_after = ( + duration // 2 + if configured_restart is None and duration >= 60 * 60 + else int(configured_restart or 0) + ) + if restart_after < 0 or restart_after >= duration: + raise ValueError( + "ORACLE_SOAK_RESTART_AFTER_SECONDS must be zero or less than duration" + ) + return SoakSettings( + duration_seconds=duration, + poll_seconds=poll_seconds, + stall_seconds=stall_seconds, + minimum_advances=minimum_advances, + restart_after_seconds=restart_after, + restart_node=os.environ.get("ORACLE_SOAK_RESTART_NODE", "node4"), + ) + + +def _metadata() -> dict: + path = SUITE_DIR / "artifacts" / "oracle_live_soak_metadata.json" + return json.loads(path.read_text()) + + +def _current_epoch(w3: Web3) -> int: + return int.from_bytes( + w3.eth.call({"to": RECONFIGURATION_ADDRESS, "data": SEL_CURRENT_EPOCH}), + "big", + ) + + +def _remaining_epoch_seconds(w3: Web3) -> int: + return int.from_bytes( + w3.eth.call( + {"to": RECONFIGURATION_ADDRESS, "data": SEL_REMAINING_TIME} + ), + "big", + ) + + +async def _wait_for_epoch_advance(cluster: Cluster, start_epoch: int) -> int: + deadline = time.monotonic() + EPOCH_TIMEOUT_SECONDS + node1 = cluster.get_node("node1") + while time.monotonic() < deadline: + epoch = _current_epoch(node1.w3) + if epoch > start_epoch: + for node_id, node in cluster.nodes.items(): + while _current_epoch(node.w3) < epoch: + if time.monotonic() >= deadline: + raise TimeoutError(f"{node_id} did not reach epoch {epoch}") + await asyncio.sleep(1) + return epoch + await asyncio.sleep(1) + raise TimeoutError(f"chain did not advance past epoch {start_epoch}") + + +async def _wait_for_proposal_window(cluster: Cluster) -> int: + node1 = cluster.get_node("node1") + deadline = time.monotonic() + EPOCH_TIMEOUT_SECONDS + while time.monotonic() < deadline: + remaining = _remaining_epoch_seconds(node1.w3) + if remaining >= MIN_PROPOSAL_WINDOW_SECONDS: + return _current_epoch(node1.w3) + current = _current_epoch(node1.w3) + LOG.info( + "Only %ss remain in epoch %s; waiting for a proposal window", + remaining, + current, + ) + await _wait_for_epoch_advance(cluster, current) + raise TimeoutError("no epoch had enough time for governance activation") + + +def _validator_log(cluster: Cluster, node_id: str) -> Path: + return cluster.base_dir / node_id / "consensus_log" / "validator.log" + + +def _log_contains(cluster: Cluster, node_id: str, marker: str, issuer: str) -> bool: + path = _validator_log(cluster, node_id) + content = path.read_text(errors="replace") if path.exists() else "" + return any(marker in line and issuer in line for line in content.splitlines()) + + +async def _wait_for_consensus_evidence( + cluster: Cluster, issuers: list[str] +) -> None: + missing_observers = { + (node_id, issuer) + for node_id in cluster.nodes + for issuer in issuers + } + certifiers = {issuer: set() for issuer in issuers} + missing_quorums = set(issuers) + deadline = time.monotonic() + ORACLE_TIMEOUT_SECONDS + while time.monotonic() < deadline: + logs = { + node_id: ( + _validator_log(cluster, node_id).read_text(errors="replace") + if _validator_log(cluster, node_id).exists() + else "" + ) + for node_id in cluster.nodes + } + for node_id, issuer in list(missing_observers): + if any( + "JWKObserver spawned." in line and issuer in line + for line in logs[node_id].splitlines() + ): + missing_observers.remove((node_id, issuer)) + for issuer in issuers: + for node_id, content in logs.items(): + lines = content.splitlines() + if any( + "Start certifying update." in line and issuer in line + for line in lines + ): + certifiers[issuer].add(node_id) + if any( + "Peer vote aggregated." in line + and issuer in line + and ( + "threshold_exceeded=true" in line + or '"threshold_exceeded":true' in line + ) + for line in lines + ): + missing_quorums.discard(issuer) + if ( + not missing_observers + and not missing_quorums + and all( + len(certifiers[issuer]) >= QUORUM_VALIDATORS + for issuer in issuers + ) + ): + return + await asyncio.sleep(2) + raise AssertionError( + "missing consensus evidence: " + f"observers={sorted(missing_observers)} " + f"certifiers={ {key: sorted(value) for key, value in certifiers.items()} } " + f"quorums={sorted(missing_quorums)}" + ) + + +async def _wait_for_block(w3: Web3, block_number: int, timeout: int = 90): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + try: + if w3.eth.block_number >= block_number: + return + except Exception: + pass + await asyncio.sleep(1) + raise TimeoutError(f"RPC did not reach Gravity block {block_number}") + + +async def _wait_for_settlement( + resolver, mirror_id: int, condition_id: bytes +) -> tuple: + deadline = time.monotonic() + ORACLE_TIMEOUT_SECONDS + while time.monotonic() < deadline: + settlement = tuple( + resolver.functions.getSettlement(mirror_id, condition_id).call() + ) + if settlement[0]: + return settlement + await asyncio.sleep(2) + raise TimeoutError(f"Polymarket mirror {mirror_id} was not settled") + + +def _topic(value: int) -> str: + return "0x" + value.to_bytes(32, "big").hex() + + +def _callback_success_logs( + w3: Web3, + source_type: int, + source_id: int, + from_block: int, + to_block: int | str = "latest", +) -> list: + return w3.eth.get_logs( + { + "fromBlock": from_block, + "toBlock": to_block, + "address": support.NATIVE_ORACLE_ADDRESS, + "topics": [ + CALLBACK_SUCCESS_TOPIC0, + _topic(source_type), + _topic(source_id), + ], + } + ) + + +def _round_start_ms(first_bucket_start_ms: int, delivery_nonce: int) -> int: + return first_bucket_start_ms + (delivery_nonce - 1) * INTERVAL_MS + + +def _fetch_binance_price( + base_url: str, pair: str, bucket_start_ms: int +) -> int: + bucket_end_ms = bucket_start_ms + INTERVAL_MS - 1 + query = urlencode( + { + "pair": pair, + "interval": "1m", + "startTime": bucket_start_ms, + "endTime": bucket_end_ms, + "limit": 1, + } + ) + with urlopen( + f"{base_url}/fapi/v1/indexPriceKlines?{query}", timeout=20 + ) as response: + rows = json.loads(response.read()) + assert len(rows) == 1, f"Binance returned {len(rows)} rows for {pair}" + assert int(rows[0][0]) == bucket_start_ms + assert int(rows[0][6]) == bucket_end_ms + scaled = Decimal(rows[0][4]) * (10**DECIMALS) + assert scaled == scaled.to_integral_value() + return int(scaled) + + +async def _fetch_binance_price_with_retry( + base_url: str, + pair: str, + bucket_start_ms: int, + timeout: int = 120, +) -> int: + deadline = time.monotonic() + timeout + last_error = None + while time.monotonic() < deadline: + try: + return await asyncio.to_thread( + _fetch_binance_price, base_url, pair, bucket_start_ms + ) + except (AssertionError, OSError, ValueError) as error: + last_error = error + await asyncio.sleep(5) + raise TimeoutError( + f"Binance did not return the exact closed {pair} bucket" + ) from last_error + + +def _relayer_sources(cluster: Cluster, node_id: str) -> dict: + path = cluster.base_dir / node_id / "data" / "reth" / "relayer_state.json" + if not path.exists(): + return {} + try: + return json.loads(path.read_text()).get("sources", {}) + except (OSError, json.JSONDecodeError): + return {} + + +def _relayer_quorum(cluster: Cluster, uri: str, minimum_nonce: int) -> list[str]: + reached = [] + for node_id in cluster.nodes: + raw_nonce = _relayer_sources(cluster, node_id).get(uri, {}).get("last_nonce") + try: + nonce = int(raw_nonce or 0) + except (TypeError, ValueError): + nonce = 0 + if nonce >= minimum_nonce: + reached.append(node_id) + return sorted(reached) + + +async def _wait_for_relayer_node( + cluster: Cluster, + node_id: str, + uri: str, + minimum_nonce: int, + timeout: int = 180, +) -> None: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + state = _relayer_sources(cluster, node_id).get(uri, {}) + try: + nonce = int(state.get("last_nonce") or 0) + except (TypeError, ValueError): + nonce = 0 + if nonce >= minimum_nonce: + return + await asyncio.sleep(2) + raise TimeoutError( + f"{node_id} relayer did not catch up to nonce {minimum_nonce}" + ) + + +async def _replicated_snapshot( + cluster: Cluster, + native_artifact: dict, + price_artifact: dict, + polymarket_artifact: dict, + price_resolver_address: str, + polymarket_resolver_address: str, + feed_id: int, + bucket_start_ms: int, + mirror_id: int, + condition_id: bytes, +) -> dict: + node1 = cluster.get_node("node1") + snapshot_block = node1.w3.eth.block_number + heights = {} + for node_id, node in cluster.nodes.items(): + assert node.w3.is_connected(), f"{node_id} RPC disconnected" + await _wait_for_block(node.w3, snapshot_block) + heights[node_id] = node.w3.eth.block_number + + native_oracle = node1.w3.eth.contract( + address=support.NATIVE_ORACLE_ADDRESS, abi=native_artifact["abi"] + ) + price_resolver = node1.w3.eth.contract( + address=price_resolver_address, abi=price_artifact["abi"] + ) + polymarket_resolver = node1.w3.eth.contract( + address=polymarket_resolver_address, abi=polymarket_artifact["abi"] + ) + price_progress = tuple( + native_oracle.functions.getSourceProgress( + support.SOURCE_TYPE_PRICE_FEED, feed_id + ).call(block_identifier=snapshot_block) + ) + latest_price = tuple( + price_resolver.functions.latestPrice(feed_id).call( + block_identifier=snapshot_block + ) + ) + polymarket_progress = tuple( + native_oracle.functions.getSourceProgress( + SOURCE_TYPE_POLYMARKET, mirror_id + ).call(block_identifier=snapshot_block) + ) + settlement = tuple( + polymarket_resolver.functions.getSettlement( + mirror_id, condition_id + ).call(block_identifier=snapshot_block) + ) + + assert price_progress[0] >= 1 + price_round_start = _round_start_ms(bucket_start_ms, price_progress[0]) + assert latest_price[0] + assert latest_price[1] == price_round_start // INTERVAL_MS + assert latest_price[2] == price_round_start + INTERVAL_MS - 1 + assert price_progress[1] == latest_price[2] + assert latest_price[3] == DECIMALS + assert latest_price[4] > 0 + assert polymarket_progress[0] == 1 + assert settlement[0] + + for node_id, node in cluster.nodes.items(): + replica_native = node.w3.eth.contract( + address=support.NATIVE_ORACLE_ADDRESS, abi=native_artifact["abi"] + ) + replica_price = node.w3.eth.contract( + address=price_resolver_address, abi=price_artifact["abi"] + ) + replica_polymarket = node.w3.eth.contract( + address=polymarket_resolver_address, + abi=polymarket_artifact["abi"], + ) + assert tuple( + replica_native.functions.getSourceProgress( + support.SOURCE_TYPE_PRICE_FEED, feed_id + ).call(block_identifier=snapshot_block) + ) == price_progress, f"{node_id} price progress diverged" + assert tuple( + replica_price.functions.latestPrice(feed_id).call( + block_identifier=snapshot_block + ) + ) == latest_price, f"{node_id} latest price diverged" + assert tuple( + replica_native.functions.getSourceProgress( + SOURCE_TYPE_POLYMARKET, mirror_id + ).call(block_identifier=snapshot_block) + ) == polymarket_progress, f"{node_id} Polymarket progress diverged" + assert tuple( + replica_polymarket.functions.getSettlement( + mirror_id, condition_id + ).call(block_identifier=snapshot_block) + ) == settlement, f"{node_id} Polymarket settlement diverged" + + return { + "block": snapshot_block, + "heights": heights, + "priceProgress": price_progress, + "latestPrice": latest_price, + "polymarketProgress": polymarket_progress, + "settlement": settlement, + } + + +def _append_json_line(path: Path, payload: dict) -> None: + with path.open("a") as output: + output.write(json.dumps(payload, sort_keys=True) + "\n") + + +async def _restart_validator( + cluster: Cluster, + node_id: str, + target_block: int, + uri: str, + target_nonce: int, +) -> float: + node = cluster.get_node(node_id) + if node is None: + raise AssertionError(f"unknown restart node {node_id}") + started = time.monotonic() + assert await node.restart(), f"failed to restart {node_id}" + await _wait_for_block(node.w3, target_block, timeout=180) + assert await cluster.check_block_increasing(timeout=60) + await _wait_for_relayer_node(cluster, node_id, uri, target_nonce) + return time.monotonic() - started + + +async def _run_soak( + cluster: Cluster, + settings: SoakSettings, + native_artifact: dict, + price_artifact: dict, + polymarket_artifact: dict, + price_resolver_address: str, + polymarket_resolver_address: str, + binance: dict, + polymarket: dict, + expected_settlement: tuple, +) -> dict: + feed_id = int(binance["feedId"]) + mirror_id = int(polymarket["mirrorId"]) + condition_id = bytes.fromhex(polymarket["conditionId"].removeprefix("0x")) + heartbeat_path = SUITE_DIR / "artifacts" / _HEARTBEAT_FILE + heartbeat_path.unlink(missing_ok=True) + + initial = await _replicated_snapshot( + cluster, + native_artifact, + price_artifact, + polymarket_artifact, + price_resolver_address, + polymarket_resolver_address, + feed_id, + int(binance["bucketStartMs"]), + mirror_id, + condition_id, + ) + initial_nonce = int(initial["priceProgress"][0]) + last_nonce = initial_nonce + started = time.monotonic() + deadline = started + settings.duration_seconds + last_price_advance = started + max_price_gap = 0.0 + last_height_change = {node_id: started for node_id in cluster.nodes} + previous_heights = dict(initial["heights"]) + restart_done = False + restart_recovery_seconds = None + samples = 0 + final = initial + + while time.monotonic() < deadline: + now = time.monotonic() + if ( + settings.restart_after_seconds + and not restart_done + and now - started >= settings.restart_after_seconds + ): + restart_recovery_seconds = await _restart_validator( + cluster, + settings.restart_node, + int(final["block"]), + binance["taskUri"], + int(final["priceProgress"][0]), + ) + restart_done = True + + final = await _replicated_snapshot( + cluster, + native_artifact, + price_artifact, + polymarket_artifact, + price_resolver_address, + polymarket_resolver_address, + feed_id, + int(binance["bucketStartMs"]), + mirror_id, + condition_id, + ) + now = time.monotonic() + nonce = int(final["priceProgress"][0]) + assert nonce >= last_nonce, "Binance source nonce regressed" + if nonce > last_nonce: + max_price_gap = max(max_price_gap, now - last_price_advance) + last_price_advance = now + last_nonce = nonce + assert now - last_price_advance <= settings.stall_seconds, ( + f"NVDA price feed stalled at nonce {nonce} for " + f"{now - last_price_advance:.1f}s" + ) + + for node_id, height in final["heights"].items(): + assert height >= previous_heights[node_id], ( + f"{node_id} block height regressed" + ) + if height > previous_heights[node_id]: + last_height_change[node_id] = now + previous_heights[node_id] = height + assert now - last_height_change[node_id] <= settings.stall_seconds, ( + f"{node_id} chain height stalled for " + f"{now - last_height_change[node_id]:.1f}s" + ) + + assert tuple(final["polymarketProgress"]) == ( + 1, + int(polymarket["blockNumber"]), + ) + assert tuple(final["settlement"]) == expected_settlement + relayer_nodes = _relayer_quorum(cluster, binance["taskUri"], nonce) + assert len(relayer_nodes) >= QUORUM_VALIDATORS, ( + f"only {relayer_nodes} persisted NVDA nonce {nonce}" + ) + + samples += 1 + heartbeat = { + "elapsedSeconds": round(now - started, 3), + "gravityBlock": int(final["block"]), + "nodeHeights": final["heights"], + "nvdaNonce": nonce, + "nvdaPosition": int(final["priceProgress"][1]), + "nvdaRound": int(final["latestPrice"][1]), + "nvdaPrice": str(final["latestPrice"][4]), + "relayerQuorumNodes": relayer_nodes, + "polymarketNonce": int(final["polymarketProgress"][0]), + "restartCompleted": restart_done, + } + _append_json_line(heartbeat_path, heartbeat) + LOG.info("Oracle soak heartbeat: %s", heartbeat) + await asyncio.sleep( + min(settings.poll_seconds, max(0.0, deadline - time.monotonic())) + ) + + final = await _replicated_snapshot( + cluster, + native_artifact, + price_artifact, + polymarket_artifact, + price_resolver_address, + polymarket_resolver_address, + feed_id, + int(binance["bucketStartMs"]), + mirror_id, + condition_id, + ) + final_nonce = int(final["priceProgress"][0]) + advances = final_nonce - initial_nonce + assert advances >= settings.minimum_advances, ( + f"NVDA advanced {advances} rounds; expected at least " + f"{settings.minimum_advances}" + ) + if settings.restart_after_seconds: + assert restart_done, "configured validator restart did not run" + await _wait_for_relayer_node( + cluster, + settings.restart_node, + binance["taskUri"], + final_nonce, + ) + + final_bucket = _round_start_ms( + int(binance["bucketStartMs"]), final_nonce + ) + max_price_gap = max(max_price_gap, time.monotonic() - last_price_advance) + expected_price = await _fetch_binance_price_with_retry( + binance["baseUrl"], binance["pair"], final_bucket + ) + assert int(final["latestPrice"][4]) == expected_price + return { + "status": "passed", + "configuredDurationSeconds": settings.duration_seconds, + "actualDurationSeconds": round(time.monotonic() - started, 3), + "samples": samples, + "initialNonce": initial_nonce, + "finalNonce": final_nonce, + "advances": advances, + "minimumAdvances": settings.minimum_advances, + "maxObservedPriceGapSeconds": round(max_price_gap, 3), + "finalGravityBlock": int(final["block"]), + "restartNode": ( + settings.restart_node if settings.restart_after_seconds else None + ), + "restartRecoverySeconds": restart_recovery_seconds, + "polymarketMirrorId": mirror_id, + "polymarketNonce": int(final["polymarketProgress"][0]), + } + + +def _write_summary(payload: dict) -> None: + path = SUITE_DIR / "artifacts" / _SUMMARY_FILE + path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + + +@pytest.mark.asyncio +async def test_governance_activated_oracles_soak_for_configured_duration( + cluster: Cluster, +): + settings = _soak_settings() + metadata = _metadata() + binance = metadata["binance"] + polymarket = metadata["polymarket"] + feed_id = int(binance["feedId"]) + mirror_id = int(polymarket["mirrorId"]) + condition_id = bytes.fromhex(polymarket["conditionId"].removeprefix("0x")) + + assert len(cluster.nodes) == 4 + assert await cluster.set_full_live(timeout=180) + assert await cluster.check_block_increasing(timeout=60) + active = await cluster.validator_list() + assert {node.id for node in active.active} == set(cluster.nodes) + + node1 = cluster.get_node("node1") + assert node1 is not None and node1.w3.is_connected() + w3 = node1.w3 + required = [ + ("PriceFeedResolver.sol", "PriceFeedResolver"), + ("PolymarketSettlementResolver.sol", "PolymarketSettlementResolver"), + ("NativeOracle.sol", "NativeOracle"), + ("OracleTaskConfig.sol", "OracleTaskConfig"), + ] + contracts_out = support.ensure_contract_artifacts(SUITE_DIR, required) + price_artifact = support.load_artifact( + contracts_out, "PriceFeedResolver.sol", "PriceFeedResolver" + ) + polymarket_artifact = support.load_artifact( + contracts_out, + "PolymarketSettlementResolver.sol", + "PolymarketSettlementResolver", + ) + 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_artifact) + polymarket_resolver = support.deploy_contract(w3, polymarket_artifact) + 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"] + ) + + assert not task_config.functions.hasTask( + support.SOURCE_TYPE_PRICE_FEED, feed_id, support.TASK_PRICE_FEED + ).call() + assert not task_config.functions.hasTask( + SOURCE_TYPE_POLYMARKET, mirror_id, POLYMARKET_TASK + ).call() + + setup_epoch = await _wait_for_proposal_window(cluster) + receipt = await support.execute_governance_proposal( + w3, + support.faucet_voting_pool(w3), + [ + support.NATIVE_ORACLE_ADDRESS, + support.ORACLE_TASK_CONFIG_ADDRESS, + support.ORACLE_TASK_CONFIG_ADDRESS, + support.NATIVE_ORACLE_ADDRESS, + polymarket_resolver.address, + ], + [ + support.function_calldata( + native_oracle.functions.setDefaultCallback( + support.SOURCE_TYPE_PRICE_FEED, price_resolver.address + ) + ), + support.function_calldata( + task_config.functions.setTask( + support.SOURCE_TYPE_PRICE_FEED, + feed_id, + support.TASK_PRICE_FEED, + binance["taskUri"].encode(), + ) + ), + support.function_calldata( + task_config.functions.setTask( + SOURCE_TYPE_POLYMARKET, + mirror_id, + POLYMARKET_TASK, + polymarket["taskUri"].encode(), + ) + ), + support.function_calldata( + native_oracle.functions.setCallback( + SOURCE_TYPE_POLYMARKET, + mirror_id, + polymarket_resolver.address, + ) + ), + support.function_calldata( + polymarket_resolver.functions.registerMirror( + mirror_id, + POLYGON_CHAIN_ID, + CTF_ADDRESS, + condition_id, + int(polymarket["outcomeSlotCount"]), + ) + ), + ], + "activate-live-binance-and-polymarket-soak", + gas=8_000_000, + ) + assert _current_epoch(w3) == setup_epoch + for source_type, source_id, task_name, expected_uri in ( + ( + support.SOURCE_TYPE_PRICE_FEED, + feed_id, + support.TASK_PRICE_FEED, + binance["taskUri"], + ), + ( + SOURCE_TYPE_POLYMARKET, + mirror_id, + POLYMARKET_TASK, + polymarket["taskUri"], + ), + ): + assert task_config.functions.hasTask( + source_type, source_id, task_name + ).call() + task = task_config.functions.getTask( + source_type, source_id, task_name + ).call() + assert bytes(task[0]).decode() == expected_uri + assert native_oracle.functions.getLatestNonce( + source_type, source_id + ).call() == 0 + + issuers = [f"gravity://3/{feed_id}", f"gravity://6/{mirror_id}"] + for node_id in cluster.nodes: + for issuer in issuers: + assert not _log_contains( + cluster, node_id, "JWKObserver spawned.", issuer + ), f"{node_id} started {issuer} before the epoch boundary" + + activation_epoch = await _wait_for_epoch_advance(cluster, setup_epoch) + assert activation_epoch == setup_epoch + 1 + price_progress, initial_price = await support.wait_for_latest_price( + native_oracle, + price_resolver, + feed_id, + 1, + timeout=ORACLE_TIMEOUT_SECONDS, + ) + deadline = time.monotonic() + ORACLE_TIMEOUT_SECONDS + while time.monotonic() < deadline: + polymarket_progress = tuple( + native_oracle.functions.getSourceProgress( + SOURCE_TYPE_POLYMARKET, mirror_id + ).call() + ) + if polymarket_progress[0] >= 1: + break + await asyncio.sleep(2) + else: + raise TimeoutError("Polymarket source did not reach nonce 1") + + assert polymarket_progress == (1, int(polymarket["blockNumber"])) + settlement = await _wait_for_settlement( + polymarket_resolver, mirror_id, condition_id + ) + assert settlement[1] == 1 + assert settlement[2] == POLYGON_CHAIN_ID + assert Web3.to_checksum_address(settlement[3]) == CTF_ADDRESS + assert Web3.to_checksum_address(settlement[4]) == Web3.to_checksum_address( + polymarket["oracle"] + ) + assert Web3.to_hex(settlement[5]).lower() == polymarket["questionId"].lower() + assert settlement[6] == int(polymarket["outcomeSlotCount"]) + assert Web3.to_hex(settlement[7]).lower() == polymarket[ + "transactionHash" + ].lower() + assert settlement[8] == int(polymarket["logIndex"]) + observation = tuple( + polymarket_resolver.functions.getSettlementObservation( + mirror_id, condition_id + ).call() + ) + assert observation[0] == 1 + expected_winning_slot = next( + index + for index, payout in enumerate(polymarket["payoutNumerators"]) + if payout > 0 + ) + assert observation[1] == expected_winning_slot + assert observation[2] == 1 + assert Web3.to_hex(observation[4]).lower() == polymarket[ + "transactionHash" + ].lower() + assert observation[5] == int(polymarket["logIndex"]) + resolution_events = ( + polymarket_resolver.events.PolymarketConditionResolved().get_logs( + from_block=receipt["blockNumber"], + to_block="latest", + argument_filters={ + "mirrorId": mirror_id, + "conditionId": condition_id, + }, + ) + ) + assert len(resolution_events) == 1 + assert list(resolution_events[0]["args"]["payoutNumerators"]) == polymarket[ + "payoutNumerators" + ] + + first_bucket = _round_start_ms( + int(binance["bucketStartMs"]), int(price_progress[0]) + ) + expected_initial_price = await _fetch_binance_price_with_retry( + binance["baseUrl"], binance["pair"], first_bucket + ) + assert int(initial_price[4]) == expected_initial_price + await _wait_for_consensus_evidence(cluster, issuers) + assert len( + _relayer_quorum(cluster, binance["taskUri"], int(price_progress[0])) + ) >= QUORUM_VALIDATORS + + try: + summary = await _run_soak( + cluster, + settings, + native_artifact, + price_artifact, + polymarket_artifact, + price_resolver.address, + polymarket_resolver.address, + binance, + polymarket, + settlement, + ) + except BaseException as error: + _write_summary( + { + "status": "failed", + "errorType": type(error).__name__, + "error": str(error), + "configuredDurationSeconds": settings.duration_seconds, + "polymarketMirrorId": mirror_id, + } + ) + raise + + price_delivery_count = len( + _callback_success_logs( + w3, + support.SOURCE_TYPE_PRICE_FEED, + feed_id, + receipt["blockNumber"], + summary["finalGravityBlock"], + ) + ) + assert price_delivery_count == summary["finalNonce"], ( + "NVDA callback count does not match final source nonce" + ) + polymarket_delivery_count = len( + _callback_success_logs( + w3, + SOURCE_TYPE_POLYMARKET, + mirror_id, + receipt["blockNumber"], + summary["finalGravityBlock"], + ) + ) + assert polymarket_delivery_count == 1, ( + "Polymarket settlement was delivered more than once" + ) + summary.update( + { + "activationEpoch": activation_epoch, + "governanceBlock": receipt["blockNumber"], + "priceCallbackEvents": price_delivery_count, + "polymarketCallbackEvents": polymarket_delivery_count, + } + ) + _write_summary(summary) + LOG.info("Oracle live soak passed: %s", summary) diff --git a/gravity_e2e/gravity_e2e/utils/oracle_test_support.py b/gravity_e2e/gravity_e2e/utils/oracle_test_support.py index ddd0ff47..d0d3e164 100644 --- a/gravity_e2e/gravity_e2e/utils/oracle_test_support.py +++ b/gravity_e2e/gravity_e2e/utils/oracle_test_support.py @@ -219,6 +219,8 @@ async def execute_governance_proposal( targets: list[str], datas: list[bytes], description: str, + *, + gas: int = 5_000_000, ) -> dict: assert len(targets) == len(datas) and targets _ensure_faucet_executor(w3) @@ -230,9 +232,9 @@ async def execute_governance_proposal( "from": FAUCET_ADDRESS, "to": GOVERNANCE_ADDRESS, "data": create_data, - "gas": 5_000_000, + "gas": gas, }) - receipt = _send_tx(w3, GOVERNANCE_ADDRESS, create_data, gas=5_000_000) + receipt = _send_tx(w3, GOVERNANCE_ADDRESS, create_data, gas=gas) proposal_id = next( ( int.from_bytes(log["topics"][1], "big") @@ -274,7 +276,7 @@ async def execute_governance_proposal( ["uint64", "address[]", "bytes[]"], [proposal_id, targets, datas], ) - return _send_tx(w3, GOVERNANCE_ADDRESS, execute_data, gas=5_000_000) + return _send_tx(w3, GOVERNANCE_ADDRESS, execute_data, gas=gas) async def wait_for_latest_price( diff --git a/gravity_e2e/runner.py b/gravity_e2e/runner.py index a20dbeb5..737d16d2 100644 --- a/gravity_e2e/runner.py +++ b/gravity_e2e/runner.py @@ -328,6 +328,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, @@ -439,7 +443,7 @@ def main(): ) # Long-running suites excluded from CI by default. # Run them explicitly: runner.py long_test - DEFAULT_EXCLUDES = ["long_test", "rolling_upgrade"] + DEFAULT_EXCLUDES = ["long_test", "rolling_upgrade", "oracle_live_soak"] parser.add_argument( "--exclude", From 0e9f9cd5f8d496eafc06f810afd0d70b918e604c Mon Sep 17 00:00:00 2001 From: ByteYue Date: Tue, 4 Aug 2026 03:58:46 +0800 Subject: [PATCH 02/12] test(oracle): stabilize live soak epochs --- .../oracle_live_soak/README.md | 14 +++- .../oracle_live_soak/test_oracle_live_soak.py | 66 ++++++++++++++++++- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md index f7ce3a47..90534d1b 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md @@ -7,9 +7,16 @@ This manual, non-gating suite runs four equal-power Gravity validators against: finalized on Polygon (`sourceType=6`). The suite discovers the live inputs before deployment, then submits one Gravity -governance proposal that registers both tasks and their resolver callbacks. It -proves that observers remain absent in proposal epoch `E`, start in `E+1`, and -reach a three-of-four JWK quorum before the soak timer begins. +governance proposal that registers both tasks and their resolver callbacks. The +same proposal changes the epoch interval from the 60-second bootstrap value to +two hours for `E+1`. It proves that observers remain absent in proposal epoch +`E`, start in `E+1`, and reach a three-of-four JWK quorum before the soak timer +begins. + +The short bootstrap epoch keeps dynamic task activation fast. The two-hour soak +epoch intentionally follows the repository's regular four-validator topology: +a 24-hour Oracle soak should exercise roughly 12 epoch transitions, rather than +also acting as a 1,440-transition consensus reconfiguration stress test. This is operational evidence, not merge-gating CI. The deterministic Binance and Polymarket suites remain the reproducible CI coverage. @@ -24,6 +31,7 @@ and Polymarket suites remain the reproducible CI coverage. budget; - at least three relayers have checkpointed the committed Binance nonce; - the finalized Polymarket settlement remains immutable at nonce `1`. +- the governance-pending two-hour epoch interval is active before timing starts. At completion, the suite fetches the exact final Binance bucket again and compares its close with the onchain value. It also counts NativeOracle callback diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py index 12101bfb..f7d8dd77 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py @@ -32,6 +32,9 @@ RECONFIGURATION_ADDRESS = Web3.to_checksum_address( "0x00000000000000000000000000000001625F2003" ) +EPOCH_CONFIG_ADDRESS = Web3.to_checksum_address( + "0x00000000000000000000000000000001625F1005" +) SEL_CURRENT_EPOCH = Web3.keccak(text="currentEpoch()")[:4] SEL_REMAINING_TIME = Web3.keccak(text="getRemainingTimeSeconds()")[:4] CALLBACK_SUCCESS_TOPIC0 = Web3.keccak( @@ -45,6 +48,8 @@ ORACLE_TIMEOUT_SECONDS = 360 MIN_PROPOSAL_WINDOW_SECONDS = 40 QUORUM_VALIDATORS = 3 +BOOTSTRAP_EPOCH_INTERVAL_MICROS = 60 * 1_000_000 +SOAK_EPOCH_INTERVAL_MICROS = 2 * 60 * 60 * 1_000_000 _HEARTBEAT_FILE = "oracle_live_soak_heartbeat.jsonl" _SUMMARY_FILE = "oracle_live_soak_summary.json" @@ -234,7 +239,9 @@ async def _wait_for_consensus_evidence( ) -async def _wait_for_block(w3: Web3, block_number: int, timeout: int = 90): +async def _wait_for_block( + node_id: str, w3: Web3, block_number: int, timeout: int = 90 +): deadline = time.monotonic() + timeout while time.monotonic() < deadline: try: @@ -243,7 +250,15 @@ async def _wait_for_block(w3: Web3, block_number: int, timeout: int = 90): except Exception: pass await asyncio.sleep(1) - raise TimeoutError(f"RPC did not reach Gravity block {block_number}") + last_height = None + try: + last_height = w3.eth.block_number + except Exception: + pass + raise TimeoutError( + f"{node_id} RPC did not reach Gravity block {block_number}; " + f"last height was {last_height}" + ) async def _wait_for_settlement( @@ -397,7 +412,7 @@ async def _replicated_snapshot( heights = {} for node_id, node in cluster.nodes.items(): assert node.w3.is_connected(), f"{node_id} RPC disconnected" - await _wait_for_block(node.w3, snapshot_block) + await _wait_for_block(node_id, node.w3, snapshot_block) heights[node_id] = node.w3.eth.block_number native_oracle = node1.w3.eth.contract( @@ -716,6 +731,7 @@ async def test_governance_activated_oracles_soak_for_configured_duration( ("PolymarketSettlementResolver.sol", "PolymarketSettlementResolver"), ("NativeOracle.sol", "NativeOracle"), ("OracleTaskConfig.sol", "OracleTaskConfig"), + ("EpochConfig.sol", "EpochConfig"), ] contracts_out = support.ensure_contract_artifacts(SUITE_DIR, required) price_artifact = support.load_artifact( @@ -732,6 +748,9 @@ async def test_governance_activated_oracles_soak_for_configured_duration( task_artifact = support.load_artifact( contracts_out, "OracleTaskConfig.sol", "OracleTaskConfig" ) + epoch_artifact = support.load_artifact( + contracts_out, "EpochConfig.sol", "EpochConfig" + ) price_resolver = support.deploy_contract(w3, price_artifact) polymarket_resolver = support.deploy_contract(w3, polymarket_artifact) @@ -741,6 +760,15 @@ async def test_governance_activated_oracles_soak_for_configured_duration( task_config = w3.eth.contract( address=support.ORACLE_TASK_CONFIG_ADDRESS, abi=task_artifact["abi"] ) + epoch_config = w3.eth.contract( + address=EPOCH_CONFIG_ADDRESS, abi=epoch_artifact["abi"] + ) + + assert ( + epoch_config.functions.epochIntervalMicros().call() + == BOOTSTRAP_EPOCH_INTERVAL_MICROS + ) + assert tuple(epoch_config.functions.getPendingConfig().call()) == (False, 0) assert not task_config.functions.hasTask( support.SOURCE_TYPE_PRICE_FEED, feed_id, support.TASK_PRICE_FEED @@ -754,6 +782,7 @@ async def test_governance_activated_oracles_soak_for_configured_duration( w3, support.faucet_voting_pool(w3), [ + EPOCH_CONFIG_ADDRESS, support.NATIVE_ORACLE_ADDRESS, support.ORACLE_TASK_CONFIG_ADDRESS, support.ORACLE_TASK_CONFIG_ADDRESS, @@ -761,6 +790,11 @@ async def test_governance_activated_oracles_soak_for_configured_duration( polymarket_resolver.address, ], [ + support.function_calldata( + epoch_config.functions.setForNextEpoch( + SOAK_EPOCH_INTERVAL_MICROS + ) + ), support.function_calldata( native_oracle.functions.setDefaultCallback( support.SOURCE_TYPE_PRICE_FEED, price_resolver.address @@ -803,6 +837,14 @@ async def test_governance_activated_oracles_soak_for_configured_duration( gas=8_000_000, ) assert _current_epoch(w3) == setup_epoch + assert ( + epoch_config.functions.epochIntervalMicros().call() + == BOOTSTRAP_EPOCH_INTERVAL_MICROS + ) + assert tuple(epoch_config.functions.getPendingConfig().call()) == ( + True, + SOAK_EPOCH_INTERVAL_MICROS, + ) for source_type, source_id, task_name, expected_uri in ( ( support.SOURCE_TYPE_PRICE_FEED, @@ -837,6 +879,14 @@ async def test_governance_activated_oracles_soak_for_configured_duration( activation_epoch = await _wait_for_epoch_advance(cluster, setup_epoch) assert activation_epoch == setup_epoch + 1 + assert ( + epoch_config.functions.epochIntervalMicros().call() + == SOAK_EPOCH_INTERVAL_MICROS + ) + assert tuple(epoch_config.functions.getPendingConfig().call()) == (False, 0) + assert _remaining_epoch_seconds(w3) > ( + SOAK_EPOCH_INTERVAL_MICROS // 1_000_000 - 300 + ) price_progress, initial_price = await support.wait_for_latest_price( native_oracle, price_resolver, @@ -931,6 +981,12 @@ async def test_governance_activated_oracles_soak_for_configured_duration( settlement, ) except BaseException as error: + last_heartbeat = None + heartbeat_path = SUITE_DIR / "artifacts" / _HEARTBEAT_FILE + if heartbeat_path.exists(): + lines = heartbeat_path.read_text().splitlines() + if lines: + last_heartbeat = json.loads(lines[-1]) _write_summary( { "status": "failed", @@ -938,6 +994,7 @@ async def test_governance_activated_oracles_soak_for_configured_duration( "error": str(error), "configuredDurationSeconds": settings.duration_seconds, "polymarketMirrorId": mirror_id, + "lastHeartbeat": last_heartbeat, } ) raise @@ -972,6 +1029,9 @@ async def test_governance_activated_oracles_soak_for_configured_duration( "governanceBlock": receipt["blockNumber"], "priceCallbackEvents": price_delivery_count, "polymarketCallbackEvents": polymarket_delivery_count, + "soakEpochIntervalSeconds": ( + SOAK_EPOCH_INTERVAL_MICROS // 1_000_000 + ), } ) _write_summary(summary) From fd3c38b190da0c58dce24255ed6b6c9b92a87919 Mon Sep 17 00:00:00 2001 From: ByteYue Date: Tue, 4 Aug 2026 10:08:39 +0800 Subject: [PATCH 03/12] test(oracle): stabilize replica snapshots --- .../oracle_live_soak/README.md | 6 ++- .../oracle_live_soak/test_oracle_live_soak.py | 40 ++++++++++++++++--- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md index 90534d1b..3f73764f 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md @@ -23,8 +23,10 @@ and Polymarket suites remain the reproducible CI coverage. ## What Every Heartbeat Checks -- all four Gravity RPC replicas can serve one common snapshot block; -- each replica returns the same NativeOracle progress and resolver state; +- all four Gravity RPC replicas expose the same block hash at the common + confirmed height `min(latest node heights) - 16`; +- each replica returns the same NativeOracle progress and resolver state at + that historical block, outside the latest-block execution window; - Binance delivery nonce, source position, and resolver round never regress; - the latest resolver value maps to the exact closed one-minute bucket; - the Binance feed and every Gravity node stay within the configured stall diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py index f7d8dd77..61f7610b 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py @@ -50,6 +50,7 @@ QUORUM_VALIDATORS = 3 BOOTSTRAP_EPOCH_INTERVAL_MICROS = 60 * 1_000_000 SOAK_EPOCH_INTERVAL_MICROS = 2 * 60 * 60 * 1_000_000 +SNAPSHOT_CONFIRMATION_BLOCKS = 16 _HEARTBEAT_FILE = "oracle_live_soak_heartbeat.jsonl" _SUMMARY_FILE = "oracle_live_soak_summary.json" @@ -407,14 +408,29 @@ async def _replicated_snapshot( mirror_id: int, condition_id: bytes, ) -> dict: - node1 = cluster.get_node("node1") - snapshot_block = node1.w3.eth.block_number heights = {} for node_id, node in cluster.nodes.items(): assert node.w3.is_connected(), f"{node_id} RPC disconnected" - await _wait_for_block(node_id, node.w3, snapshot_block) heights[node_id] = node.w3.eth.block_number + minimum_height = min(heights.values()) + assert minimum_height >= SNAPSHOT_CONFIRMATION_BLOCKS, ( + "Gravity chain is too young for a confirmed Oracle snapshot" + ) + snapshot_block = minimum_height - SNAPSHOT_CONFIRMATION_BLOCKS + snapshot_hash = None + for node_id, node in cluster.nodes.items(): + block = node.w3.eth.get_block(snapshot_block) + assert int(block["number"]) == snapshot_block + block_hash = Web3.to_hex(block["hash"]).lower() + if snapshot_hash is None: + snapshot_hash = block_hash + assert block_hash == snapshot_hash, ( + f"{node_id} block hash diverged at Gravity block {snapshot_block}" + ) + + node1 = cluster.get_node("node1") + native_oracle = node1.w3.eth.contract( address=support.NATIVE_ORACLE_ADDRESS, abi=native_artifact["abi"] ) @@ -490,6 +506,7 @@ async def _replicated_snapshot( return { "block": snapshot_block, + "blockHash": snapshot_hash, "heights": heights, "priceProgress": price_progress, "latestPrice": latest_price, @@ -515,7 +532,7 @@ async def _restart_validator( raise AssertionError(f"unknown restart node {node_id}") started = time.monotonic() assert await node.restart(), f"failed to restart {node_id}" - await _wait_for_block(node.w3, target_block, timeout=180) + await _wait_for_block(node_id, node.w3, target_block, timeout=180) assert await cluster.check_block_increasing(timeout=60) await _wait_for_relayer_node(cluster, node_id, uri, target_nonce) return time.monotonic() - started @@ -539,6 +556,17 @@ async def _run_soak( heartbeat_path = SUITE_DIR / "artifacts" / _HEARTBEAT_FILE heartbeat_path.unlink(missing_ok=True) + initial_tip = max( + node.w3.eth.block_number for node in cluster.nodes.values() + ) + confirmation_target = ( + initial_tip + SNAPSHOT_CONFIRMATION_BLOCKS + 1 + ) + for node_id, node in cluster.nodes.items(): + await _wait_for_block( + node_id, node.w3, confirmation_target, timeout=90 + ) + initial = await _replicated_snapshot( cluster, native_artifact, @@ -574,7 +602,7 @@ async def _run_soak( restart_recovery_seconds = await _restart_validator( cluster, settings.restart_node, - int(final["block"]), + max(int(height) for height in final["heights"].values()), binance["taskUri"], int(final["priceProgress"][0]), ) @@ -630,6 +658,7 @@ async def _run_soak( heartbeat = { "elapsedSeconds": round(now - started, 3), "gravityBlock": int(final["block"]), + "gravityBlockHash": final["blockHash"], "nodeHeights": final["heights"], "nvdaNonce": nonce, "nvdaPosition": int(final["priceProgress"][1]), @@ -691,6 +720,7 @@ async def _run_soak( "minimumAdvances": settings.minimum_advances, "maxObservedPriceGapSeconds": round(max_price_gap, 3), "finalGravityBlock": int(final["block"]), + "finalGravityBlockHash": final["blockHash"], "restartNode": ( settings.restart_node if settings.restart_after_seconds else None ), From 9c9990a3343a80c2d1b9cfafe5794192829483d9 Mon Sep 17 00:00:00 2001 From: ByteYue Date: Tue, 4 Aug 2026 12:21:37 +0800 Subject: [PATCH 04/12] test(oracle): bind soak reads to canonical block hash --- .../oracle_live_soak/README.md | 3 +- .../oracle_live_soak/test_oracle_live_soak.py | 90 ++++++++++++++----- 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md index 3f73764f..56e33a51 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md @@ -26,7 +26,8 @@ and Polymarket suites remain the reproducible CI coverage. - all four Gravity RPC replicas expose the same block hash at the common confirmed height `min(latest node heights) - 16`; - each replica returns the same NativeOracle progress and resolver state at - that historical block, outside the latest-block execution window; + that exact EIP-1898 canonical block hash, outside number-to-state view + transitions near the execution head; - Binance delivery nonce, source position, and resolver round never regress; - the latest resolver value maps to the exact closed one-minute bucket; - the Binance feed and every Gravity node stay within the configured stall diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py index 61f7610b..e7ae964e 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py @@ -3,6 +3,8 @@ import asyncio from dataclasses import dataclass from decimal import Decimal +from eth_utils.abi import get_abi_output_types +from hexbytes import HexBytes import json import logging import os @@ -13,6 +15,8 @@ import pytest from web3 import Web3 +from web3._utils.abi import map_abi_data +from web3._utils.normalizers import BASE_RETURN_NORMALIZERS from gravity_e2e.cluster.manager import Cluster from gravity_e2e.utils import oracle_test_support as support @@ -276,6 +280,30 @@ async def _wait_for_settlement( raise TimeoutError(f"Polymarket mirror {mirror_id} was not settled") +def _call_at_block_hash(function, block_hash: str): + response = function.w3.provider.make_request( + "eth_call", + [ + { + "to": function.address, + "data": function._encode_transaction_data(), + }, + {"blockHash": block_hash, "requireCanonical": True}, + ], + ) + assert "error" not in response, ( + f"EIP-1898 eth_call failed at {block_hash}: {response['error']}" + ) + output_types = get_abi_output_types(function.abi) + decoded = function.w3.codec.decode( + output_types, HexBytes(response["result"]) + ) + normalized = map_abi_data( + BASE_RETURN_NORMALIZERS, output_types, decoded + ) + return normalized[0] if len(normalized) == 1 else normalized + + def _topic(value: int) -> str: return "0x" + value.to_bytes(32, "big").hex() @@ -441,24 +469,33 @@ async def _replicated_snapshot( address=polymarket_resolver_address, abi=polymarket_artifact["abi"] ) price_progress = tuple( - native_oracle.functions.getSourceProgress( - support.SOURCE_TYPE_PRICE_FEED, feed_id - ).call(block_identifier=snapshot_block) + _call_at_block_hash( + native_oracle.functions.getSourceProgress( + support.SOURCE_TYPE_PRICE_FEED, feed_id + ), + snapshot_hash, + ) ) latest_price = tuple( - price_resolver.functions.latestPrice(feed_id).call( - block_identifier=snapshot_block + _call_at_block_hash( + price_resolver.functions.latestPrice(feed_id), snapshot_hash ) ) polymarket_progress = tuple( - native_oracle.functions.getSourceProgress( - SOURCE_TYPE_POLYMARKET, mirror_id - ).call(block_identifier=snapshot_block) + _call_at_block_hash( + native_oracle.functions.getSourceProgress( + SOURCE_TYPE_POLYMARKET, mirror_id + ), + snapshot_hash, + ) ) settlement = tuple( - polymarket_resolver.functions.getSettlement( - mirror_id, condition_id - ).call(block_identifier=snapshot_block) + _call_at_block_hash( + polymarket_resolver.functions.getSettlement( + mirror_id, condition_id + ), + snapshot_hash, + ) ) assert price_progress[0] >= 1 @@ -484,24 +521,33 @@ async def _replicated_snapshot( abi=polymarket_artifact["abi"], ) assert tuple( - replica_native.functions.getSourceProgress( - support.SOURCE_TYPE_PRICE_FEED, feed_id - ).call(block_identifier=snapshot_block) + _call_at_block_hash( + replica_native.functions.getSourceProgress( + support.SOURCE_TYPE_PRICE_FEED, feed_id + ), + snapshot_hash, + ) ) == price_progress, f"{node_id} price progress diverged" assert tuple( - replica_price.functions.latestPrice(feed_id).call( - block_identifier=snapshot_block + _call_at_block_hash( + replica_price.functions.latestPrice(feed_id), snapshot_hash ) ) == latest_price, f"{node_id} latest price diverged" assert tuple( - replica_native.functions.getSourceProgress( - SOURCE_TYPE_POLYMARKET, mirror_id - ).call(block_identifier=snapshot_block) + _call_at_block_hash( + replica_native.functions.getSourceProgress( + SOURCE_TYPE_POLYMARKET, mirror_id + ), + snapshot_hash, + ) ) == polymarket_progress, f"{node_id} Polymarket progress diverged" assert tuple( - replica_polymarket.functions.getSettlement( - mirror_id, condition_id - ).call(block_identifier=snapshot_block) + _call_at_block_hash( + replica_polymarket.functions.getSettlement( + mirror_id, condition_id + ), + snapshot_hash, + ) ) == settlement, f"{node_id} Polymarket settlement diverged" return { From fae8fca461283c2923b5648806e7d120994453f1 Mon Sep 17 00:00:00 2001 From: ByteYue Date: Tue, 4 Aug 2026 15:49:40 +0800 Subject: [PATCH 05/12] test(oracle): soak multiple Binance feeds --- .../oracle_live_soak/README.md | 42 +- .../oracle_live_soak/hooks.py | 45 +- .../oracle_live_soak/test_oracle_live_soak.py | 458 ++++++++++++------ 3 files changed, 352 insertions(+), 193 deletions(-) diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md index 56e33a51..330d5b95 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md @@ -2,7 +2,8 @@ This manual, non-gating suite runs four equal-power Gravity validators against: -- one continuous Binance `NVDAUSDT` closed index-price kline feed (`sourceType=3`); +- three continuous Binance `NVDAUSDT`, `BTCUSDT`, and `ETHUSDT` closed + index-price kline feeds (`sourceType=3`, feed IDs `1001` through `1003`); - one recently closed binary Polymarket market whose CTF resolution is already finalized on Polygon (`sourceType=6`). @@ -28,18 +29,26 @@ and Polymarket suites remain the reproducible CI coverage. - each replica returns the same NativeOracle progress and resolver state at that exact EIP-1898 canonical block hash, outside number-to-state view transitions near the execution head; -- Binance delivery nonce, source position, and resolver round never regress; -- the latest resolver value maps to the exact closed one-minute bucket; -- the Binance feed and every Gravity node stay within the configured stall - budget; -- at least three relayers have checkpointed the committed Binance nonce; +- each price snapshot uses a progress/resolver/progress seqlock read so an RPC + view transition between cross-contract calls is retried, not mistaken for + inconsistent Oracle state; +- every Binance feed's delivery nonce, source position, and resolver round never + regress independently; +- every latest resolver value maps to that pair's exact closed one-minute + bucket; +- all three Binance feeds and every Gravity node stay within the configured + stall budget; +- at least three relayers have checkpointed each committed Binance nonce; - the finalized Polymarket settlement remains immutable at nonce `1`. - the governance-pending two-hour epoch interval is active before timing starts. -At completion, the suite fetches the exact final Binance bucket again and -compares its close with the onchain value. It also counts NativeOracle callback -successes: the Binance count must equal its final delivery nonce, while the -Polymarket settlement must have exactly one successful delivery. +At completion, the suite fetches each exact final Binance bucket again and +compares its close with the corresponding onchain value. It also counts +NativeOracle callback successes: each pair's count must equal its independent +final delivery nonce, while the Polymarket settlement must have exactly one +successful delivery. Heartbeats and the final summary report observed price +changes per pair; price changes are informational because timely identical +index closes are valid Oracle updates. For runs of at least one hour, `node4` restarts halfway through by default. The suite requires its chain and relayer checkpoints to catch up without nonce @@ -54,11 +63,12 @@ regression or a duplicate Polymarket delivery. - `POLYGON_RPC_URL` or `POLYGON_QUICKNONE_HTTP_URL` set to an HTTPS Polygon RPC. The Binance Futures testnet `indexPriceKlines` endpoint used here is public and -does not require an API key. It exercises a live HTTP source and the complete -validator consensus path, but its values are testnet index data and must be -labeled that way in a demo. Do not put RPC credentials in committed files. The -generated relayer mapping and source metadata live under the ignored suite -`artifacts/` directory and are removed during normal teardown. +does not require an API key. Each validator independently fetches all three +pairs. This exercises multiple live HTTP tasks and the complete validator +consensus path, but the values are testnet index data and must be labeled that +way in a demo. Do not put RPC credentials in committed files. The generated +relayer mapping and source metadata live under the ignored suite `artifacts/` +directory and are removed during normal teardown. The runner performs a global local `gravity_node` cleanup before and after the suite. Do not run this beside another local Gravity cluster you need to keep. @@ -126,7 +136,7 @@ cluster. Do not terminate it after merely seeing the first onchain update. | `ORACLE_SOAK_DURATION_SECONDS` | `86400` | Monitored soak duration | | `ORACLE_SOAK_POLL_SECONDS` | `15` | Onchain heartbeat interval | | `ORACLE_SOAK_STALL_TIMEOUT_SECONDS` | `360` | Maximum feed or chain stall | -| `ORACLE_SOAK_MIN_ADVANCES` | 80% of expected minutes | Required Binance nonce advances | +| `ORACLE_SOAK_MIN_ADVANCES` | 80% of expected minutes | Required nonce advances for each Binance feed | | `ORACLE_SOAK_RESTART_AFTER_SECONDS` | Halfway for runs >= 1 hour | Restart time; `0` disables | | `ORACLE_SOAK_RESTART_NODE` | `node4` | Validator selected for restart | | `BINANCE_PRICE_FEED_BASE_URL` | `https://testnet.binancefuture.com` | Binance Futures testnet base URL | diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/hooks.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/hooks.py index 02e7e08a..8b014dc5 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/hooks.py +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/hooks.py @@ -21,8 +21,11 @@ DEFAULT_GRACE_MS = 120_000 DEFAULT_BINANCE_BASE_URL = "https://testnet.binancefuture.com" DEFAULT_GAMMA_URL = "https://gamma-api.polymarket.com" -NVDA_FEED_ID = 1001 -NVDA_PAIR = "NVDAUSDT" +BINANCE_FEEDS = ( + {"feedId": 1001, "pair": "NVDAUSDT"}, + {"feedId": 1002, "pair": "BTCUSDT"}, + {"feedId": 1003, "pair": "ETHUSDT"}, +) POLYGON_CHAIN_ID = 137 CTF_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045" @@ -256,10 +259,12 @@ def _bucket_start_ms(env: dict) -> int: return bucket_start_ms -def _price_uri(bucket_start_ms: int, grace_ms: int) -> str: +def _price_uri( + feed_id: int, pair: str, bucket_start_ms: int, grace_ms: int +) -> str: return ( - f"gravity://3/{NVDA_FEED_ID}/price_feed?" - f"provider=binance_index_kline_v1&pair={NVDA_PAIR}&interval=1m&" + f"gravity://3/{feed_id}/price_feed?" + f"provider=binance_index_kline_v1&pair={pair}&interval=1m&" f"bucketStartMs={bucket_start_ms}&decimals=8&graceMs={grace_ms}" ) @@ -288,7 +293,18 @@ def pre_deploy(test_dir: Path, env: dict, pytest_args: list[str]): if grace_ms < 0: raise RuntimeError("BINANCE_PRICE_FEED_GRACE_MS must be non-negative") bucket_start_ms = _bucket_start_ms(env) - price_uri = _price_uri(bucket_start_ms, grace_ms) + binance_feeds = [ + { + "baseUrl": binance_base_url, + **feed, + "bucketStartMs": bucket_start_ms, + "graceMs": grace_ms, + "taskUri": _price_uri( + feed["feedId"], feed["pair"], bucket_start_ms, grace_ms + ), + } + for feed in BINANCE_FEEDS + ] market = _discover_market(polygon_rpc_url, gamma_url) artifacts = test_dir / "artifacts" @@ -301,7 +317,10 @@ def pre_deploy(test_dir: Path, env: dict, pytest_args: list[str]): json.dumps( { "uri_mappings": { - price_uri: binance_base_url, + **{ + feed["taskUri"]: binance_base_url + for feed in binance_feeds + }, market["taskUri"]: polygon_rpc_url, } }, @@ -313,14 +332,7 @@ def pre_deploy(test_dir: Path, env: dict, pytest_args: list[str]): metadata_path.write_text( json.dumps( { - "binance": { - "baseUrl": binance_base_url, - "feedId": NVDA_FEED_ID, - "pair": NVDA_PAIR, - "bucketStartMs": bucket_start_ms, - "graceMs": grace_ms, - "taskUri": price_uri, - }, + "binanceFeeds": binance_feeds, "polymarket": market, }, indent=2, @@ -330,8 +342,9 @@ def pre_deploy(test_dir: Path, env: dict, pytest_args: list[str]): ) env["RELAYER_CONFIG_TPL"] = str(relayer_path) LOG.info( - "Prepared live Oracle inputs: Binance anchor=%s; Polymarket " + "Prepared live Oracle inputs: Binance pairs=%s anchor=%s; Polymarket " "mirrorId=%s block=%s finalized=%s", + [feed["pair"] for feed in binance_feeds], bucket_start_ms, market["mirrorId"], market["blockNumber"], diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py index e7ae964e..d268bccb 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py @@ -55,6 +55,7 @@ BOOTSTRAP_EPOCH_INTERVAL_MICROS = 60 * 1_000_000 SOAK_EPOCH_INTERVAL_MICROS = 2 * 60 * 60 * 1_000_000 SNAPSHOT_CONFIRMATION_BLOCKS = 16 +SNAPSHOT_READ_RETRIES = 20 _HEARTBEAT_FILE = "oracle_live_soak_heartbeat.jsonl" _SUMMARY_FILE = "oracle_live_soak_summary.json" @@ -304,6 +305,49 @@ def _call_at_block_hash(function, block_hash: str): return normalized[0] if len(normalized) == 1 else normalized +def _price_state_at_block_hash( + native_oracle, + price_resolver, + feed_id: int, + pair: str, + block_hash: str, +) -> tuple[tuple, tuple]: + for _ in range(SNAPSHOT_READ_RETRIES): + progress_before = tuple( + _call_at_block_hash( + native_oracle.functions.getSourceProgress( + support.SOURCE_TYPE_PRICE_FEED, feed_id + ), + block_hash, + ) + ) + latest = tuple( + _call_at_block_hash( + price_resolver.functions.latestPrice(feed_id), block_hash + ) + ) + progress_after = tuple( + _call_at_block_hash( + native_oracle.functions.getSourceProgress( + support.SOURCE_TYPE_PRICE_FEED, feed_id + ), + block_hash, + ) + ) + if ( + progress_before == progress_after + and latest[0] + and progress_after[1] == latest[2] + ): + return progress_after, latest + time.sleep(0.05) + raise AssertionError( + f"could not obtain an atomic {pair} Oracle snapshot at {block_hash}: " + f"progress_before={progress_before}, latest={latest}, " + f"progress_after={progress_after}" + ) + + def _topic(value: int) -> str: return "0x" + value.to_bytes(32, "big").hex() @@ -431,8 +475,7 @@ async def _replicated_snapshot( polymarket_artifact: dict, price_resolver_address: str, polymarket_resolver_address: str, - feed_id: int, - bucket_start_ms: int, + binance_feeds: list[dict], mirror_id: int, condition_id: bytes, ) -> dict: @@ -468,19 +511,32 @@ async def _replicated_snapshot( polymarket_resolver = node1.w3.eth.contract( address=polymarket_resolver_address, abi=polymarket_artifact["abi"] ) - price_progress = tuple( - _call_at_block_hash( - native_oracle.functions.getSourceProgress( - support.SOURCE_TYPE_PRICE_FEED, feed_id - ), + price_feeds = {} + for feed in binance_feeds: + feed_id = int(feed["feedId"]) + pair = feed["pair"] + progress, latest = _price_state_at_block_hash( + native_oracle, + price_resolver, + feed_id, + pair, snapshot_hash, ) - ) - latest_price = tuple( - _call_at_block_hash( - price_resolver.functions.latestPrice(feed_id), snapshot_hash + assert progress[0] >= 1 + round_start = _round_start_ms( + int(feed["bucketStartMs"]), progress[0] ) - ) + assert latest[0] + assert latest[1] == round_start // INTERVAL_MS + assert latest[2] == round_start + INTERVAL_MS - 1 + assert progress[1] == latest[2] + assert latest[3] == DECIMALS + assert latest[4] > 0 + price_feeds[pair] = { + "feedId": feed_id, + "progress": progress, + "latestPrice": latest, + } polymarket_progress = tuple( _call_at_block_hash( native_oracle.functions.getSourceProgress( @@ -498,14 +554,6 @@ async def _replicated_snapshot( ) ) - assert price_progress[0] >= 1 - price_round_start = _round_start_ms(bucket_start_ms, price_progress[0]) - assert latest_price[0] - assert latest_price[1] == price_round_start // INTERVAL_MS - assert latest_price[2] == price_round_start + INTERVAL_MS - 1 - assert price_progress[1] == latest_price[2] - assert latest_price[3] == DECIMALS - assert latest_price[4] > 0 assert polymarket_progress[0] == 1 assert settlement[0] @@ -520,19 +568,22 @@ async def _replicated_snapshot( address=polymarket_resolver_address, abi=polymarket_artifact["abi"], ) - assert tuple( - _call_at_block_hash( - replica_native.functions.getSourceProgress( - support.SOURCE_TYPE_PRICE_FEED, feed_id - ), + for feed in binance_feeds: + feed_id = int(feed["feedId"]) + pair = feed["pair"] + replica_progress, replica_latest = _price_state_at_block_hash( + replica_native, + replica_price, + feed_id, + pair, snapshot_hash, ) - ) == price_progress, f"{node_id} price progress diverged" - assert tuple( - _call_at_block_hash( - replica_price.functions.latestPrice(feed_id), snapshot_hash + assert replica_progress == price_feeds[pair]["progress"], ( + f"{node_id} {pair} progress diverged" + ) + assert replica_latest == price_feeds[pair]["latestPrice"], ( + f"{node_id} {pair} latest price diverged" ) - ) == latest_price, f"{node_id} latest price diverged" assert tuple( _call_at_block_hash( replica_native.functions.getSourceProgress( @@ -554,8 +605,7 @@ async def _replicated_snapshot( "block": snapshot_block, "blockHash": snapshot_hash, "heights": heights, - "priceProgress": price_progress, - "latestPrice": latest_price, + "priceFeeds": price_feeds, "polymarketProgress": polymarket_progress, "settlement": settlement, } @@ -570,8 +620,7 @@ async def _restart_validator( cluster: Cluster, node_id: str, target_block: int, - uri: str, - target_nonce: int, + price_targets: list[tuple[str, int]], ) -> float: node = cluster.get_node(node_id) if node is None: @@ -580,7 +629,10 @@ async def _restart_validator( assert await node.restart(), f"failed to restart {node_id}" await _wait_for_block(node_id, node.w3, target_block, timeout=180) assert await cluster.check_block_increasing(timeout=60) - await _wait_for_relayer_node(cluster, node_id, uri, target_nonce) + for uri, target_nonce in price_targets: + await _wait_for_relayer_node( + cluster, node_id, uri, target_nonce + ) return time.monotonic() - started @@ -592,11 +644,10 @@ async def _run_soak( polymarket_artifact: dict, price_resolver_address: str, polymarket_resolver_address: str, - binance: dict, + binance_feeds: list[dict], polymarket: dict, expected_settlement: tuple, ) -> dict: - feed_id = int(binance["feedId"]) mirror_id = int(polymarket["mirrorId"]) condition_id = bytes.fromhex(polymarket["conditionId"].removeprefix("0x")) heartbeat_path = SUITE_DIR / "artifacts" / _HEARTBEAT_FILE @@ -620,17 +671,24 @@ async def _run_soak( polymarket_artifact, price_resolver_address, polymarket_resolver_address, - feed_id, - int(binance["bucketStartMs"]), + binance_feeds, mirror_id, condition_id, ) - initial_nonce = int(initial["priceProgress"][0]) - last_nonce = initial_nonce + initial_nonces = { + pair: int(state["progress"][0]) + for pair, state in initial["priceFeeds"].items() + } + last_nonces = dict(initial_nonces) + last_prices = { + pair: int(state["latestPrice"][4]) + for pair, state in initial["priceFeeds"].items() + } + observed_price_changes = {pair: 0 for pair in initial_nonces} started = time.monotonic() deadline = started + settings.duration_seconds - last_price_advance = started - max_price_gap = 0.0 + last_price_advances = {pair: started for pair in initial_nonces} + max_price_gaps = {pair: 0.0 for pair in initial_nonces} last_height_change = {node_id: started for node_id in cluster.nodes} previous_heights = dict(initial["heights"]) restart_done = False @@ -649,8 +707,13 @@ async def _run_soak( cluster, settings.restart_node, max(int(height) for height in final["heights"].values()), - binance["taskUri"], - int(final["priceProgress"][0]), + [ + ( + feed["taskUri"], + int(final["priceFeeds"][feed["pair"]]["progress"][0]), + ) + for feed in binance_feeds + ], ) restart_done = True @@ -661,22 +724,48 @@ async def _run_soak( polymarket_artifact, price_resolver_address, polymarket_resolver_address, - feed_id, - int(binance["bucketStartMs"]), + binance_feeds, mirror_id, condition_id, ) now = time.monotonic() - nonce = int(final["priceProgress"][0]) - assert nonce >= last_nonce, "Binance source nonce regressed" - if nonce > last_nonce: - max_price_gap = max(max_price_gap, now - last_price_advance) - last_price_advance = now - last_nonce = nonce - assert now - last_price_advance <= settings.stall_seconds, ( - f"NVDA price feed stalled at nonce {nonce} for " - f"{now - last_price_advance:.1f}s" - ) + price_heartbeats = {} + for feed in binance_feeds: + pair = feed["pair"] + state = final["priceFeeds"][pair] + nonce = int(state["progress"][0]) + price = int(state["latestPrice"][4]) + assert nonce >= last_nonces[pair], ( + f"{pair} source nonce regressed" + ) + if nonce > last_nonces[pair]: + max_price_gaps[pair] = max( + max_price_gaps[pair], now - last_price_advances[pair] + ) + last_price_advances[pair] = now + last_nonces[pair] = nonce + if price != last_prices[pair]: + observed_price_changes[pair] += 1 + last_prices[pair] = price + assert now - last_price_advances[pair] <= settings.stall_seconds, ( + f"{pair} price feed stalled at nonce {nonce} for " + f"{now - last_price_advances[pair]:.1f}s" + ) + relayer_nodes = _relayer_quorum( + cluster, feed["taskUri"], nonce + ) + assert len(relayer_nodes) >= QUORUM_VALIDATORS, ( + f"only {relayer_nodes} persisted {pair} nonce {nonce}" + ) + price_heartbeats[pair] = { + "feedId": int(feed["feedId"]), + "nonce": nonce, + "position": int(state["progress"][1]), + "round": int(state["latestPrice"][1]), + "price": str(price), + "observedPriceChanges": observed_price_changes[pair], + "relayerQuorumNodes": relayer_nodes, + } for node_id, height in final["heights"].items(): assert height >= previous_heights[node_id], ( @@ -695,10 +784,6 @@ async def _run_soak( int(polymarket["blockNumber"]), ) assert tuple(final["settlement"]) == expected_settlement - relayer_nodes = _relayer_quorum(cluster, binance["taskUri"], nonce) - assert len(relayer_nodes) >= QUORUM_VALIDATORS, ( - f"only {relayer_nodes} persisted NVDA nonce {nonce}" - ) samples += 1 heartbeat = { @@ -706,11 +791,7 @@ async def _run_soak( "gravityBlock": int(final["block"]), "gravityBlockHash": final["blockHash"], "nodeHeights": final["heights"], - "nvdaNonce": nonce, - "nvdaPosition": int(final["priceProgress"][1]), - "nvdaRound": int(final["latestPrice"][1]), - "nvdaPrice": str(final["latestPrice"][4]), - "relayerQuorumNodes": relayer_nodes, + "priceFeeds": price_heartbeats, "polymarketNonce": int(final["polymarketProgress"][0]), "restartCompleted": restart_done, } @@ -727,44 +808,57 @@ async def _run_soak( polymarket_artifact, price_resolver_address, polymarket_resolver_address, - feed_id, - int(binance["bucketStartMs"]), + binance_feeds, mirror_id, condition_id, ) - final_nonce = int(final["priceProgress"][0]) - advances = final_nonce - initial_nonce - assert advances >= settings.minimum_advances, ( - f"NVDA advanced {advances} rounds; expected at least " - f"{settings.minimum_advances}" - ) if settings.restart_after_seconds: assert restart_done, "configured validator restart did not run" - await _wait_for_relayer_node( - cluster, - settings.restart_node, - binance["taskUri"], - final_nonce, - ) - final_bucket = _round_start_ms( - int(binance["bucketStartMs"]), final_nonce - ) - max_price_gap = max(max_price_gap, time.monotonic() - last_price_advance) - expected_price = await _fetch_binance_price_with_retry( - binance["baseUrl"], binance["pair"], final_bucket - ) - assert int(final["latestPrice"][4]) == expected_price + price_summaries = {} + for feed in binance_feeds: + pair = feed["pair"] + state = final["priceFeeds"][pair] + final_nonce = int(state["progress"][0]) + advances = final_nonce - initial_nonces[pair] + assert advances >= settings.minimum_advances, ( + f"{pair} advanced {advances} rounds; expected at least " + f"{settings.minimum_advances}" + ) + if settings.restart_after_seconds: + await _wait_for_relayer_node( + cluster, + settings.restart_node, + feed["taskUri"], + final_nonce, + ) + final_bucket = _round_start_ms( + int(feed["bucketStartMs"]), final_nonce + ) + max_price_gaps[pair] = max( + max_price_gaps[pair], + time.monotonic() - last_price_advances[pair], + ) + expected_price = await _fetch_binance_price_with_retry( + feed["baseUrl"], pair, final_bucket + ) + assert int(state["latestPrice"][4]) == expected_price + price_summaries[pair] = { + "feedId": int(feed["feedId"]), + "initialNonce": initial_nonces[pair], + "finalNonce": final_nonce, + "advances": advances, + "minimumAdvances": settings.minimum_advances, + "finalPrice": str(state["latestPrice"][4]), + "observedPriceChanges": observed_price_changes[pair], + "maxObservedGapSeconds": round(max_price_gaps[pair], 3), + } return { "status": "passed", "configuredDurationSeconds": settings.duration_seconds, "actualDurationSeconds": round(time.monotonic() - started, 3), "samples": samples, - "initialNonce": initial_nonce, - "finalNonce": final_nonce, - "advances": advances, - "minimumAdvances": settings.minimum_advances, - "maxObservedPriceGapSeconds": round(max_price_gap, 3), + "priceFeeds": price_summaries, "finalGravityBlock": int(final["block"]), "finalGravityBlockHash": final["blockHash"], "restartNode": ( @@ -787,9 +881,16 @@ async def test_governance_activated_oracles_soak_for_configured_duration( ): settings = _soak_settings() metadata = _metadata() - binance = metadata["binance"] + binance_feeds = metadata["binanceFeeds"] polymarket = metadata["polymarket"] - feed_id = int(binance["feedId"]) + assert [feed["pair"] for feed in binance_feeds] == [ + "NVDAUSDT", + "BTCUSDT", + "ETHUSDT", + ] + assert len({int(feed["feedId"]) for feed in binance_feeds}) == len( + binance_feeds + ) mirror_id = int(polymarket["mirrorId"]) condition_id = bytes.fromhex(polymarket["conditionId"].removeprefix("0x")) @@ -846,44 +947,51 @@ async def test_governance_activated_oracles_soak_for_configured_duration( ) assert tuple(epoch_config.functions.getPendingConfig().call()) == (False, 0) - assert not task_config.functions.hasTask( - support.SOURCE_TYPE_PRICE_FEED, feed_id, support.TASK_PRICE_FEED - ).call() + for feed in binance_feeds: + assert not task_config.functions.hasTask( + support.SOURCE_TYPE_PRICE_FEED, + int(feed["feedId"]), + support.TASK_PRICE_FEED, + ).call() assert not task_config.functions.hasTask( SOURCE_TYPE_POLYMARKET, mirror_id, POLYMARKET_TASK ).call() setup_epoch = await _wait_for_proposal_window(cluster) - receipt = await support.execute_governance_proposal( - w3, - support.faucet_voting_pool(w3), - [ - EPOCH_CONFIG_ADDRESS, - support.NATIVE_ORACLE_ADDRESS, - support.ORACLE_TASK_CONFIG_ADDRESS, - support.ORACLE_TASK_CONFIG_ADDRESS, - support.NATIVE_ORACLE_ADDRESS, - polymarket_resolver.address, - ], - [ - support.function_calldata( - epoch_config.functions.setForNextEpoch( - SOAK_EPOCH_INTERVAL_MICROS - ) - ), - support.function_calldata( - native_oracle.functions.setDefaultCallback( - support.SOURCE_TYPE_PRICE_FEED, price_resolver.address - ) - ), + targets = [EPOCH_CONFIG_ADDRESS, support.NATIVE_ORACLE_ADDRESS] + datas = [ + support.function_calldata( + epoch_config.functions.setForNextEpoch( + SOAK_EPOCH_INTERVAL_MICROS + ) + ), + support.function_calldata( + native_oracle.functions.setDefaultCallback( + support.SOURCE_TYPE_PRICE_FEED, price_resolver.address + ) + ), + ] + for feed in binance_feeds: + targets.append(support.ORACLE_TASK_CONFIG_ADDRESS) + datas.append( support.function_calldata( task_config.functions.setTask( support.SOURCE_TYPE_PRICE_FEED, - feed_id, + int(feed["feedId"]), support.TASK_PRICE_FEED, - binance["taskUri"].encode(), + feed["taskUri"].encode(), ) - ), + ) + ) + targets.extend( + [ + support.ORACLE_TASK_CONFIG_ADDRESS, + support.NATIVE_ORACLE_ADDRESS, + polymarket_resolver.address, + ] + ) + datas.extend( + [ support.function_calldata( task_config.functions.setTask( SOURCE_TYPE_POLYMARKET, @@ -908,7 +1016,13 @@ async def test_governance_activated_oracles_soak_for_configured_duration( int(polymarket["outcomeSlotCount"]), ) ), - ], + ] + ) + receipt = await support.execute_governance_proposal( + w3, + support.faucet_voting_pool(w3), + targets, + datas, "activate-live-binance-and-polymarket-soak", gas=8_000_000, ) @@ -921,20 +1035,24 @@ async def test_governance_activated_oracles_soak_for_configured_duration( True, SOAK_EPOCH_INTERVAL_MICROS, ) - for source_type, source_id, task_name, expected_uri in ( + configured_tasks = [ ( support.SOURCE_TYPE_PRICE_FEED, - feed_id, + int(feed["feedId"]), support.TASK_PRICE_FEED, - binance["taskUri"], - ), + feed["taskUri"], + ) + for feed in binance_feeds + ] + configured_tasks.append( ( SOURCE_TYPE_POLYMARKET, mirror_id, POLYMARKET_TASK, polymarket["taskUri"], - ), - ): + ) + ) + for source_type, source_id, task_name, expected_uri in configured_tasks: assert task_config.functions.hasTask( source_type, source_id, task_name ).call() @@ -946,7 +1064,10 @@ async def test_governance_activated_oracles_soak_for_configured_duration( source_type, source_id ).call() == 0 - issuers = [f"gravity://3/{feed_id}", f"gravity://6/{mirror_id}"] + issuers = [ + *(f"gravity://3/{int(feed['feedId'])}" for feed in binance_feeds), + f"gravity://6/{mirror_id}", + ] for node_id in cluster.nodes: for issuer in issuers: assert not _log_contains( @@ -963,13 +1084,15 @@ async def test_governance_activated_oracles_soak_for_configured_duration( assert _remaining_epoch_seconds(w3) > ( SOAK_EPOCH_INTERVAL_MICROS // 1_000_000 - 300 ) - price_progress, initial_price = await support.wait_for_latest_price( - native_oracle, - price_resolver, - feed_id, - 1, - timeout=ORACLE_TIMEOUT_SECONDS, - ) + initial_prices = {} + for feed in binance_feeds: + initial_prices[feed["pair"]] = await support.wait_for_latest_price( + native_oracle, + price_resolver, + int(feed["feedId"]), + 1, + timeout=ORACLE_TIMEOUT_SECONDS, + ) deadline = time.monotonic() + ORACLE_TIMEOUT_SECONDS while time.monotonic() < deadline: polymarket_progress = tuple( @@ -1031,17 +1154,23 @@ async def test_governance_activated_oracles_soak_for_configured_duration( "payoutNumerators" ] - first_bucket = _round_start_ms( - int(binance["bucketStartMs"]), int(price_progress[0]) - ) - expected_initial_price = await _fetch_binance_price_with_retry( - binance["baseUrl"], binance["pair"], first_bucket - ) - assert int(initial_price[4]) == expected_initial_price + for feed in binance_feeds: + price_progress, initial_price = initial_prices[feed["pair"]] + first_bucket = _round_start_ms( + int(feed["bucketStartMs"]), int(price_progress[0]) + ) + expected_initial_price = await _fetch_binance_price_with_retry( + feed["baseUrl"], feed["pair"], first_bucket + ) + assert int(initial_price[4]) == expected_initial_price await _wait_for_consensus_evidence(cluster, issuers) - assert len( - _relayer_quorum(cluster, binance["taskUri"], int(price_progress[0])) - ) >= QUORUM_VALIDATORS + for feed in binance_feeds: + price_progress, _ = initial_prices[feed["pair"]] + assert len( + _relayer_quorum( + cluster, feed["taskUri"], int(price_progress[0]) + ) + ) >= QUORUM_VALIDATORS try: summary = await _run_soak( @@ -1052,7 +1181,7 @@ async def test_governance_activated_oracles_soak_for_configured_duration( polymarket_artifact, price_resolver.address, polymarket_resolver.address, - binance, + binance_feeds, polymarket, settlement, ) @@ -1069,24 +1198,31 @@ async def test_governance_activated_oracles_soak_for_configured_duration( "errorType": type(error).__name__, "error": str(error), "configuredDurationSeconds": settings.duration_seconds, + "binancePairs": [ + feed["pair"] for feed in binance_feeds + ], "polymarketMirrorId": mirror_id, "lastHeartbeat": last_heartbeat, } ) raise - price_delivery_count = len( - _callback_success_logs( - w3, - support.SOURCE_TYPE_PRICE_FEED, - feed_id, - receipt["blockNumber"], - summary["finalGravityBlock"], + price_delivery_counts = {} + for feed in binance_feeds: + pair = feed["pair"] + delivery_count = len( + _callback_success_logs( + w3, + support.SOURCE_TYPE_PRICE_FEED, + int(feed["feedId"]), + receipt["blockNumber"], + summary["finalGravityBlock"], + ) ) - ) - assert price_delivery_count == summary["finalNonce"], ( - "NVDA callback count does not match final source nonce" - ) + assert delivery_count == summary["priceFeeds"][pair]["finalNonce"], ( + f"{pair} callback count does not match final source nonce" + ) + price_delivery_counts[pair] = delivery_count polymarket_delivery_count = len( _callback_success_logs( w3, @@ -1103,7 +1239,7 @@ async def test_governance_activated_oracles_soak_for_configured_duration( { "activationEpoch": activation_epoch, "governanceBlock": receipt["blockNumber"], - "priceCallbackEvents": price_delivery_count, + "priceCallbackEvents": price_delivery_counts, "polymarketCallbackEvents": polymarket_delivery_count, "soakEpochIntervalSeconds": ( SOAK_EPOCH_INTERVAL_MICROS // 1_000_000 From 78a31e205eef8f71297c0bef1d64717ea16639de Mon Sep 17 00:00:00 2001 From: ByteYue Date: Wed, 5 Aug 2026 04:06:19 +0800 Subject: [PATCH 06/12] test(oracle): avoid epoch-boundary restart collision --- .../oracle_live_soak/README.md | 19 ++++--- .../oracle_live_soak/test_oracle_live_soak.py | 50 ++++++++++++++----- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md index 330d5b95..2007960e 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md @@ -50,9 +50,12 @@ successful delivery. Heartbeats and the final summary report observed price changes per pair; price changes are informational because timely identical index closes are valid Oracle updates. -For runs of at least one hour, `node4` restarts halfway through by default. The -suite requires its chain and relayer checkpoints to catch up without nonce -regression or a duplicate Polymarket delivery. +For runs of at least one hour, `node4` becomes eligible to restart halfway +through by default. The runner defers the restart while the chain is within +five minutes of an epoch boundary, then requires the node's chain and relayer +checkpoints to catch up without nonce regression or a duplicate Polymarket +delivery. This keeps the Oracle restart check independent from a simultaneous +epoch reconfiguration. ## Prerequisites @@ -125,9 +128,11 @@ ORACLE_SOAK_STALL_TIMEOUT_SECONDS=360 \ --log-cli-level=INFO ``` -With the defaults, `node4` restarts after 12 hours. The pytest process is the -monitor, performs final assertions, writes the report, and tears down the -cluster. Do not terminate it after merely seeing the first onchain update. +With the defaults, `node4` becomes eligible to restart after 12 hours and runs +as soon as it is outside the five-minute epoch-transition guard. The pytest +process is the monitor, performs final assertions, writes the report, and tears +down the cluster. Do not terminate it after merely seeing the first onchain +update. ## Controls @@ -137,7 +142,7 @@ cluster. Do not terminate it after merely seeing the first onchain update. | `ORACLE_SOAK_POLL_SECONDS` | `15` | Onchain heartbeat interval | | `ORACLE_SOAK_STALL_TIMEOUT_SECONDS` | `360` | Maximum feed or chain stall | | `ORACLE_SOAK_MIN_ADVANCES` | 80% of expected minutes | Required nonce advances for each Binance feed | -| `ORACLE_SOAK_RESTART_AFTER_SECONDS` | Halfway for runs >= 1 hour | Restart time; `0` disables | +| `ORACLE_SOAK_RESTART_AFTER_SECONDS` | Halfway for runs >= 1 hour | Earliest restart time; `0` disables | | `ORACLE_SOAK_RESTART_NODE` | `node4` | Validator selected for restart | | `BINANCE_PRICE_FEED_BASE_URL` | `https://testnet.binancefuture.com` | Binance Futures testnet base URL | | `BINANCE_PRICE_FEED_GRACE_MS` | `120000` | Closed-bucket safety delay | diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py index d268bccb..54178a11 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py @@ -54,6 +54,7 @@ QUORUM_VALIDATORS = 3 BOOTSTRAP_EPOCH_INTERVAL_MICROS = 60 * 1_000_000 SOAK_EPOCH_INTERVAL_MICROS = 2 * 60 * 60 * 1_000_000 +RESTART_EPOCH_GUARD_SECONDS = 5 * 60 SNAPSHOT_CONFIRMATION_BLOCKS = 16 SNAPSHOT_READ_RETRIES = 20 @@ -140,6 +141,15 @@ def _remaining_epoch_seconds(w3: Web3) -> int: ) +def _restart_window_is_safe(w3: Web3) -> bool: + remaining = _remaining_epoch_seconds(w3) + interval = SOAK_EPOCH_INTERVAL_MICROS // 1_000_000 + return ( + RESTART_EPOCH_GUARD_SECONDS < remaining + < interval - RESTART_EPOCH_GUARD_SECONDS + ) + + async def _wait_for_epoch_advance(cluster: Cluster, start_epoch: int) -> int: deadline = time.monotonic() + EPOCH_TIMEOUT_SECONDS node1 = cluster.get_node("node1") @@ -693,6 +703,7 @@ async def _run_soak( previous_heights = dict(initial["heights"]) restart_done = False restart_recovery_seconds = None + restart_epoch_guard_deferrals = 0 samples = 0 final = initial @@ -703,19 +714,32 @@ async def _run_soak( and not restart_done and now - started >= settings.restart_after_seconds ): - restart_recovery_seconds = await _restart_validator( - cluster, - settings.restart_node, - max(int(height) for height in final["heights"].values()), - [ - ( - feed["taskUri"], - int(final["priceFeeds"][feed["pair"]]["progress"][0]), + epoch_rpc = next(iter(cluster.nodes.values())).w3 + if _restart_window_is_safe(epoch_rpc): + restart_recovery_seconds = await _restart_validator( + cluster, + settings.restart_node, + max(int(height) for height in final["heights"].values()), + [ + ( + feed["taskUri"], + int( + final["priceFeeds"][feed["pair"]][ + "progress" + ][0] + ), + ) + for feed in binance_feeds + ], + ) + restart_done = True + else: + restart_epoch_guard_deferrals += 1 + if restart_epoch_guard_deferrals == 1: + LOG.info( + "deferring validator restart outside the epoch " + "transition guard" ) - for feed in binance_feeds - ], - ) - restart_done = True final = await _replicated_snapshot( cluster, @@ -794,6 +818,7 @@ async def _run_soak( "priceFeeds": price_heartbeats, "polymarketNonce": int(final["polymarketProgress"][0]), "restartCompleted": restart_done, + "restartEpochGuardDeferrals": restart_epoch_guard_deferrals, } _append_json_line(heartbeat_path, heartbeat) LOG.info("Oracle soak heartbeat: %s", heartbeat) @@ -865,6 +890,7 @@ async def _run_soak( settings.restart_node if settings.restart_after_seconds else None ), "restartRecoverySeconds": restart_recovery_seconds, + "restartEpochGuardDeferrals": restart_epoch_guard_deferrals, "polymarketMirrorId": mirror_id, "polymarketNonce": int(final["polymarketProgress"][0]), } From f20872fc14f9fed3d2a0bcc86cf9b2a09aa0f69d Mon Sep 17 00:00:00 2001 From: ByteYue Date: Thu, 6 Aug 2026 04:46:26 +0800 Subject: [PATCH 07/12] test(oracle): retry transient replica snapshots --- .../oracle_live_soak/README.md | 3 + .../oracle_live_soak/test_oracle_live_soak.py | 108 +++++++++++++++--- 2 files changed, 93 insertions(+), 18 deletions(-) diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md index 2007960e..2bc94673 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md @@ -29,6 +29,9 @@ and Polymarket suites remain the reproducible CI coverage. - each replica returns the same NativeOracle progress and resolver state at that exact EIP-1898 canonical block hash, outside number-to-state view transitions near the execution head; +- a transient cross-replica execution-view lag retries the entire canonical + snapshot for up to five seconds; a state difference that persists across the + bounded retry window still fails the soak; - each price snapshot uses a progress/resolver/progress seqlock read so an RPC view transition between cross-contract calls is retried, not mistaken for inconsistent Oracle state; diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py index 54178a11..cc656ba4 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py @@ -57,6 +57,12 @@ RESTART_EPOCH_GUARD_SECONDS = 5 * 60 SNAPSHOT_CONFIRMATION_BLOCKS = 16 SNAPSHOT_READ_RETRIES = 20 +SNAPSHOT_CONVERGENCE_RETRIES = 20 +SNAPSHOT_CONVERGENCE_DELAY_SECONDS = 0.25 + + +class SnapshotNotConverged(AssertionError): + """A canonical snapshot is not yet readable consistently by every RPC.""" _HEARTBEAT_FILE = "oracle_live_soak_heartbeat.jsonl" _SUMMARY_FILE = "oracle_live_soak_summary.json" @@ -302,9 +308,10 @@ def _call_at_block_hash(function, block_hash: str): {"blockHash": block_hash, "requireCanonical": True}, ], ) - assert "error" not in response, ( - f"EIP-1898 eth_call failed at {block_hash}: {response['error']}" - ) + if "error" in response: + raise SnapshotNotConverged( + f"EIP-1898 eth_call failed at {block_hash}: {response['error']}" + ) output_types = get_abi_output_types(function.abi) decoded = function.w3.codec.decode( output_types, HexBytes(response["result"]) @@ -351,7 +358,7 @@ def _price_state_at_block_hash( ): return progress_after, latest time.sleep(0.05) - raise AssertionError( + raise SnapshotNotConverged( f"could not obtain an atomic {pair} Oracle snapshot at {block_hash}: " f"progress_before={progress_before}, latest={latest}, " f"progress_after={progress_after}" @@ -478,7 +485,7 @@ async def _wait_for_relayer_node( ) -async def _replicated_snapshot( +async def _replicated_snapshot_once( cluster: Cluster, native_artifact: dict, price_artifact: dict, @@ -506,9 +513,11 @@ async def _replicated_snapshot( block_hash = Web3.to_hex(block["hash"]).lower() if snapshot_hash is None: snapshot_hash = block_hash - assert block_hash == snapshot_hash, ( - f"{node_id} block hash diverged at Gravity block {snapshot_block}" - ) + if block_hash != snapshot_hash: + raise SnapshotNotConverged( + f"{node_id} block hash diverged at Gravity block " + f"{snapshot_block}: {block_hash} != {snapshot_hash}" + ) node1 = cluster.get_node("node1") @@ -588,28 +597,44 @@ async def _replicated_snapshot( pair, snapshot_hash, ) - assert replica_progress == price_feeds[pair]["progress"], ( - f"{node_id} {pair} progress diverged" - ) - assert replica_latest == price_feeds[pair]["latestPrice"], ( - f"{node_id} {pair} latest price diverged" - ) - assert tuple( + if replica_progress != price_feeds[pair]["progress"]: + raise SnapshotNotConverged( + f"{node_id} {pair} progress diverged: " + f"{replica_progress} != " + f"{price_feeds[pair]['progress']}" + ) + if replica_latest != price_feeds[pair]["latestPrice"]: + raise SnapshotNotConverged( + f"{node_id} {pair} latest price diverged: " + f"{replica_latest} != " + f"{price_feeds[pair]['latestPrice']}" + ) + replica_polymarket_progress = tuple( _call_at_block_hash( replica_native.functions.getSourceProgress( SOURCE_TYPE_POLYMARKET, mirror_id ), snapshot_hash, ) - ) == polymarket_progress, f"{node_id} Polymarket progress diverged" - assert tuple( + ) + if replica_polymarket_progress != polymarket_progress: + raise SnapshotNotConverged( + f"{node_id} Polymarket progress diverged: " + f"{replica_polymarket_progress} != {polymarket_progress}" + ) + replica_settlement = tuple( _call_at_block_hash( replica_polymarket.functions.getSettlement( mirror_id, condition_id ), snapshot_hash, ) - ) == settlement, f"{node_id} Polymarket settlement diverged" + ) + if replica_settlement != settlement: + raise SnapshotNotConverged( + f"{node_id} Polymarket settlement diverged: " + f"{replica_settlement} != {settlement}" + ) return { "block": snapshot_block, @@ -621,6 +646,53 @@ async def _replicated_snapshot( } +async def _replicated_snapshot( + cluster: Cluster, + native_artifact: dict, + price_artifact: dict, + polymarket_artifact: dict, + price_resolver_address: str, + polymarket_resolver_address: str, + binance_feeds: list[dict], + mirror_id: int, + condition_id: bytes, +) -> dict: + first_error = None + for attempt in range(1, SNAPSHOT_CONVERGENCE_RETRIES + 1): + try: + snapshot = await _replicated_snapshot_once( + cluster, + native_artifact, + price_artifact, + polymarket_artifact, + price_resolver_address, + polymarket_resolver_address, + binance_feeds, + mirror_id, + condition_id, + ) + if attempt > 1: + LOG.info( + "canonical replica snapshot converged after %d attempts", + attempt, + ) + return snapshot + except SnapshotNotConverged as exc: + if first_error is None: + first_error = str(exc) + LOG.warning( + "canonical replica snapshot not yet converged: %s", exc + ) + if attempt == SNAPSHOT_CONVERGENCE_RETRIES: + raise AssertionError( + "canonical replica snapshot did not converge after " + f"{attempt} attempts; first={first_error}; last={exc}" + ) from exc + await asyncio.sleep(SNAPSHOT_CONVERGENCE_DELAY_SECONDS) + + raise AssertionError("unreachable replica snapshot retry state") + + def _append_json_line(path: Path, payload: dict) -> None: with path.open("a") as output: output.write(json.dumps(payload, sort_keys=True) + "\n") From 4f024a1c7f7f37960d152eadb9d055a0255a2c2e Mon Sep 17 00:00:00 2001 From: ByteYue Date: Thu, 6 Aug 2026 17:16:13 +0800 Subject: [PATCH 08/12] test(e2e): allow long validator RPC recovery --- .../cluster_test_cases/oracle_live_soak/README.md | 6 +++++- .../oracle_live_soak/test_oracle_live_soak.py | 14 +++++++++++++- gravity_e2e/gravity_e2e/cluster/node.py | 11 +++++++---- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md index 2bc94673..8fa12bba 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md @@ -135,7 +135,10 @@ With the defaults, `node4` becomes eligible to restart after 12 hours and runs as soon as it is outside the five-minute epoch-transition guard. The pytest process is the monitor, performs final assertions, writes the report, and tears down the cluster. Do not terminate it after merely seeing the first onchain -update. +update. A long-lived node can spend more than the cluster harness's usual 30 +seconds reopening its persisted databases, so this suite gives restart RPC +recovery a three-minute window while still failing immediately if the process +exits. ## Controls @@ -147,6 +150,7 @@ update. | `ORACLE_SOAK_MIN_ADVANCES` | 80% of expected minutes | Required nonce advances for each Binance feed | | `ORACLE_SOAK_RESTART_AFTER_SECONDS` | Halfway for runs >= 1 hour | Earliest restart time; `0` disables | | `ORACLE_SOAK_RESTART_NODE` | `node4` | Validator selected for restart | +| `ORACLE_SOAK_RESTART_RPC_TIMEOUT_SECONDS` | `180` | RPC recovery window for the restarted validator | | `BINANCE_PRICE_FEED_BASE_URL` | `https://testnet.binancefuture.com` | Binance Futures testnet base URL | | `BINANCE_PRICE_FEED_GRACE_MS` | `120000` | Closed-bucket safety delay | | `POLYMARKET_GAMMA_URL` | `https://gamma-api.polymarket.com` | Market discovery API | diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py index cc656ba4..703b627d 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py @@ -48,6 +48,7 @@ DEFAULT_SOAK_SECONDS = 24 * 60 * 60 DEFAULT_POLL_SECONDS = 15 DEFAULT_STALL_SECONDS = 6 * 60 +DEFAULT_RESTART_RPC_TIMEOUT_SECONDS = 3 * 60 EPOCH_TIMEOUT_SECONDS = 180 ORACLE_TIMEOUT_SECONDS = 360 MIN_PROPOSAL_WINDOW_SECONDS = 40 @@ -76,6 +77,7 @@ class SoakSettings: minimum_advances: int restart_after_seconds: int restart_node: str + restart_rpc_timeout_seconds: int def _env_int(name: str, default: int, minimum: int = 0) -> int: @@ -123,6 +125,11 @@ def _soak_settings() -> SoakSettings: minimum_advances=minimum_advances, restart_after_seconds=restart_after, restart_node=os.environ.get("ORACLE_SOAK_RESTART_NODE", "node4"), + restart_rpc_timeout_seconds=_env_int( + "ORACLE_SOAK_RESTART_RPC_TIMEOUT_SECONDS", + DEFAULT_RESTART_RPC_TIMEOUT_SECONDS, + minimum=30, + ), ) @@ -703,12 +710,15 @@ async def _restart_validator( node_id: str, target_block: int, price_targets: list[tuple[str, int]], + rpc_timeout: int, ) -> float: node = cluster.get_node(node_id) if node is None: raise AssertionError(f"unknown restart node {node_id}") started = time.monotonic() - assert await node.restart(), f"failed to restart {node_id}" + assert await node.restart(rpc_timeout=rpc_timeout), ( + f"failed to restart {node_id} within {rpc_timeout}s RPC timeout" + ) await _wait_for_block(node_id, node.w3, target_block, timeout=180) assert await cluster.check_block_increasing(timeout=60) for uri, target_nonce in price_targets: @@ -803,6 +813,7 @@ async def _run_soak( ) for feed in binance_feeds ], + settings.restart_rpc_timeout_seconds, ) restart_done = True else: @@ -961,6 +972,7 @@ async def _run_soak( "restartNode": ( settings.restart_node if settings.restart_after_seconds else None ), + "restartRpcTimeoutSeconds": settings.restart_rpc_timeout_seconds, "restartRecoverySeconds": restart_recovery_seconds, "restartEpochGuardDeferrals": restart_epoch_guard_deferrals, "polymarketMirrorId": mirror_id, diff --git a/gravity_e2e/gravity_e2e/cluster/node.py b/gravity_e2e/gravity_e2e/cluster/node.py index 9371863b..e4d61cf3 100644 --- a/gravity_e2e/gravity_e2e/cluster/node.py +++ b/gravity_e2e/gravity_e2e/cluster/node.py @@ -184,10 +184,13 @@ async def get_state(self) -> Tuple[NodeState, int]: else: return NodeState.STOPPED, -1 - async def start(self) -> bool: + async def start(self, rpc_timeout: int = 30) -> bool: """ Start this individual node. Returns True if node is now RUNNING. + + ``rpc_timeout`` may be increased by long-running recovery tests whose + persisted databases need more than the default startup window. """ if not self.start_script.exists(): LOG.warning( @@ -259,7 +262,7 @@ async def log_stream(stream, level): return False # Wait for RPC to come up - if await self.wait_for_rpc(timeout=30): + if await self.wait_for_rpc(timeout=rpc_timeout): LOG.info(f"Node {self.id} started and RPC verified.") return True else: @@ -361,13 +364,13 @@ async def _wait_for_pid_exit(self, pid: int, timeout: float = 50.0) -> bool: await asyncio.sleep(0.25) return False - async def restart(self) -> bool: + async def restart(self, rpc_timeout: int = 30) -> bool: """Bounce the node.""" if not await self.stop(): return False # stop() already waits for process exit / RocksDB unlock; short settle only. await asyncio.sleep(0.5) - return await self.start() + return await self.start(rpc_timeout=rpc_timeout) def is_running(self) -> bool: """Check if node process is running based on PID file.""" From f505602595652260e03125bf00b28e6a26241370 Mon Sep 17 00:00:00 2001 From: ByteYue Date: Fri, 7 Aug 2026 09:49:01 +0800 Subject: [PATCH 09/12] test(e2e): exercise repeated validator restarts --- .../oracle_live_soak/README.md | 35 +++- .../oracle_live_soak/test_oracle_live_soak.py | 162 +++++++++++++++--- 2 files changed, 167 insertions(+), 30 deletions(-) diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md index 8fa12bba..2da38fc9 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md @@ -54,11 +54,11 @@ changes per pair; price changes are informational because timely identical index closes are valid Oracle updates. For runs of at least one hour, `node4` becomes eligible to restart halfway -through by default. The runner defers the restart while the chain is within -five minutes of an epoch boundary, then requires the node's chain and relayer -checkpoints to catch up without nonce regression or a duplicate Polymarket -delivery. This keeps the Oracle restart check independent from a simultaneous -epoch reconfiguration. +through by default. A focused regression can configure multiple restart times. +The runner defers each restart while the chain is within five minutes of an +epoch boundary, then requires the node's chain and relayer checkpoints to catch +up without nonce regression or a duplicate Polymarket delivery. This keeps the +Oracle restart check independent from a simultaneous epoch reconfiguration. ## Prerequisites @@ -117,6 +117,30 @@ ORACLE_SOAK_RESTART_AFTER_SECONDS=0 \ Do not start the 24-hour run unless this command exits successfully and the summary reports `status: passed`. +## Focused Multi-Restart Regression + +Use this after changing validator startup, persistence, JWK consensus, or +relayer recovery. It restarts `node4` three times while the same live Binance +and Polygon tasks continue running: + +```bash +ORACLE_SOAK_DURATION_SECONDS=1200 \ +ORACLE_SOAK_POLL_SECONDS=15 \ +ORACLE_SOAK_STALL_TIMEOUT_SECONDS=360 \ +ORACLE_SOAK_MIN_ADVANCES=15 \ +ORACLE_SOAK_RESTART_SCHEDULE_SECONDS=360,720,960 \ + ./gravity_e2e/run_test.sh \ + oracle_live_soak \ + --force-init \ + --log-cli-level=INFO +``` + +The schedule is measured from the start of the monitored soak, after task +activation and initial quorum. A scheduled restart can run later than its +threshold when the epoch guard is closed. The summary must contain three +entries in `restartRecoveries`, and the final snapshot must still show all four +replicas and every relayer checkpoint caught up. + ## 24-Hour Soak Run from a terminal multiplexer or another session that will stay alive: @@ -149,6 +173,7 @@ exits. | `ORACLE_SOAK_STALL_TIMEOUT_SECONDS` | `360` | Maximum feed or chain stall | | `ORACLE_SOAK_MIN_ADVANCES` | 80% of expected minutes | Required nonce advances for each Binance feed | | `ORACLE_SOAK_RESTART_AFTER_SECONDS` | Halfway for runs >= 1 hour | Earliest restart time; `0` disables | +| `ORACLE_SOAK_RESTART_SCHEDULE_SECONDS` | unset | Strictly increasing comma-separated restart times; mutually exclusive with `ORACLE_SOAK_RESTART_AFTER_SECONDS` | | `ORACLE_SOAK_RESTART_NODE` | `node4` | Validator selected for restart | | `ORACLE_SOAK_RESTART_RPC_TIMEOUT_SECONDS` | `180` | RPC recovery window for the restarted validator | | `BINANCE_PRICE_FEED_BASE_URL` | `https://testnet.binancefuture.com` | Binance Futures testnet base URL | diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py index 703b627d..1655c7b8 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py @@ -75,7 +75,7 @@ class SoakSettings: poll_seconds: int stall_seconds: int minimum_advances: int - restart_after_seconds: int + restart_schedule_seconds: tuple[int, ...] restart_node: str restart_rpc_timeout_seconds: int @@ -108,22 +108,45 @@ def _soak_settings() -> SoakSettings: if minimum_advances < 0: raise ValueError("ORACLE_SOAK_MIN_ADVANCES must be non-negative") - configured_restart = os.environ.get("ORACLE_SOAK_RESTART_AFTER_SECONDS") - restart_after = ( - duration // 2 - if configured_restart is None and duration >= 60 * 60 - else int(configured_restart or 0) + configured_schedule = os.environ.get( + "ORACLE_SOAK_RESTART_SCHEDULE_SECONDS" ) - if restart_after < 0 or restart_after >= duration: + configured_restart = os.environ.get("ORACLE_SOAK_RESTART_AFTER_SECONDS") + if configured_schedule is not None and configured_restart is not None: + raise ValueError( + "configure only one of ORACLE_SOAK_RESTART_SCHEDULE_SECONDS " + "and ORACLE_SOAK_RESTART_AFTER_SECONDS" + ) + if configured_schedule is not None: + restart_schedule = tuple( + int(value.strip()) + for value in configured_schedule.split(",") + if value.strip() + ) + elif configured_restart is not None: + restart_after = int(configured_restart or 0) + restart_schedule = (restart_after,) if restart_after else () + elif duration >= 60 * 60: + restart_schedule = (duration // 2,) + else: + restart_schedule = () + if any( + restart_after <= 0 or restart_after >= duration + for restart_after in restart_schedule + ): + raise ValueError( + "restart schedule entries must be positive and less than duration" + ) + if tuple(sorted(set(restart_schedule))) != restart_schedule: raise ValueError( - "ORACLE_SOAK_RESTART_AFTER_SECONDS must be zero or less than duration" + "restart schedule entries must be unique and strictly increasing" ) return SoakSettings( duration_seconds=duration, poll_seconds=poll_seconds, stall_seconds=stall_seconds, minimum_advances=minimum_advances, - restart_after_seconds=restart_after, + restart_schedule_seconds=restart_schedule, restart_node=os.environ.get("ORACLE_SOAK_RESTART_NODE", "node4"), restart_rpc_timeout_seconds=_env_int( "ORACLE_SOAK_RESTART_RPC_TIMEOUT_SECONDS", @@ -133,6 +156,57 @@ def _soak_settings() -> SoakSettings: ) +def test_soak_settings_defaults_to_one_midpoint_restart(monkeypatch): + monkeypatch.setenv("ORACLE_SOAK_DURATION_SECONDS", "7200") + monkeypatch.delenv( + "ORACLE_SOAK_RESTART_SCHEDULE_SECONDS", raising=False + ) + monkeypatch.delenv("ORACLE_SOAK_RESTART_AFTER_SECONDS", raising=False) + + assert _soak_settings().restart_schedule_seconds == (3600,) + + +def test_soak_settings_accepts_multiple_restarts(monkeypatch): + monkeypatch.setenv("ORACLE_SOAK_DURATION_SECONDS", "1200") + monkeypatch.setenv( + "ORACLE_SOAK_RESTART_SCHEDULE_SECONDS", "360, 720,960" + ) + monkeypatch.delenv("ORACLE_SOAK_RESTART_AFTER_SECONDS", raising=False) + + assert _soak_settings().restart_schedule_seconds == (360, 720, 960) + + +def test_soak_settings_legacy_zero_disables_restart(monkeypatch): + monkeypatch.setenv("ORACLE_SOAK_DURATION_SECONDS", "7200") + monkeypatch.delenv( + "ORACLE_SOAK_RESTART_SCHEDULE_SECONDS", raising=False + ) + monkeypatch.setenv("ORACLE_SOAK_RESTART_AFTER_SECONDS", "0") + + assert _soak_settings().restart_schedule_seconds == () + + +@pytest.mark.parametrize("schedule", ["720,360", "360,360", "0", "1200"]) +def test_soak_settings_rejects_invalid_restart_schedule( + monkeypatch, schedule +): + monkeypatch.setenv("ORACLE_SOAK_DURATION_SECONDS", "1200") + monkeypatch.setenv("ORACLE_SOAK_RESTART_SCHEDULE_SECONDS", schedule) + monkeypatch.delenv("ORACLE_SOAK_RESTART_AFTER_SECONDS", raising=False) + + with pytest.raises(ValueError): + _soak_settings() + + +def test_soak_settings_rejects_two_restart_controls(monkeypatch): + monkeypatch.setenv("ORACLE_SOAK_DURATION_SECONDS", "1200") + monkeypatch.setenv("ORACLE_SOAK_RESTART_SCHEDULE_SECONDS", "360") + monkeypatch.setenv("ORACLE_SOAK_RESTART_AFTER_SECONDS", "720") + + with pytest.raises(ValueError, match="configure only one"): + _soak_settings() + + def _metadata() -> dict: path = SUITE_DIR / "artifacts" / "oracle_live_soak_metadata.json" return json.loads(path.read_text()) @@ -783,8 +857,8 @@ async def _run_soak( max_price_gaps = {pair: 0.0 for pair in initial_nonces} last_height_change = {node_id: started for node_id in cluster.nodes} previous_heights = dict(initial["heights"]) - restart_done = False - restart_recovery_seconds = None + restart_index = 0 + restart_recoveries = [] restart_epoch_guard_deferrals = 0 samples = 0 final = initial @@ -792,13 +866,16 @@ async def _run_soak( while time.monotonic() < deadline: now = time.monotonic() if ( - settings.restart_after_seconds - and not restart_done - and now - started >= settings.restart_after_seconds + restart_index < len(settings.restart_schedule_seconds) + and now - started + >= settings.restart_schedule_seconds[restart_index] ): epoch_rpc = next(iter(cluster.nodes.values())).w3 if _restart_window_is_safe(epoch_rpc): - restart_recovery_seconds = await _restart_validator( + scheduled_after = settings.restart_schedule_seconds[ + restart_index + ] + recovery_seconds = await _restart_validator( cluster, settings.restart_node, max(int(height) for height in final["heights"].values()), @@ -815,13 +892,27 @@ async def _run_soak( ], settings.restart_rpc_timeout_seconds, ) - restart_done = True + restart_recoveries.append( + { + "sequence": restart_index + 1, + "scheduledAfterSeconds": scheduled_after, + "actualAfterSeconds": round( + time.monotonic() - started, 3 + ), + "recoverySeconds": round(recovery_seconds, 3), + } + ) + restart_index += 1 else: restart_epoch_guard_deferrals += 1 - if restart_epoch_guard_deferrals == 1: + if restart_epoch_guard_deferrals == 1 or ( + restart_epoch_guard_deferrals - 1 + ) % 20 == 0: LOG.info( - "deferring validator restart outside the epoch " - "transition guard" + "deferring validator restart %d/%d outside the " + "epoch transition guard", + restart_index + 1, + len(settings.restart_schedule_seconds), ) final = await _replicated_snapshot( @@ -900,7 +991,9 @@ async def _run_soak( "nodeHeights": final["heights"], "priceFeeds": price_heartbeats, "polymarketNonce": int(final["polymarketProgress"][0]), - "restartCompleted": restart_done, + "restartCompleted": bool(settings.restart_schedule_seconds) + and restart_index == len(settings.restart_schedule_seconds), + "restartCount": restart_index, "restartEpochGuardDeferrals": restart_epoch_guard_deferrals, } _append_json_line(heartbeat_path, heartbeat) @@ -920,8 +1013,11 @@ async def _run_soak( mirror_id, condition_id, ) - if settings.restart_after_seconds: - assert restart_done, "configured validator restart did not run" + if settings.restart_schedule_seconds: + assert restart_index == len(settings.restart_schedule_seconds), ( + f"only {restart_index}/{len(settings.restart_schedule_seconds)} " + "configured validator restarts ran" + ) price_summaries = {} for feed in binance_feeds: @@ -933,7 +1029,7 @@ async def _run_soak( f"{pair} advanced {advances} rounds; expected at least " f"{settings.minimum_advances}" ) - if settings.restart_after_seconds: + if settings.restart_schedule_seconds: await _wait_for_relayer_node( cluster, settings.restart_node, @@ -970,10 +1066,18 @@ async def _run_soak( "finalGravityBlock": int(final["block"]), "finalGravityBlockHash": final["blockHash"], "restartNode": ( - settings.restart_node if settings.restart_after_seconds else None + settings.restart_node + if settings.restart_schedule_seconds + else None ), + "restartScheduleSeconds": list(settings.restart_schedule_seconds), "restartRpcTimeoutSeconds": settings.restart_rpc_timeout_seconds, - "restartRecoverySeconds": restart_recovery_seconds, + "restartRecoverySeconds": ( + restart_recoveries[0]["recoverySeconds"] + if len(restart_recoveries) == 1 + else None + ), + "restartRecoveries": restart_recoveries, "restartEpochGuardDeferrals": restart_epoch_guard_deferrals, "polymarketMirrorId": mirror_id, "polymarketNonce": int(final["polymarketProgress"][0]), @@ -1308,6 +1412,14 @@ async def test_governance_activated_oracles_soak_for_configured_duration( "errorType": type(error).__name__, "error": str(error), "configuredDurationSeconds": settings.duration_seconds, + "restartNode": ( + settings.restart_node + if settings.restart_schedule_seconds + else None + ), + "restartScheduleSeconds": list( + settings.restart_schedule_seconds + ), "binancePairs": [ feed["pair"] for feed in binance_feeds ], From bf683e516e090881de899df61ce1e1bad9878076 Mon Sep 17 00:00:00 2001 From: ByteYue Date: Sun, 9 Aug 2026 15:56:06 +0800 Subject: [PATCH 10/12] test(e2e): paginate long soak callback scans --- .../oracle_live_soak/test_oracle_live_soak.py | 94 ++++++++++++++++--- 1 file changed, 83 insertions(+), 11 deletions(-) diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py index 1655c7b8..b5ffb509 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py @@ -60,6 +60,7 @@ SNAPSHOT_READ_RETRIES = 20 SNAPSHOT_CONVERGENCE_RETRIES = 20 SNAPSHOT_CONVERGENCE_DELAY_SECONDS = 0.25 +MAX_GET_LOGS_BLOCKS = 100_000 class SnapshotNotConverged(AssertionError): @@ -457,18 +458,89 @@ def _callback_success_logs( from_block: int, to_block: int | str = "latest", ) -> list: - return w3.eth.get_logs( - { - "fromBlock": from_block, - "toBlock": to_block, - "address": support.NATIVE_ORACLE_ADDRESS, - "topics": [ - CALLBACK_SUCCESS_TOPIC0, - _topic(source_type), - _topic(source_id), - ], - } + final_block = ( + int(w3.eth.block_number) if to_block == "latest" else int(to_block) ) + if final_block < from_block: + return [] + + logs = [] + for chunk_start in range( + from_block, final_block + 1, MAX_GET_LOGS_BLOCKS + ): + chunk_end = min( + chunk_start + MAX_GET_LOGS_BLOCKS - 1, final_block + ) + logs.extend( + w3.eth.get_logs( + { + "fromBlock": chunk_start, + "toBlock": chunk_end, + "address": support.NATIVE_ORACLE_ADDRESS, + "topics": [ + CALLBACK_SUCCESS_TOPIC0, + _topic(source_type), + _topic(source_id), + ], + } + ) + ) + return logs + + +def test_callback_success_logs_chunks_large_ranges(): + class FakeEth: + block_number = 400_000 + + def __init__(self): + self.requests = [] + + def get_logs(self, request): + self.requests.append(request) + return [(request["fromBlock"], request["toBlock"])] + + class FakeWeb3: + def __init__(self): + self.eth = FakeEth() + + w3 = FakeWeb3() + logs = _callback_success_logs(w3, 3, 1001, 68, 343_799) + + assert logs == [ + (68, 100_067), + (100_068, 200_067), + (200_068, 300_067), + (300_068, 343_799), + ] + assert all( + request["address"] == support.NATIVE_ORACLE_ADDRESS + and request["topics"] + == [CALLBACK_SUCCESS_TOPIC0, _topic(3), _topic(1001)] + for request in w3.eth.requests + ) + + +def test_callback_success_logs_snapshots_latest_once(): + class FakeEth: + block_number = 200_001 + + def __init__(self): + self.requests = [] + + def get_logs(self, request): + self.requests.append(request) + return [] + + class FakeWeb3: + def __init__(self): + self.eth = FakeEth() + + w3 = FakeWeb3() + assert _callback_success_logs(w3, 6, 42, 0) == [] + assert [ + (request["fromBlock"], request["toBlock"]) + for request in w3.eth.requests + ] == [(0, 99_999), (100_000, 199_999), (200_000, 200_001)] def _round_start_ms(first_bucket_start_ms: int, delivery_nonce: int) -> int: From 964d41a9a988f3d7b7e22333a182b6d0bc622cff Mon Sep 17 00:00:00 2001 From: ByteYue Date: Sun, 9 Aug 2026 19:55:08 +0800 Subject: [PATCH 11/12] test(oracle): make live soak Binance-only --- .../oracle_live_soak/README.md | 173 +++++------ .../oracle_live_soak/cluster.toml | 2 +- .../oracle_live_soak/genesis.toml | 8 +- .../oracle_live_soak/hooks.py | 231 +-------------- .../oracle_live_soak/test_oracle_live_soak.py | 277 ++---------------- .../gravity_e2e/utils/oracle_test_support.py | 8 +- 6 files changed, 108 insertions(+), 591 deletions(-) diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md index 2da38fc9..ab43f535 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md @@ -1,83 +1,71 @@ -# Wave 7 Manual Live Oracle Soak +# Wave 7 Binance Price Feed Live Soak -This manual, non-gating suite runs four equal-power Gravity validators against: +This manual, non-gating suite runs four equal-power Gravity validators against +three continuous Binance Futures testnet closed index-price kline feeds: -- three continuous Binance `NVDAUSDT`, `BTCUSDT`, and `ETHUSDT` closed - index-price kline feeds (`sourceType=3`, feed IDs `1001` through `1003`); -- one recently closed binary Polymarket market whose CTF resolution is already - finalized on Polygon (`sourceType=6`). +- `NVDAUSDT` (`sourceType=3`, feed ID `1001`); +- `BTCUSDT` (`sourceType=3`, feed ID `1002`); +- `ETHUSDT` (`sourceType=3`, feed ID `1003`). -The suite discovers the live inputs before deployment, then submits one Gravity -governance proposal that registers both tasks and their resolver callbacks. The -same proposal changes the epoch interval from the 60-second bootstrap value to -two hours for `E+1`. It proves that observers remain absent in proposal epoch -`E`, start in `E+1`, and reach a three-of-four JWK quorum before the soak timer -begins. +The suite submits one Gravity governance proposal that registers the three +price tasks and the `PriceFeedResolver` callback. The same proposal changes the +epoch interval from the 60-second bootstrap value to two hours for `E+1`. It +proves that observers remain absent in proposal epoch `E`, start in `E+1`, and +reach a three-of-four JWK quorum before the soak timer begins. -The short bootstrap epoch keeps dynamic task activation fast. The two-hour soak -epoch intentionally follows the repository's regular four-validator topology: -a 24-hour Oracle soak should exercise roughly 12 epoch transitions, rather than -also acting as a 1,440-transition consensus reconfiguration stress test. +This is the initial Binance-only launch gate tracked by gravity-audit #1093. +The suite does not enable source type 6, deploy or register a Polymarket +resolver, create a Polymarket task or callback, require Polygon credentials, or +write a `gravity://6/...` validator mapping. The merged deterministic +Polymarket transport suite remains separate regression coverage for a possible +future activation. -This is operational evidence, not merge-gating CI. The deterministic Binance -and Polymarket suites remain the reproducible CI coverage. +The short bootstrap epoch keeps dynamic task activation fast. The two-hour soak +epoch intentionally follows the repository's regular four-validator topology, +so a long Oracle soak does not also become a minute-by-minute reconfiguration +stress test. ## What Every Heartbeat Checks - all four Gravity RPC replicas expose the same block hash at the common confirmed height `min(latest node heights) - 16`; -- each replica returns the same NativeOracle progress and resolver state at - that exact EIP-1898 canonical block hash, outside number-to-state view - transitions near the execution head; -- a transient cross-replica execution-view lag retries the entire canonical - snapshot for up to five seconds; a state difference that persists across the - bounded retry window still fails the soak; -- each price snapshot uses a progress/resolver/progress seqlock read so an RPC - view transition between cross-contract calls is retried, not mistaken for - inconsistent Oracle state; -- every Binance feed's delivery nonce, source position, and resolver round never - regress independently; -- every latest resolver value maps to that pair's exact closed one-minute - bucket; -- all three Binance feeds and every Gravity node stay within the configured - stall budget; +- each replica returns the same NativeOracle progress and PriceFeedResolver + state at that exact EIP-1898 canonical block hash; +- bounded whole-snapshot retries tolerate a transient execution-view lag, while + a state difference that persists across the retry window fails the soak; +- every price snapshot uses a progress/resolver/progress seqlock read; +- each feed's delivery nonce, source position, and resolver round never regress; +- each latest resolver value maps to the pair's exact closed one-minute bucket; +- all three feeds and every Gravity node stay within the stall budget; - at least three relayers have checkpointed each committed Binance nonce; -- the finalized Polymarket settlement remains immutable at nonce `1`. - the governance-pending two-hour epoch interval is active before timing starts. At completion, the suite fetches each exact final Binance bucket again and -compares its close with the corresponding onchain value. It also counts -NativeOracle callback successes: each pair's count must equal its independent -final delivery nonce, while the Polymarket settlement must have exactly one -successful delivery. Heartbeats and the final summary report observed price -changes per pair; price changes are informational because timely identical -index closes are valid Oracle updates. +compares its close with the corresponding onchain value. Each feed's +NativeOracle callback count must equal its independent final delivery nonce. +Observed price changes remain informational because timely identical index +closes are valid Oracle updates. For runs of at least one hour, `node4` becomes eligible to restart halfway -through by default. A focused regression can configure multiple restart times. -The runner defers each restart while the chain is within five minutes of an -epoch boundary, then requires the node's chain and relayer checkpoints to catch -up without nonce regression or a duplicate Polymarket delivery. This keeps the -Oracle restart check independent from a simultaneous epoch reconfiguration. +through by default. A focused run can configure multiple restart times. The +runner defers each restart while the chain is within five minutes of an epoch +boundary, then requires RPC, block height, and all three relayer checkpoints to +catch up without nonce regression. ## Prerequisites - Current quick-release `gravity_node` and `gravity_cli` binaries. -- Foundry `forge` for the resolver artifacts. -- Approved outbound access to Binance Futures testnet, Polymarket Gamma, and - Polygon. -- `POLYGON_RPC_URL` or `POLYGON_QUICKNONE_HTTP_URL` set to an HTTPS Polygon RPC. - -The Binance Futures testnet `indexPriceKlines` endpoint used here is public and -does not require an API key. Each validator independently fetches all three -pairs. This exercises multiple live HTTP tasks and the complete validator -consensus path, but the values are testnet index data and must be labeled that -way in a demo. Do not put RPC credentials in committed files. The generated -relayer mapping and source metadata live under the ignored suite `artifacts/` -directory and are removed during normal teardown. +- Foundry `forge` for the PriceFeedResolver artifact. +- Approved outbound access to Binance Futures testnet. + +The Binance Futures testnet `indexPriceKlines` endpoint is public and does not +require an API key. Each validator independently fetches all three pairs. The +values are testnet index data and must be labeled that way in any demo. The +generated relayer mapping and source metadata live under the ignored suite +`artifacts/` directory and are removed during normal teardown. The runner performs a global local `gravity_node` cleanup before and after the -suite. Do not run this beside another local Gravity cluster you need to keep. +suite. Do not run it beside another local Gravity cluster that must stay alive. ## Build @@ -88,24 +76,15 @@ make MODE=quick-release gravity_node gravity_cli export PATH="$HOME/.foundry/bin:$PWD/target/quick-release:$PATH" ``` -Export the private Polygon RPC without printing it. For a standard shell-safe -environment file, either load it normally or extract only the required entry: - -```bash -export POLYGON_QUICKNONE_HTTP_URL="$( - sed -n 's/^POLYGON_QUICKNONE_HTTP_URL=//p' /path/to/private.env -)" -``` - -## Required Gate: 30-Minute Burn-In +## Quick Burn-In -Run this first. Validator restart is intentionally disabled for the burn-in; -restart recovery belongs to the 24-hour run. +Use a short no-restart run after changing task activation, payload handling, or +the price resolver: ```bash ORACLE_SOAK_DURATION_SECONDS=1800 \ ORACLE_SOAK_POLL_SECONDS=15 \ -ORACLE_SOAK_STALL_TIMEOUT_SECONDS=360 \ +ORACLE_SOAK_STALL_TIMEOUT_SECONDS=900 \ ORACLE_SOAK_MIN_ADVANCES=24 \ ORACLE_SOAK_RESTART_AFTER_SECONDS=0 \ ./gravity_e2e/run_test.sh \ @@ -114,41 +93,36 @@ ORACLE_SOAK_RESTART_AFTER_SECONDS=0 \ --log-cli-level=INFO ``` -Do not start the 24-hour run unless this command exits successfully and the -summary reports `status: passed`. - -## Focused Multi-Restart Regression +## Four-Hour Two-Restart Gate -Use this after changing validator startup, persistence, JWK consensus, or -relayer recovery. It restarts `node4` three times while the same live Binance -and Polygon tasks continue running: +This is the focused release-profile stability run. It schedules node4 restarts +one and three hours into the monitored four-hour period, away from the expected +two-hour epoch boundaries: ```bash -ORACLE_SOAK_DURATION_SECONDS=1200 \ +ORACLE_SOAK_DURATION_SECONDS=14400 \ ORACLE_SOAK_POLL_SECONDS=15 \ -ORACLE_SOAK_STALL_TIMEOUT_SECONDS=360 \ -ORACLE_SOAK_MIN_ADVANCES=15 \ -ORACLE_SOAK_RESTART_SCHEDULE_SECONDS=360,720,960 \ +ORACLE_SOAK_STALL_TIMEOUT_SECONDS=900 \ +ORACLE_SOAK_MIN_ADVANCES=192 \ +ORACLE_SOAK_RESTART_SCHEDULE_SECONDS=3600,10800 \ ./gravity_e2e/run_test.sh \ oracle_live_soak \ --force-init \ --log-cli-level=INFO ``` -The schedule is measured from the start of the monitored soak, after task -activation and initial quorum. A scheduled restart can run later than its -threshold when the epoch guard is closed. The summary must contain three -entries in `restartRecoveries`, and the final snapshot must still show all four -replicas and every relayer checkpoint caught up. +The summary must contain two entries in `restartRecoveries`; the final snapshot +must still show all four replicas, every relayer checkpoint caught up, and every +price callback count equal to its source nonce. -## 24-Hour Soak +## Optional 24-Hour Soak Run from a terminal multiplexer or another session that will stay alive: ```bash ORACLE_SOAK_DURATION_SECONDS=86400 \ ORACLE_SOAK_POLL_SECONDS=15 \ -ORACLE_SOAK_STALL_TIMEOUT_SECONDS=360 \ +ORACLE_SOAK_STALL_TIMEOUT_SECONDS=900 \ ./gravity_e2e/run_test.sh \ oracle_live_soak \ --force-init \ @@ -156,13 +130,11 @@ ORACLE_SOAK_STALL_TIMEOUT_SECONDS=360 \ ``` With the defaults, `node4` becomes eligible to restart after 12 hours and runs -as soon as it is outside the five-minute epoch-transition guard. The pytest -process is the monitor, performs final assertions, writes the report, and tears -down the cluster. Do not terminate it after merely seeing the first onchain -update. A long-lived node can spend more than the cluster harness's usual 30 -seconds reopening its persisted databases, so this suite gives restart RPC -recovery a three-minute window while still failing immediately if the process -exits. +as soon as the five-minute epoch-transition guard is open. The pytest process +is the monitor, performs final assertions, writes the report, and tears down the +cluster. A long-lived node can spend more than the harness's usual 30 seconds +reopening its persisted databases, so this suite gives restart RPC recovery a +three-minute window while still failing if the process exits. ## Controls @@ -170,7 +142,7 @@ exits. | --- | ---: | --- | | `ORACLE_SOAK_DURATION_SECONDS` | `86400` | Monitored soak duration | | `ORACLE_SOAK_POLL_SECONDS` | `15` | Onchain heartbeat interval | -| `ORACLE_SOAK_STALL_TIMEOUT_SECONDS` | `360` | Maximum feed or chain stall | +| `ORACLE_SOAK_STALL_TIMEOUT_SECONDS` | `900` | Maximum feed or chain stall | | `ORACLE_SOAK_MIN_ADVANCES` | 80% of expected minutes | Required nonce advances for each Binance feed | | `ORACLE_SOAK_RESTART_AFTER_SECONDS` | Halfway for runs >= 1 hour | Earliest restart time; `0` disables | | `ORACLE_SOAK_RESTART_SCHEDULE_SECONDS` | unset | Strictly increasing comma-separated restart times; mutually exclusive with `ORACLE_SOAK_RESTART_AFTER_SECONDS` | @@ -178,7 +150,6 @@ exits. | `ORACLE_SOAK_RESTART_RPC_TIMEOUT_SECONDS` | `180` | RPC recovery window for the restarted validator | | `BINANCE_PRICE_FEED_BASE_URL` | `https://testnet.binancefuture.com` | Binance Futures testnet base URL | | `BINANCE_PRICE_FEED_GRACE_MS` | `120000` | Closed-bucket safety delay | -| `POLYMARKET_GAMMA_URL` | `https://gamma-api.polymarket.com` | Market discovery API | ## Evidence @@ -186,7 +157,7 @@ Runtime evidence remains local and ignored by Git: - `artifacts/oracle_live_soak_heartbeat.jsonl`: one bounded checkpoint per poll; - `artifacts/oracle_live_soak_summary.json`: final PASS/FAIL report and restart - recovery time. + recovery times. -The summary is the acceptance artifact. A process that is still running has -not yet passed the soak. +The summary is the acceptance artifact. A process that is still running has not +yet passed the soak. diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/cluster.toml b/gravity_e2e/cluster_test_cases/oracle_live_soak/cluster.toml index 08ea537f..4dc9d61e 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/cluster.toml +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/cluster.toml @@ -1,4 +1,4 @@ -# Four-validator live Binance and Polymarket soak test. +# Four-validator live Binance price-feed soak test. # Keep fixed listeners below Linux's default ephemeral source-port range. [cluster] diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/genesis.toml b/gravity_e2e/cluster_test_cases/oracle_live_soak/genesis.toml index 29581c70..7cf62633 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/genesis.toml +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/genesis.toml @@ -1,5 +1,6 @@ -# Source types 3 and 6 are enabled at genesis, while their concrete tasks are -# registered by governance during the test. +# Source type 3 is enabled at genesis, while its concrete price-feed tasks are +# registered by governance during the test. Source type 6 stays disabled for +# the initial Binance-only launch profile. [dependencies.genesis_contracts] repo = "https://github.com/Galxe/gravity_chain_core_contracts.git" @@ -87,11 +88,10 @@ reconstruction_threshold = 12297829382473033728 fast_path_secrecy_threshold = 12297829382473033728 [genesis.oracle_config] -source_types = [1, 3, 6] +source_types = [1, 3] callbacks = [ "0x00000000000000000000000000000001625F4001", "0x0000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000", ] [genesis.jwk_config] diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/hooks.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/hooks.py index 8b014dc5..9045f864 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/hooks.py +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/hooks.py @@ -1,4 +1,4 @@ -"""Prepare live Binance and Polygon inputs before deploying the soak cluster.""" +"""Prepare live Binance inputs before deploying the soak cluster.""" import ipaddress import json @@ -6,8 +6,7 @@ import os from pathlib import Path import time -from urllib.parse import urlencode, urlparse -from urllib.request import Request, urlopen +from urllib.parse import urlparse try: import tomllib @@ -20,21 +19,12 @@ INTERVAL_MS = 60_000 DEFAULT_GRACE_MS = 120_000 DEFAULT_BINANCE_BASE_URL = "https://testnet.binancefuture.com" -DEFAULT_GAMMA_URL = "https://gamma-api.polymarket.com" BINANCE_FEEDS = ( {"feedId": 1001, "pair": "NVDAUSDT"}, {"feedId": 1002, "pair": "BTCUSDT"}, {"feedId": 1003, "pair": "ETHUSDT"}, ) -POLYGON_CHAIN_ID = 137 -CTF_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045" -CONDITION_RESOLUTION_TOPIC0 = ( - "0xb44d84d3289691f71497564b85d4233648d9dbae8cbdbb4329f301c3a0185894" -) -SCAN_CHUNK = 2_000 -MAX_SCAN_BLOCKS = 100_000 - _METADATA_FILE = "oracle_live_soak_metadata.json" _RELAYER_FILE = "relayer_config.live.json" _HEARTBEAT_FILE = "oracle_live_soak_heartbeat.jsonl" @@ -57,188 +47,6 @@ def _require_public_https_url(value: str, name: str) -> str: return value.rstrip("/") -def _read_json(request: Request) -> object: - with urlopen(request, timeout=30) as response: - return json.loads(response.read()) - - -def _rpc(rpc_url: str, method: str, params: list): - payload = json.dumps( - {"jsonrpc": "2.0", "id": 1, "method": method, "params": params} - ).encode() - body = _read_json( - Request( - rpc_url, - data=payload, - headers={"Content-Type": "application/json"}, - ) - ) - if not isinstance(body, dict) or "result" not in body: - detail = body.get("error") if isinstance(body, dict) else body - raise RuntimeError(f"Polygon {method} failed: {detail}") - return body["result"] - - -def _gamma_markets(base_url: str) -> list[dict]: - query = urlencode( - { - "closed": "true", - "limit": 100, - "order": "closedTime", - "ascending": "false", - } - ) - markets = _read_json( - Request( - f"{base_url}/markets?{query}", - headers={ - "Accept": "application/json", - "User-Agent": "gravity-oracle-e2e/1.0", - }, - ) - ) - if not isinstance(markets, list): - raise RuntimeError("Polymarket Gamma response is not a market list") - return markets - - -def _parse_list(value) -> list: - if isinstance(value, str): - value = json.loads(value) - return list(value or []) - - -def _binary_markets(base_url: str) -> dict[str, dict]: - result = {} - for market in _gamma_markets(base_url): - condition_id = market.get("conditionId") - outcomes = _parse_list(market.get("outcomes")) - if ( - isinstance(condition_id, str) - and len(condition_id) == 66 - and condition_id.startswith("0x") - and len(outcomes) == 2 - and market.get("id") is not None - ): - result[condition_id.lower()] = {**market, "outcomes": outcomes} - if not result: - raise RuntimeError("Gamma returned no recently closed binary markets") - return result - - -def _decode_resolution(log: dict) -> tuple[int, list[int]]: - data = bytes.fromhex(log["data"].removeprefix("0x")) - if len(data) < 96: - raise ValueError("ConditionResolution data is too short") - outcome_count = int.from_bytes(data[0:32], "big") - offset = int.from_bytes(data[32:64], "big") - if offset + 32 > len(data): - raise ValueError("ConditionResolution payout offset is invalid") - array_len = int.from_bytes(data[offset : offset + 32], "big") - end = offset + 32 + array_len * 32 - if end > len(data): - raise ValueError("ConditionResolution payouts exceed encoded data") - payouts = [ - int.from_bytes( - data[offset + 32 + index * 32 : offset + 64 + index * 32], - "big", - ) - for index in range(array_len) - ] - if outcome_count != array_len: - raise ValueError("ConditionResolution outcome count does not match payouts") - return outcome_count, payouts - - -def _discover_market(rpc_url: str, gamma_url: str) -> dict: - chain_id = int(_rpc(rpc_url, "eth_chainId", []), 16) - if chain_id != POLYGON_CHAIN_ID: - raise RuntimeError(f"Polygon RPC chainId must be 137, got {chain_id}") - finalized = _rpc(rpc_url, "eth_getBlockByNumber", ["finalized", False]) - if not finalized: - raise RuntimeError("Polygon RPC did not return a finalized block") - finalized_block = int(finalized["number"], 16) - markets = _binary_markets(gamma_url) - - scanned = 0 - end = finalized_block - while end >= 0 and scanned < MAX_SCAN_BLOCKS: - start = max(0, end - SCAN_CHUNK + 1) - logs = _rpc( - rpc_url, - "eth_getLogs", - [ - { - "fromBlock": hex(start), - "toBlock": hex(end), - "address": CTF_ADDRESS, - "topics": [CONDITION_RESOLUTION_TOPIC0], - } - ], - ) - logs.sort( - key=lambda item: ( - int(item["blockNumber"], 16), - int(item["logIndex"], 16), - ), - reverse=True, - ) - for log in logs: - if log.get("removed") or len(log.get("topics", [])) < 4: - continue - condition_id = log["topics"][1].lower() - market = markets.get(condition_id) - if market is None: - continue - outcome_count, payouts = _decode_resolution(log) - if outcome_count != 2 or sum(value > 0 for value in payouts) != 1: - continue - receipt = _rpc( - rpc_url, - "eth_getTransactionReceipt", - [log["transactionHash"]], - ) - if not receipt or int(receipt["status"], 16) != 1: - continue - - block_number = int(log["blockNumber"], 16) - mirror_id = int(market["id"]) - task_uri = ( - f"gravity://6/{mirror_id}/polymarket_settlement?" - f"ctf={CTF_ADDRESS}&condition={condition_id}&" - f"fromBlock={max(0, block_number - 1)}&chainId=137&" - "maxBlocksPerPoll=100" - ) - return { - "mirrorId": mirror_id, - "question": market.get("question"), - "outcomes": market["outcomes"], - "conditionId": condition_id, - "questionId": log["topics"][3], - "oracle": "0x" + log["topics"][2][-40:], - "transactionHash": log["transactionHash"], - "blockNumber": block_number, - "finalizedBlock": finalized_block, - "logIndex": int(log["logIndex"], 16), - "outcomeSlotCount": outcome_count, - "payoutNumerators": payouts, - "winningSlot": next( - index for index, value in enumerate(payouts) if value > 0 - ), - "taskUri": task_uri, - } - - scanned += end - start + 1 - if start == 0: - break - end = start - 1 - - raise RuntimeError( - "no recently closed binary Polymarket market had a finalized CTF " - f"resolution in the last {MAX_SCAN_BLOCKS} Polygon blocks" - ) - - def _bucket_start_ms(env: dict) -> int: configured = env.get("BINANCE_PRICE_FEED_BUCKET_START_MS") if configured: @@ -274,21 +82,6 @@ def pre_deploy(test_dir: Path, env: dict, pytest_args: list[str]): env.get("BINANCE_PRICE_FEED_BASE_URL", DEFAULT_BINANCE_BASE_URL), "BINANCE_PRICE_FEED_BASE_URL", ) - gamma_url = _require_public_https_url( - env.get("POLYMARKET_GAMMA_URL", DEFAULT_GAMMA_URL), - "POLYMARKET_GAMMA_URL", - ) - polygon_rpc_url = env.get("POLYGON_RPC_URL") or env.get( - "POLYGON_QUICKNONE_HTTP_URL" - ) - if not polygon_rpc_url: - raise RuntimeError( - "POLYGON_RPC_URL or POLYGON_QUICKNONE_HTTP_URL is required" - ) - polygon_rpc_url = _require_public_https_url( - polygon_rpc_url, "Polygon RPC URL" - ) - grace_ms = int(env.get("BINANCE_PRICE_FEED_GRACE_MS", DEFAULT_GRACE_MS)) if grace_ms < 0: raise RuntimeError("BINANCE_PRICE_FEED_GRACE_MS must be non-negative") @@ -305,8 +98,6 @@ def pre_deploy(test_dir: Path, env: dict, pytest_args: list[str]): } for feed in BINANCE_FEEDS ] - market = _discover_market(polygon_rpc_url, gamma_url) - artifacts = test_dir / "artifacts" artifacts.mkdir(parents=True, exist_ok=True) for name in (_HEARTBEAT_FILE, _SUMMARY_FILE): @@ -317,11 +108,8 @@ def pre_deploy(test_dir: Path, env: dict, pytest_args: list[str]): json.dumps( { "uri_mappings": { - **{ - feed["taskUri"]: binance_base_url - for feed in binance_feeds - }, - market["taskUri"]: polygon_rpc_url, + feed["taskUri"]: binance_base_url + for feed in binance_feeds } }, indent=2, @@ -331,10 +119,7 @@ def pre_deploy(test_dir: Path, env: dict, pytest_args: list[str]): metadata_path = artifacts / _METADATA_FILE metadata_path.write_text( json.dumps( - { - "binanceFeeds": binance_feeds, - "polymarket": market, - }, + {"binanceFeeds": binance_feeds}, indent=2, ensure_ascii=False, ) @@ -342,13 +127,9 @@ def pre_deploy(test_dir: Path, env: dict, pytest_args: list[str]): ) env["RELAYER_CONFIG_TPL"] = str(relayer_path) LOG.info( - "Prepared live Oracle inputs: Binance pairs=%s anchor=%s; Polymarket " - "mirrorId=%s block=%s finalized=%s", + "Prepared live Oracle inputs: Binance pairs=%s anchor=%s", [feed["pair"] for feed in binance_feeds], bucket_start_ms, - market["mirrorId"], - market["blockNumber"], - market["finalizedBlock"], ) diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py index b5ffb509..fb2b5854 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py @@ -1,4 +1,4 @@ -"""Manual four-validator soak against live Binance and Polygon data.""" +"""Manual four-validator soak against live Binance index-price data.""" import asyncio from dataclasses import dataclass @@ -13,6 +13,11 @@ from urllib.parse import urlencode from urllib.request import urlopen +try: + import tomllib +except ImportError: + import tomli as tomllib + import pytest from web3 import Web3 from web3._utils.abi import map_abi_data @@ -27,12 +32,6 @@ INTERVAL_MS = 60_000 DECIMALS = 8 -SOURCE_TYPE_POLYMARKET = 6 -POLYGON_CHAIN_ID = 137 -CTF_ADDRESS = Web3.to_checksum_address( - "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045" -) -POLYMARKET_TASK = Web3.keccak(text="polymarket_settlement") RECONFIGURATION_ADDRESS = Web3.to_checksum_address( "0x00000000000000000000000000000001625F2003" ) @@ -47,7 +46,7 @@ DEFAULT_SOAK_SECONDS = 24 * 60 * 60 DEFAULT_POLL_SECONDS = 15 -DEFAULT_STALL_SECONDS = 6 * 60 +DEFAULT_STALL_SECONDS = 15 * 60 DEFAULT_RESTART_RPC_TIMEOUT_SECONDS = 3 * 60 EPOCH_TIMEOUT_SECONDS = 180 ORACLE_TIMEOUT_SECONDS = 360 @@ -365,20 +364,6 @@ async def _wait_for_block( ) -async def _wait_for_settlement( - resolver, mirror_id: int, condition_id: bytes -) -> tuple: - deadline = time.monotonic() + ORACLE_TIMEOUT_SECONDS - while time.monotonic() < deadline: - settlement = tuple( - resolver.functions.getSettlement(mirror_id, condition_id).call() - ) - if settlement[0]: - return settlement - await asyncio.sleep(2) - raise TimeoutError(f"Polymarket mirror {mirror_id} was not settled") - - def _call_at_block_hash(function, block_hash: str): response = function.w3.provider.make_request( "eth_call", @@ -536,7 +521,7 @@ def __init__(self): self.eth = FakeEth() w3 = FakeWeb3() - assert _callback_success_logs(w3, 6, 42, 0) == [] + assert _callback_success_logs(w3, 3, 42, 0) == [] assert [ (request["fromBlock"], request["toBlock"]) for request in w3.eth.requests @@ -642,12 +627,8 @@ async def _replicated_snapshot_once( cluster: Cluster, native_artifact: dict, price_artifact: dict, - polymarket_artifact: dict, price_resolver_address: str, - polymarket_resolver_address: str, binance_feeds: list[dict], - mirror_id: int, - condition_id: bytes, ) -> dict: heights = {} for node_id, node in cluster.nodes.items(): @@ -680,9 +661,6 @@ async def _replicated_snapshot_once( price_resolver = node1.w3.eth.contract( address=price_resolver_address, abi=price_artifact["abi"] ) - polymarket_resolver = node1.w3.eth.contract( - address=polymarket_resolver_address, abi=polymarket_artifact["abi"] - ) price_feeds = {} for feed in binance_feeds: feed_id = int(feed["feedId"]) @@ -709,25 +687,6 @@ async def _replicated_snapshot_once( "progress": progress, "latestPrice": latest, } - polymarket_progress = tuple( - _call_at_block_hash( - native_oracle.functions.getSourceProgress( - SOURCE_TYPE_POLYMARKET, mirror_id - ), - snapshot_hash, - ) - ) - settlement = tuple( - _call_at_block_hash( - polymarket_resolver.functions.getSettlement( - mirror_id, condition_id - ), - snapshot_hash, - ) - ) - - assert polymarket_progress[0] == 1 - assert settlement[0] for node_id, node in cluster.nodes.items(): replica_native = node.w3.eth.contract( @@ -736,10 +695,6 @@ async def _replicated_snapshot_once( replica_price = node.w3.eth.contract( address=price_resolver_address, abi=price_artifact["abi"] ) - replica_polymarket = node.w3.eth.contract( - address=polymarket_resolver_address, - abi=polymarket_artifact["abi"], - ) for feed in binance_feeds: feed_id = int(feed["feedId"]) pair = feed["pair"] @@ -762,40 +717,12 @@ async def _replicated_snapshot_once( f"{replica_latest} != " f"{price_feeds[pair]['latestPrice']}" ) - replica_polymarket_progress = tuple( - _call_at_block_hash( - replica_native.functions.getSourceProgress( - SOURCE_TYPE_POLYMARKET, mirror_id - ), - snapshot_hash, - ) - ) - if replica_polymarket_progress != polymarket_progress: - raise SnapshotNotConverged( - f"{node_id} Polymarket progress diverged: " - f"{replica_polymarket_progress} != {polymarket_progress}" - ) - replica_settlement = tuple( - _call_at_block_hash( - replica_polymarket.functions.getSettlement( - mirror_id, condition_id - ), - snapshot_hash, - ) - ) - if replica_settlement != settlement: - raise SnapshotNotConverged( - f"{node_id} Polymarket settlement diverged: " - f"{replica_settlement} != {settlement}" - ) return { "block": snapshot_block, "blockHash": snapshot_hash, "heights": heights, "priceFeeds": price_feeds, - "polymarketProgress": polymarket_progress, - "settlement": settlement, } @@ -803,12 +730,8 @@ async def _replicated_snapshot( cluster: Cluster, native_artifact: dict, price_artifact: dict, - polymarket_artifact: dict, price_resolver_address: str, - polymarket_resolver_address: str, binance_feeds: list[dict], - mirror_id: int, - condition_id: bytes, ) -> dict: first_error = None for attempt in range(1, SNAPSHOT_CONVERGENCE_RETRIES + 1): @@ -817,12 +740,8 @@ async def _replicated_snapshot( cluster, native_artifact, price_artifact, - polymarket_artifact, price_resolver_address, - polymarket_resolver_address, binance_feeds, - mirror_id, - condition_id, ) if attempt > 1: LOG.info( @@ -879,15 +798,9 @@ async def _run_soak( settings: SoakSettings, native_artifact: dict, price_artifact: dict, - polymarket_artifact: dict, price_resolver_address: str, - polymarket_resolver_address: str, binance_feeds: list[dict], - polymarket: dict, - expected_settlement: tuple, ) -> dict: - mirror_id = int(polymarket["mirrorId"]) - condition_id = bytes.fromhex(polymarket["conditionId"].removeprefix("0x")) heartbeat_path = SUITE_DIR / "artifacts" / _HEARTBEAT_FILE heartbeat_path.unlink(missing_ok=True) @@ -906,12 +819,8 @@ async def _run_soak( cluster, native_artifact, price_artifact, - polymarket_artifact, price_resolver_address, - polymarket_resolver_address, binance_feeds, - mirror_id, - condition_id, ) initial_nonces = { pair: int(state["progress"][0]) @@ -991,12 +900,8 @@ async def _run_soak( cluster, native_artifact, price_artifact, - polymarket_artifact, price_resolver_address, - polymarket_resolver_address, binance_feeds, - mirror_id, - condition_id, ) now = time.monotonic() price_heartbeats = {} @@ -1049,12 +954,6 @@ async def _run_soak( f"{now - last_height_change[node_id]:.1f}s" ) - assert tuple(final["polymarketProgress"]) == ( - 1, - int(polymarket["blockNumber"]), - ) - assert tuple(final["settlement"]) == expected_settlement - samples += 1 heartbeat = { "elapsedSeconds": round(now - started, 3), @@ -1062,7 +961,6 @@ async def _run_soak( "gravityBlockHash": final["blockHash"], "nodeHeights": final["heights"], "priceFeeds": price_heartbeats, - "polymarketNonce": int(final["polymarketProgress"][0]), "restartCompleted": bool(settings.restart_schedule_seconds) and restart_index == len(settings.restart_schedule_seconds), "restartCount": restart_index, @@ -1078,12 +976,8 @@ async def _run_soak( cluster, native_artifact, price_artifact, - polymarket_artifact, price_resolver_address, - polymarket_resolver_address, binance_feeds, - mirror_id, - condition_id, ) if settings.restart_schedule_seconds: assert restart_index == len(settings.restart_schedule_seconds), ( @@ -1151,8 +1045,6 @@ async def _run_soak( ), "restartRecoveries": restart_recoveries, "restartEpochGuardDeferrals": restart_epoch_guard_deferrals, - "polymarketMirrorId": mirror_id, - "polymarketNonce": int(final["polymarketProgress"][0]), } @@ -1162,13 +1054,13 @@ def _write_summary(payload: dict) -> None: @pytest.mark.asyncio -async def test_governance_activated_oracles_soak_for_configured_duration( +async def test_governance_activated_price_feeds_soak_for_configured_duration( cluster: Cluster, ): settings = _soak_settings() metadata = _metadata() + assert set(metadata) == {"binanceFeeds"} binance_feeds = metadata["binanceFeeds"] - polymarket = metadata["polymarket"] assert [feed["pair"] for feed in binance_feeds] == [ "NVDAUSDT", "BTCUSDT", @@ -1177,8 +1069,16 @@ async def test_governance_activated_oracles_soak_for_configured_duration( assert len({int(feed["feedId"]) for feed in binance_feeds}) == len( binance_feeds ) - mirror_id = int(polymarket["mirrorId"]) - condition_id = bytes.fromhex(polymarket["conditionId"].removeprefix("0x")) + relayer_config = json.loads( + (SUITE_DIR / "artifacts" / "relayer_config.live.json").read_text() + ) + expected_uris = {feed["taskUri"] for feed in binance_feeds} + assert set(relayer_config["uri_mappings"]) == expected_uris + assert all(uri.startswith("gravity://3/") for uri in expected_uris) + with (SUITE_DIR / "genesis.toml").open("rb") as genesis_file: + oracle_config = tomllib.load(genesis_file)["genesis"]["oracle_config"] + assert oracle_config["source_types"] == [1, 3] + assert len(oracle_config["callbacks"]) == 2 assert len(cluster.nodes) == 4 assert await cluster.set_full_live(timeout=180) @@ -1191,7 +1091,6 @@ async def test_governance_activated_oracles_soak_for_configured_duration( w3 = node1.w3 required = [ ("PriceFeedResolver.sol", "PriceFeedResolver"), - ("PolymarketSettlementResolver.sol", "PolymarketSettlementResolver"), ("NativeOracle.sol", "NativeOracle"), ("OracleTaskConfig.sol", "OracleTaskConfig"), ("EpochConfig.sol", "EpochConfig"), @@ -1200,11 +1099,6 @@ async def test_governance_activated_oracles_soak_for_configured_duration( price_artifact = support.load_artifact( contracts_out, "PriceFeedResolver.sol", "PriceFeedResolver" ) - polymarket_artifact = support.load_artifact( - contracts_out, - "PolymarketSettlementResolver.sol", - "PolymarketSettlementResolver", - ) native_artifact = support.load_artifact( contracts_out, "NativeOracle.sol", "NativeOracle" ) @@ -1216,7 +1110,6 @@ async def test_governance_activated_oracles_soak_for_configured_duration( ) price_resolver = support.deploy_contract(w3, price_artifact) - polymarket_resolver = support.deploy_contract(w3, polymarket_artifact) native_oracle = w3.eth.contract( address=support.NATIVE_ORACLE_ADDRESS, abi=native_artifact["abi"] ) @@ -1239,9 +1132,6 @@ async def test_governance_activated_oracles_soak_for_configured_duration( int(feed["feedId"]), support.TASK_PRICE_FEED, ).call() - assert not task_config.functions.hasTask( - SOURCE_TYPE_POLYMARKET, mirror_id, POLYMARKET_TASK - ).call() setup_epoch = await _wait_for_proposal_window(cluster) targets = [EPOCH_CONFIG_ADDRESS, support.NATIVE_ORACLE_ADDRESS] @@ -1269,48 +1159,12 @@ async def test_governance_activated_oracles_soak_for_configured_duration( ) ) ) - targets.extend( - [ - support.ORACLE_TASK_CONFIG_ADDRESS, - support.NATIVE_ORACLE_ADDRESS, - polymarket_resolver.address, - ] - ) - datas.extend( - [ - support.function_calldata( - task_config.functions.setTask( - SOURCE_TYPE_POLYMARKET, - mirror_id, - POLYMARKET_TASK, - polymarket["taskUri"].encode(), - ) - ), - support.function_calldata( - native_oracle.functions.setCallback( - SOURCE_TYPE_POLYMARKET, - mirror_id, - polymarket_resolver.address, - ) - ), - support.function_calldata( - polymarket_resolver.functions.registerMirror( - mirror_id, - POLYGON_CHAIN_ID, - CTF_ADDRESS, - condition_id, - int(polymarket["outcomeSlotCount"]), - ) - ), - ] - ) receipt = await support.execute_governance_proposal( w3, support.faucet_voting_pool(w3), targets, datas, - "activate-live-binance-and-polymarket-soak", - gas=8_000_000, + "activate-live-binance-price-soak", ) assert _current_epoch(w3) == setup_epoch assert ( @@ -1330,14 +1184,6 @@ async def test_governance_activated_oracles_soak_for_configured_duration( ) for feed in binance_feeds ] - configured_tasks.append( - ( - SOURCE_TYPE_POLYMARKET, - mirror_id, - POLYMARKET_TASK, - polymarket["taskUri"], - ) - ) for source_type, source_id, task_name, expected_uri in configured_tasks: assert task_config.functions.hasTask( source_type, source_id, task_name @@ -1351,8 +1197,7 @@ async def test_governance_activated_oracles_soak_for_configured_duration( ).call() == 0 issuers = [ - *(f"gravity://3/{int(feed['feedId'])}" for feed in binance_feeds), - f"gravity://6/{mirror_id}", + f"gravity://3/{int(feed['feedId'])}" for feed in binance_feeds ] for node_id in cluster.nodes: for issuer in issuers: @@ -1379,66 +1224,6 @@ async def test_governance_activated_oracles_soak_for_configured_duration( 1, timeout=ORACLE_TIMEOUT_SECONDS, ) - deadline = time.monotonic() + ORACLE_TIMEOUT_SECONDS - while time.monotonic() < deadline: - polymarket_progress = tuple( - native_oracle.functions.getSourceProgress( - SOURCE_TYPE_POLYMARKET, mirror_id - ).call() - ) - if polymarket_progress[0] >= 1: - break - await asyncio.sleep(2) - else: - raise TimeoutError("Polymarket source did not reach nonce 1") - - assert polymarket_progress == (1, int(polymarket["blockNumber"])) - settlement = await _wait_for_settlement( - polymarket_resolver, mirror_id, condition_id - ) - assert settlement[1] == 1 - assert settlement[2] == POLYGON_CHAIN_ID - assert Web3.to_checksum_address(settlement[3]) == CTF_ADDRESS - assert Web3.to_checksum_address(settlement[4]) == Web3.to_checksum_address( - polymarket["oracle"] - ) - assert Web3.to_hex(settlement[5]).lower() == polymarket["questionId"].lower() - assert settlement[6] == int(polymarket["outcomeSlotCount"]) - assert Web3.to_hex(settlement[7]).lower() == polymarket[ - "transactionHash" - ].lower() - assert settlement[8] == int(polymarket["logIndex"]) - observation = tuple( - polymarket_resolver.functions.getSettlementObservation( - mirror_id, condition_id - ).call() - ) - assert observation[0] == 1 - expected_winning_slot = next( - index - for index, payout in enumerate(polymarket["payoutNumerators"]) - if payout > 0 - ) - assert observation[1] == expected_winning_slot - assert observation[2] == 1 - assert Web3.to_hex(observation[4]).lower() == polymarket[ - "transactionHash" - ].lower() - assert observation[5] == int(polymarket["logIndex"]) - resolution_events = ( - polymarket_resolver.events.PolymarketConditionResolved().get_logs( - from_block=receipt["blockNumber"], - to_block="latest", - argument_filters={ - "mirrorId": mirror_id, - "conditionId": condition_id, - }, - ) - ) - assert len(resolution_events) == 1 - assert list(resolution_events[0]["args"]["payoutNumerators"]) == polymarket[ - "payoutNumerators" - ] for feed in binance_feeds: price_progress, initial_price = initial_prices[feed["pair"]] @@ -1464,12 +1249,8 @@ async def test_governance_activated_oracles_soak_for_configured_duration( settings, native_artifact, price_artifact, - polymarket_artifact, price_resolver.address, - polymarket_resolver.address, binance_feeds, - polymarket, - settlement, ) except BaseException as error: last_heartbeat = None @@ -1495,7 +1276,6 @@ async def test_governance_activated_oracles_soak_for_configured_duration( "binancePairs": [ feed["pair"] for feed in binance_feeds ], - "polymarketMirrorId": mirror_id, "lastHeartbeat": last_heartbeat, } ) @@ -1517,24 +1297,11 @@ async def test_governance_activated_oracles_soak_for_configured_duration( f"{pair} callback count does not match final source nonce" ) price_delivery_counts[pair] = delivery_count - polymarket_delivery_count = len( - _callback_success_logs( - w3, - SOURCE_TYPE_POLYMARKET, - mirror_id, - receipt["blockNumber"], - summary["finalGravityBlock"], - ) - ) - assert polymarket_delivery_count == 1, ( - "Polymarket settlement was delivered more than once" - ) summary.update( { "activationEpoch": activation_epoch, "governanceBlock": receipt["blockNumber"], "priceCallbackEvents": price_delivery_counts, - "polymarketCallbackEvents": polymarket_delivery_count, "soakEpochIntervalSeconds": ( SOAK_EPOCH_INTERVAL_MICROS // 1_000_000 ), diff --git a/gravity_e2e/gravity_e2e/utils/oracle_test_support.py b/gravity_e2e/gravity_e2e/utils/oracle_test_support.py index d0d3e164..ddd0ff47 100644 --- a/gravity_e2e/gravity_e2e/utils/oracle_test_support.py +++ b/gravity_e2e/gravity_e2e/utils/oracle_test_support.py @@ -219,8 +219,6 @@ async def execute_governance_proposal( targets: list[str], datas: list[bytes], description: str, - *, - gas: int = 5_000_000, ) -> dict: assert len(targets) == len(datas) and targets _ensure_faucet_executor(w3) @@ -232,9 +230,9 @@ async def execute_governance_proposal( "from": FAUCET_ADDRESS, "to": GOVERNANCE_ADDRESS, "data": create_data, - "gas": gas, + "gas": 5_000_000, }) - receipt = _send_tx(w3, GOVERNANCE_ADDRESS, create_data, gas=gas) + receipt = _send_tx(w3, GOVERNANCE_ADDRESS, create_data, gas=5_000_000) proposal_id = next( ( int.from_bytes(log["topics"][1], "big") @@ -276,7 +274,7 @@ async def execute_governance_proposal( ["uint64", "address[]", "bytes[]"], [proposal_id, targets, datas], ) - return _send_tx(w3, GOVERNANCE_ADDRESS, execute_data, gas=gas) + return _send_tx(w3, GOVERNANCE_ADDRESS, execute_data, gas=5_000_000) async def wait_for_latest_price( From c5763079f19e3e19eba594c9bfefdca17674d508 Mon Sep 17 00:00:00 2001 From: ByteYue Date: Mon, 10 Aug 2026 16:01:11 +0800 Subject: [PATCH 12/12] test(oracle): validate packed price feed v1 --- Cargo.lock | 238 +++++++++--------- Cargo.toml | 2 +- bin/gravity_node/Cargo.toml | 2 +- .../genesis.toml | 2 +- .../test_binance_price_feed_multivalidator.py | 37 ++- .../oracle_live_soak/README.md | 26 ++ .../oracle_live_soak/genesis.toml | 2 +- .../oracle_live_soak/test_oracle_live_soak.py | 26 +- .../gravity_e2e/utils/oracle_test_support.py | 2 +- 9 files changed, 195 insertions(+), 142 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 223474e6..086981ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4981,7 +4981,7 @@ dependencies = [ "bitflags 2.13.1", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools 0.12.1", "proc-macro2", "quote", "regex", @@ -8519,7 +8519,7 @@ dependencies = [ [[package]] name = "gravity-precompiles" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "blst", @@ -8531,7 +8531,7 @@ dependencies = [ [[package]] name = "gravity-primitives" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" [[package]] name = "gravity-sdk" @@ -8545,7 +8545,7 @@ dependencies = [ [[package]] name = "gravity-storage" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "async-trait", @@ -8668,7 +8668,7 @@ dependencies = [ [[package]] name = "greth" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "async-trait", "gravity-primitives", @@ -8709,7 +8709,7 @@ dependencies = [ [[package]] name = "grevm" version = "2.2.2" -source = "git+https://github.com/Galxe/grevm?rev=dcd1460b20d9f3d793c8309c28b0b7401be91523#dcd1460b20d9f3d793c8309c28b0b7401be91523" +source = "git+https://github.com/Galxe/grevm?rev=25b71edcac1b94be7fc5ee0e4f20e5fcce6908ad#25b71edcac1b94be7fc5ee0e4f20e5fcce6908ad" dependencies = [ "ahash 0.8.12", "alloy-evm", @@ -12274,7 +12274,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 2.0.114", @@ -13671,7 +13671,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.12.1", "proc-macro2", "quote", "syn 2.0.114", @@ -13684,7 +13684,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.12.1", "proc-macro2", "quote", "syn 2.0.114", @@ -14539,7 +14539,7 @@ checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "reth" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "alloy-rpc-types 2.0.5", @@ -14586,7 +14586,7 @@ dependencies = [ [[package]] name = "reth-basic-payload-builder" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -14613,7 +14613,7 @@ dependencies = [ [[package]] name = "reth-chain-state" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -14642,7 +14642,7 @@ dependencies = [ [[package]] name = "reth-chainspec" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-chains", "alloy-consensus 2.0.5", @@ -14662,7 +14662,7 @@ dependencies = [ [[package]] name = "reth-cli" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-genesis", "clap 4.5.57", @@ -14675,7 +14675,7 @@ dependencies = [ [[package]] name = "reth-cli-commands" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-chains", "alloy-consensus 2.0.5", @@ -14759,7 +14759,7 @@ dependencies = [ [[package]] name = "reth-cli-runner" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "reth-tasks", "tokio", @@ -14769,7 +14769,7 @@ dependencies = [ [[package]] name = "reth-cli-util" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-eips 2.0.5", "alloy-primitives", @@ -14818,7 +14818,7 @@ dependencies = [ [[package]] name = "reth-config" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "eyre", "humantime-serde", @@ -14834,7 +14834,7 @@ dependencies = [ [[package]] name = "reth-consensus" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eip7928 0.4.5", @@ -14848,7 +14848,7 @@ dependencies = [ [[package]] name = "reth-consensus-common" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -14861,7 +14861,7 @@ dependencies = [ [[package]] name = "reth-consensus-debug-client" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -14886,7 +14886,7 @@ dependencies = [ [[package]] name = "reth-db" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "derive_more 2.1.1", @@ -14910,7 +14910,7 @@ dependencies = [ [[package]] name = "reth-db-api" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-genesis", @@ -14936,7 +14936,7 @@ dependencies = [ [[package]] name = "reth-db-common" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-genesis", @@ -14967,7 +14967,7 @@ dependencies = [ [[package]] name = "reth-db-models" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-eips 2.0.5", "alloy-primitives", @@ -14981,7 +14981,7 @@ dependencies = [ [[package]] name = "reth-discv4" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -15006,7 +15006,7 @@ dependencies = [ [[package]] name = "reth-discv5" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -15030,7 +15030,7 @@ dependencies = [ [[package]] name = "reth-dns-discovery" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "dashmap 6.2.1", @@ -15054,7 +15054,7 @@ dependencies = [ [[package]] name = "reth-downloaders" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -15085,7 +15085,7 @@ dependencies = [ [[package]] name = "reth-ecies" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "aes", "alloy-primitives", @@ -15113,7 +15113,7 @@ dependencies = [ [[package]] name = "reth-engine-local" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-primitives", @@ -15136,7 +15136,7 @@ dependencies = [ [[package]] name = "reth-engine-primitives" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -15161,7 +15161,7 @@ dependencies = [ [[package]] name = "reth-engine-tree" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -15216,7 +15216,7 @@ dependencies = [ [[package]] name = "reth-engine-util" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-primitives", @@ -15246,7 +15246,7 @@ dependencies = [ [[package]] name = "reth-era" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -15262,7 +15262,7 @@ dependencies = [ [[package]] name = "reth-era-downloader" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "bytes", @@ -15278,7 +15278,7 @@ dependencies = [ [[package]] name = "reth-era-utils" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-primitives", @@ -15300,7 +15300,7 @@ dependencies = [ [[package]] name = "reth-errors" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "reth-consensus", "reth-execution-errors", @@ -15311,7 +15311,7 @@ dependencies = [ [[package]] name = "reth-eth-wire" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-chains", "alloy-primitives", @@ -15339,7 +15339,7 @@ dependencies = [ [[package]] name = "reth-eth-wire-types" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-chains", "alloy-consensus 2.0.5", @@ -15361,7 +15361,7 @@ dependencies = [ [[package]] name = "reth-ethereum-cli" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "clap 4.5.57", "eyre", @@ -15384,7 +15384,7 @@ dependencies = [ [[package]] name = "reth-ethereum-consensus" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -15400,7 +15400,7 @@ dependencies = [ [[package]] name = "reth-ethereum-engine-primitives" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-eips 2.0.5", "alloy-primitives", @@ -15416,7 +15416,7 @@ dependencies = [ [[package]] name = "reth-ethereum-forks" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-eip2124", "alloy-hardforks", @@ -15429,7 +15429,7 @@ dependencies = [ [[package]] name = "reth-ethereum-payload-builder" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -15459,7 +15459,7 @@ dependencies = [ [[package]] name = "reth-ethereum-primitives" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -15473,7 +15473,7 @@ dependencies = [ [[package]] name = "reth-etl" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "rayon", "reth-db-api", @@ -15483,7 +15483,7 @@ dependencies = [ [[package]] name = "reth-evm" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eip7928 0.4.5", @@ -15493,6 +15493,7 @@ dependencies = [ "auto_impl", "derive_more 2.1.1", "futures-util", + "grevm", "metrics", "rayon", "reth-execution-errors", @@ -15508,7 +15509,7 @@ dependencies = [ [[package]] name = "reth-evm-ethereum" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -15533,7 +15534,7 @@ dependencies = [ [[package]] name = "reth-execution-cache" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "fixed-cache", @@ -15551,7 +15552,7 @@ dependencies = [ [[package]] name = "reth-execution-errors" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-evm", "alloy-primitives", @@ -15564,7 +15565,7 @@ dependencies = [ [[package]] name = "reth-execution-types" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -15583,7 +15584,7 @@ dependencies = [ [[package]] name = "reth-exex" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -15621,7 +15622,7 @@ dependencies = [ [[package]] name = "reth-exex-types" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-eips 2.0.5", "alloy-primitives", @@ -15635,7 +15636,7 @@ dependencies = [ [[package]] name = "reth-fs-util" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "serde", "serde_json", @@ -15645,7 +15646,7 @@ dependencies = [ [[package]] name = "reth-invalid-block-hooks" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-primitives", @@ -15673,7 +15674,7 @@ dependencies = [ [[package]] name = "reth-ipc" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "bytes", "futures", @@ -15693,7 +15694,7 @@ dependencies = [ [[package]] name = "reth-libmdbx" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "bitflags 2.13.1", "byteorder", @@ -15710,7 +15711,7 @@ dependencies = [ [[package]] name = "reth-mdbx-sys" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "bindgen", "cc", @@ -15719,7 +15720,7 @@ dependencies = [ [[package]] name = "reth-metrics" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "futures", "metrics", @@ -15732,7 +15733,7 @@ dependencies = [ [[package]] name = "reth-net-banlist" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "ipnet", @@ -15741,7 +15742,7 @@ dependencies = [ [[package]] name = "reth-net-nat" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "futures-util", "if-addrs", @@ -15755,7 +15756,7 @@ dependencies = [ [[package]] name = "reth-network" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -15812,7 +15813,7 @@ dependencies = [ [[package]] name = "reth-network-api" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-primitives", @@ -15837,7 +15838,7 @@ dependencies = [ [[package]] name = "reth-network-p2p" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -15859,7 +15860,7 @@ dependencies = [ [[package]] name = "reth-network-peers" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -15874,7 +15875,7 @@ dependencies = [ [[package]] name = "reth-network-types" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-eip2124", "humantime-serde", @@ -15888,7 +15889,7 @@ dependencies = [ [[package]] name = "reth-nippy-jar" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "anyhow", "bincode", @@ -15905,7 +15906,7 @@ dependencies = [ [[package]] name = "reth-node-api" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-rpc-types-engine", "eyre", @@ -15929,7 +15930,7 @@ dependencies = [ [[package]] name = "reth-node-builder" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -15998,7 +15999,7 @@ dependencies = [ [[package]] name = "reth-node-core" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -16054,7 +16055,7 @@ dependencies = [ [[package]] name = "reth-node-ethereum" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -16101,7 +16102,7 @@ dependencies = [ [[package]] name = "reth-node-ethstats" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-primitives", @@ -16125,7 +16126,7 @@ dependencies = [ [[package]] name = "reth-node-events" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -16149,7 +16150,7 @@ dependencies = [ [[package]] name = "reth-node-metrics" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "bytes", "eyre", @@ -16173,7 +16174,7 @@ dependencies = [ [[package]] name = "reth-node-types" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "reth-chainspec", "reth-db-api", @@ -16185,7 +16186,7 @@ dependencies = [ [[package]] name = "reth-payload-builder" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-primitives", @@ -16209,7 +16210,7 @@ dependencies = [ [[package]] name = "reth-payload-builder-primitives" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "pin-project", "reth-payload-primitives", @@ -16221,7 +16222,7 @@ dependencies = [ [[package]] name = "reth-payload-primitives" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -16244,7 +16245,7 @@ dependencies = [ [[package]] name = "reth-payload-validator" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-rpc-types-engine", @@ -16254,7 +16255,7 @@ dependencies = [ [[package]] name = "reth-pipe-exec-layer-event-bus" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "reth-chain-state", @@ -16267,7 +16268,7 @@ dependencies = [ [[package]] name = "reth-pipe-exec-layer-ext-v2" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -16314,7 +16315,7 @@ dependencies = [ [[package]] name = "reth-pipe-exec-layer-relayer" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-network 2.0.5", "alloy-primitives", @@ -16338,7 +16339,7 @@ dependencies = [ [[package]] name = "reth-primitives" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "once_cell", @@ -16351,7 +16352,7 @@ dependencies = [ [[package]] name = "reth-primitives-traits" version = "0.4.1" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -16381,7 +16382,7 @@ dependencies = [ [[package]] name = "reth-provider" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -16424,7 +16425,7 @@ dependencies = [ [[package]] name = "reth-prune" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -16452,7 +16453,7 @@ dependencies = [ [[package]] name = "reth-prune-types" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "derive_more 2.1.1", @@ -16467,7 +16468,7 @@ dependencies = [ [[package]] name = "reth-ress-protocol" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-primitives", @@ -16486,7 +16487,7 @@ dependencies = [ [[package]] name = "reth-ress-provider" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-primitives", @@ -16513,7 +16514,7 @@ dependencies = [ [[package]] name = "reth-revm" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -16528,7 +16529,7 @@ dependencies = [ [[package]] name = "reth-rpc" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-dyn-abi", @@ -16605,7 +16606,7 @@ dependencies = [ [[package]] name = "reth-rpc-api" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-eips 2.0.5", "alloy-genesis", @@ -16635,7 +16636,7 @@ dependencies = [ [[package]] name = "reth-rpc-builder" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-network 2.0.5", "alloy-provider 2.0.5", @@ -16678,7 +16679,7 @@ dependencies = [ [[package]] name = "reth-rpc-convert" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-evm", @@ -16699,7 +16700,7 @@ dependencies = [ [[package]] name = "reth-rpc-engine-api" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-eips 2.0.5", "alloy-primitives", @@ -16730,7 +16731,7 @@ dependencies = [ [[package]] name = "reth-rpc-eth-api" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-dyn-abi", @@ -16776,7 +16777,7 @@ dependencies = [ [[package]] name = "reth-rpc-eth-types" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-chains", "alloy-consensus 2.0.5", @@ -16826,7 +16827,7 @@ dependencies = [ [[package]] name = "reth-rpc-layer" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-rpc-types-engine", "http 1.4.0", @@ -16840,7 +16841,7 @@ dependencies = [ [[package]] name = "reth-rpc-server-types" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-eips 2.0.5", "alloy-primitives", @@ -16871,7 +16872,7 @@ dependencies = [ [[package]] name = "reth-stages" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-primitives", @@ -16915,7 +16916,7 @@ dependencies = [ [[package]] name = "reth-stages-api" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-eips 2.0.5", "alloy-primitives", @@ -16943,7 +16944,7 @@ dependencies = [ [[package]] name = "reth-stages-types" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "bytes", @@ -16956,7 +16957,7 @@ dependencies = [ [[package]] name = "reth-static-file" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "parking_lot 0.12.5", @@ -16976,7 +16977,7 @@ dependencies = [ [[package]] name = "reth-static-file-types" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "clap 4.5.57", @@ -16990,7 +16991,7 @@ dependencies = [ [[package]] name = "reth-storage-api" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -17020,7 +17021,7 @@ dependencies = [ [[package]] name = "reth-storage-errors" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-eips 2.0.5", "alloy-primitives", @@ -17038,7 +17039,7 @@ dependencies = [ [[package]] name = "reth-tasks" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "crossbeam-utils", "dashmap 6.2.1", @@ -17059,7 +17060,7 @@ dependencies = [ [[package]] name = "reth-tokio-util" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "tokio", "tokio-stream", @@ -17069,7 +17070,7 @@ dependencies = [ [[package]] name = "reth-tracing" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "clap 4.5.57", "eyre", @@ -17087,7 +17088,7 @@ dependencies = [ [[package]] name = "reth-tracing-otlp" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "base64 0.22.1", "clap 4.5.57", @@ -17105,7 +17106,7 @@ dependencies = [ [[package]] name = "reth-transaction-pool" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -17151,7 +17152,7 @@ dependencies = [ [[package]] name = "reth-trie" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-eips 2.0.5", @@ -17176,7 +17177,7 @@ dependencies = [ [[package]] name = "reth-trie-common" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-consensus 2.0.5", "alloy-primitives", @@ -17186,7 +17187,6 @@ dependencies = [ "alloy-trie", "bytes", "derive_more 2.1.1", - "gravity-primitives", "itertools 0.14.0", "nybbles", "once_cell", @@ -17203,7 +17203,7 @@ dependencies = [ [[package]] name = "reth-trie-db" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -17222,7 +17222,7 @@ dependencies = [ [[package]] name = "reth-trie-parallel" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-eip7928 0.4.5", "alloy-evm", @@ -17251,7 +17251,7 @@ dependencies = [ [[package]] name = "reth-trie-sparse" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -17272,7 +17272,7 @@ dependencies = [ [[package]] name = "reth-trie-sparse-parallel" version = "2.3.0" -source = "git+https://github.com/Galxe/gravity-reth?rev=0c038e180cc26ae400f3fb2e6199e6f079f68fbc#0c038e180cc26ae400f3fb2e6199e6f079f68fbc" +source = "git+https://github.com/Galxe/gravity-reth?rev=e63817884bd015b73a9d3568828e4f50a07cd83a#e63817884bd015b73a9d3568828e4f50a07cd83a" dependencies = [ "alloy-primitives", "alloy-rlp", diff --git a/Cargo.toml b/Cargo.toml index 15383c26..81fe4e09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -496,7 +496,7 @@ debug-assertions = true debug = true [patch.crates-io] -reth-primitives-traits = { git = "https://github.com/Galxe/gravity-reth", rev = "0c038e180cc26ae400f3fb2e6199e6f079f68fbc" } +reth-primitives-traits = { git = "https://github.com/Galxe/gravity-reth", rev = "e63817884bd015b73a9d3568828e4f50a07cd83a" } serde-reflection = { git = "https://github.com/aptos-labs/serde-reflection", rev = "73b6bbf748334b71ff6d7d09d06a29e3062ca075" } merlin = { git = "https://github.com/aptos-labs/merlin" } tonic = { git = "https://github.com/aptos-labs/tonic.git", rev = "0da1ba8b1751d6e19eb55be24cccf9ae933c666e" } diff --git a/bin/gravity_node/Cargo.toml b/bin/gravity_node/Cargo.toml index 63c6b092..a9d8f957 100644 --- a/bin/gravity_node/Cargo.toml +++ b/bin/gravity_node/Cargo.toml @@ -33,7 +33,7 @@ sha2.workspace = true bincode = "1.3" time = "0.3.36" anyhow = "1.0.87" -greth = { git = "https://github.com/Galxe/gravity-reth", rev = "0c038e180cc26ae400f3fb2e6199e6f079f68fbc" } +greth = { git = "https://github.com/Galxe/gravity-reth", rev = "e63817884bd015b73a9d3568828e4f50a07cd83a" } reqwest = "0.12.9" alloy-primitives = { version = "=1.6.0", default-features = false, features = ["map-foldhash"] } alloy-eips = { version = "=2.0.5", default-features = false } diff --git a/gravity_e2e/cluster_test_cases/binance_price_feed_multivalidator/genesis.toml b/gravity_e2e/cluster_test_cases/binance_price_feed_multivalidator/genesis.toml index 9acb8ab1..914c0faf 100644 --- a/gravity_e2e/cluster_test_cases/binance_price_feed_multivalidator/genesis.toml +++ b/gravity_e2e/cluster_test_cases/binance_price_feed_multivalidator/genesis.toml @@ -1,6 +1,6 @@ [dependencies.genesis_contracts] repo = "https://github.com/Galxe/gravity_chain_core_contracts.git" -ref = "ccd88ee62d90d38f824def26b32063fd094807c7" +ref = "d90d81583b95adac73e86c8aafd27530414df03f" # node1 is the faucet so the test can execute the governance setup with one # development key. All four validators retain equal JWK voting power. diff --git a/gravity_e2e/cluster_test_cases/binance_price_feed_multivalidator/test_binance_price_feed_multivalidator.py b/gravity_e2e/cluster_test_cases/binance_price_feed_multivalidator/test_binance_price_feed_multivalidator.py index 8f0fbb7d..806c7343 100644 --- a/gravity_e2e/cluster_test_cases/binance_price_feed_multivalidator/test_binance_price_feed_multivalidator.py +++ b/gravity_e2e/cluster_test_cases/binance_price_feed_multivalidator/test_binance_price_feed_multivalidator.py @@ -7,6 +7,7 @@ import time import pytest +from eth_abi import decode from gravity_e2e.cluster.manager import Cluster from gravity_e2e.utils import oracle_test_support as support @@ -24,6 +25,36 @@ FEEDS = {1001: "NVDAUSDT", 1002: "TSLAUSDT"} TARGET_NONCE = 1 QUORUM_VALIDATORS = 3 +PRICE_V1_GOLDEN_INNER = bytes.fromhex( + "0100000000000007d101b2e020018e23f2365f0000000000000009543637a800" +) +PRICE_V1_GOLDEN_WRAPPER = bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000020" + "0000000000000000000000000000000000000000000000000000000000000001" + "0000000000000000000000000000000000000000000000000000018e23f2365f" + "0000000000000000000000000000000000000000000000000000000000000060" + "0000000000000000000000000000000000000000000000000000000000000020" + "0100000000000007d101b2e020018e23f2365f0000000000000009543637a800" +) + + +def test_price_v1_golden_vectors_match_contracts_and_reth(): + payload = PRICE_V1_GOLDEN_INNER + assert len(payload) == 32 + assert payload[0] == 1 + assert int.from_bytes(payload[1:9], "big") == 2001 + assert int.from_bytes(payload[9:13], "big") == 28_500_000 + assert int.from_bytes(payload[13:19], "big") == 1_710_000_059_999 + assert int.from_bytes(payload[19:31], "big") == 40_067_545_000 + assert payload[31] == 0 + + assert len(PRICE_V1_GOLDEN_WRAPPER) == 192 + nonce, source_position, callback_body = decode( + ["(uint128,uint256,bytes)"], PRICE_V1_GOLDEN_WRAPPER + )[0] + assert nonce == 1 + assert source_position == 1_710_000_059_999 + assert callback_body == payload def _price_feed_uri(feed_id: int, pair: str) -> str: @@ -235,13 +266,11 @@ async def test_four_validators_resolve_deterministic_binance_prices(cluster: Clu delivery_nonce = int(progress[0]) bucket_start = BUCKET_START_MS + (delivery_nonce - 1) * INTERVAL_MS assert latest == ( - True, bucket_start // INTERVAL_MS, bucket_start + INTERVAL_MS - 1, - DECIMALS, mock_scaled_price(pair, delivery_nonce - 1), ) - assert progress[1] == latest[2] + assert progress[1] == latest[1] expected_progress[feed_id] = progress expected_rounds[feed_id] = latest @@ -272,5 +301,5 @@ async def test_four_validators_resolve_deterministic_binance_prices(cluster: Clu LOG.info( "Four-validator Binance QC stored rounds: %s", - {feed_id: round_data[1] for feed_id, round_data in expected_rounds.items()}, + {feed_id: round_data[0] for feed_id, round_data in expected_rounds.items()}, ) diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md index ab43f535..090a6b42 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/README.md @@ -7,6 +7,11 @@ three continuous Binance Futures testnet closed index-price kline feeds: - `BTCUSDT` (`sourceType=3`, feed ID `1002`); - `ETHUSDT` (`sourceType=3`, feed ID `1003`). +Each callback uses packed Price Feed V1: a canonical 32-byte big-endian body +containing `version`, `feedId`, `roundId`, `resolvedAtMs`, fixed-8 `price`, and +zero `flags`. The resolver exposes the latest round as +`(uint32 roundId, uint48 resolvedAtMs, uint96 price)`. + The suite submits one Gravity governance proposal that registers the three price tasks and the `PriceFeedResolver` callback. The same proposal changes the epoch interval from the 60-second bootstrap value to two hours for `E+1`. It @@ -115,6 +120,27 @@ The summary must contain two entries in `restartRecoveries`; the final snapshot must still show all four replicas, every relayer checkpoint caught up, and every price callback count equal to its source nonce. +## Two-Hour Packed V1 Gate + +This focused compatibility run validates the packed contracts/reth/SDK stack +and restarts `node4` at the midpoint: + +```bash +ORACLE_SOAK_DURATION_SECONDS=7200 \ +ORACLE_SOAK_POLL_SECONDS=15 \ +ORACLE_SOAK_STALL_TIMEOUT_SECONDS=900 \ +ORACLE_SOAK_MIN_ADVANCES=96 \ +ORACLE_SOAK_RESTART_AFTER_SECONDS=3600 \ + ./gravity_e2e/run_test.sh \ + oracle_live_soak \ + --force-init \ + --log-cli-level=INFO +``` + +The final summary must report `status: passed`, one restart recovery, at least +96 advances for every feed, four converged replicas, and callback counts equal +to final source nonces. + ## Optional 24-Hour Soak Run from a terminal multiplexer or another session that will stay alive: diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/genesis.toml b/gravity_e2e/cluster_test_cases/oracle_live_soak/genesis.toml index 7cf62633..27ccf7d2 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/genesis.toml +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/genesis.toml @@ -4,7 +4,7 @@ [dependencies.genesis_contracts] repo = "https://github.com/Galxe/gravity_chain_core_contracts.git" -ref = "f447687bf6f6f9efabd92626b588aeda94148f88" +ref = "d90d81583b95adac73e86c8aafd27530414df03f" # Equal voting power means a JWK QC requires matching signatures from at least # three of the four validators. diff --git a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py index fb2b5854..90b271e6 100644 --- a/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py +++ b/gravity_e2e/cluster_test_cases/oracle_live_soak/test_oracle_live_soak.py @@ -420,8 +420,8 @@ def _price_state_at_block_hash( ) if ( progress_before == progress_after - and latest[0] - and progress_after[1] == latest[2] + and latest[0] > 0 + and progress_after[1] == latest[1] ): return progress_after, latest time.sleep(0.05) @@ -676,12 +676,10 @@ async def _replicated_snapshot_once( round_start = _round_start_ms( int(feed["bucketStartMs"]), progress[0] ) - assert latest[0] - assert latest[1] == round_start // INTERVAL_MS - assert latest[2] == round_start + INTERVAL_MS - 1 - assert progress[1] == latest[2] - assert latest[3] == DECIMALS - assert latest[4] > 0 + assert latest[0] == round_start // INTERVAL_MS + assert latest[1] == round_start + INTERVAL_MS - 1 + assert progress[1] == latest[1] + assert latest[2] > 0 price_feeds[pair] = { "feedId": feed_id, "progress": progress, @@ -828,7 +826,7 @@ async def _run_soak( } last_nonces = dict(initial_nonces) last_prices = { - pair: int(state["latestPrice"][4]) + pair: int(state["latestPrice"][2]) for pair, state in initial["priceFeeds"].items() } observed_price_changes = {pair: 0 for pair in initial_nonces} @@ -909,7 +907,7 @@ async def _run_soak( pair = feed["pair"] state = final["priceFeeds"][pair] nonce = int(state["progress"][0]) - price = int(state["latestPrice"][4]) + price = int(state["latestPrice"][2]) assert nonce >= last_nonces[pair], ( f"{pair} source nonce regressed" ) @@ -936,7 +934,7 @@ async def _run_soak( "feedId": int(feed["feedId"]), "nonce": nonce, "position": int(state["progress"][1]), - "round": int(state["latestPrice"][1]), + "round": int(state["latestPrice"][0]), "price": str(price), "observedPriceChanges": observed_price_changes[pair], "relayerQuorumNodes": relayer_nodes, @@ -1012,14 +1010,14 @@ async def _run_soak( expected_price = await _fetch_binance_price_with_retry( feed["baseUrl"], pair, final_bucket ) - assert int(state["latestPrice"][4]) == expected_price + assert int(state["latestPrice"][2]) == expected_price price_summaries[pair] = { "feedId": int(feed["feedId"]), "initialNonce": initial_nonces[pair], "finalNonce": final_nonce, "advances": advances, "minimumAdvances": settings.minimum_advances, - "finalPrice": str(state["latestPrice"][4]), + "finalPrice": str(state["latestPrice"][2]), "observedPriceChanges": observed_price_changes[pair], "maxObservedGapSeconds": round(max_price_gaps[pair], 3), } @@ -1233,7 +1231,7 @@ async def test_governance_activated_price_feeds_soak_for_configured_duration( expected_initial_price = await _fetch_binance_price_with_retry( feed["baseUrl"], feed["pair"], first_bucket ) - assert int(initial_price[4]) == expected_initial_price + assert int(initial_price[2]) == expected_initial_price await _wait_for_consensus_evidence(cluster, issuers) for feed in binance_feeds: price_progress, _ = initial_prices[feed["pair"]] diff --git a/gravity_e2e/gravity_e2e/utils/oracle_test_support.py b/gravity_e2e/gravity_e2e/utils/oracle_test_support.py index ddd0ff47..4ed44bf6 100644 --- a/gravity_e2e/gravity_e2e/utils/oracle_test_support.py +++ b/gravity_e2e/gravity_e2e/utils/oracle_test_support.py @@ -293,7 +293,7 @@ async def wait_for_latest_price( ).call() ) latest = tuple(resolver.functions.latestPrice(feed_id).call()) - if progress[0] >= target_nonce and latest[0] and latest[2] == progress[1]: + if progress[0] >= target_nonce and latest[0] > 0 and latest[1] == progress[1]: return progress, latest await asyncio.sleep(2) raise TimeoutError(f"latestPrice({feed_id}) did not reach delivery nonce {target_nonce}")