diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 5833a9999..53be53eef 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -93,6 +93,37 @@ jobs: with: cache-on-failure: true - uses: taiki-e/install-action@nextest + - name: Restore Gravity genesis fixture cache + id: gravity-genesis-fixture-cache + uses: actions/cache@v4 + with: + path: crates/pipe-exec-layer-ext-v2/execute/fixtures/.cache/gravity_hardfork.json + key: gravity-genesis-fixture-${{ runner.os }}-${{ hashFiles('crates/pipe-exec-layer-ext-v2/execute/fixtures/**') }} + - uses: actions/setup-node@v4 + if: steps.gravity-genesis-fixture-cache.outputs.cache-hit != 'true' + with: + node-version: 20 + - uses: foundry-rs/foundry-toolchain@v1 + if: steps.gravity-genesis-fixture-cache.outputs.cache-hit != 'true' + with: + version: stable + - name: Regenerate Gravity genesis fixture + run: | + set -euo pipefail + + fixture_dir="crates/pipe-exec-layer-ext-v2/execute/fixtures" + cache_file="$fixture_dir/.cache/gravity_hardfork.json" + target="$fixture_dir/../gravity_hardfork.json" + + if [[ "${{ steps.gravity-genesis-fixture-cache.outputs.cache-hit }}" == "true" ]]; then + cp "$cache_file" "$target" + else + "$fixture_dir/regen.sh" + mkdir -p "$(dirname "$cache_file")" + cp "$target" "$cache_file" + fi + + git diff --exit-code -- "$target" - name: Check Gravity invariants (ripgrep) # Long-term regression-prevention lint for the Alpha system-tx # gas-exempt design. Living as a bash + ripgrep script (not a Rust diff --git a/crates/pipe-exec-layer-ext-v2/execute/fixtures/.gitignore b/crates/pipe-exec-layer-ext-v2/execute/fixtures/.gitignore new file mode 100644 index 000000000..ceddaa37f --- /dev/null +++ b/crates/pipe-exec-layer-ext-v2/execute/fixtures/.gitignore @@ -0,0 +1 @@ +.cache/ diff --git a/crates/pipe-exec-layer-ext-v2/execute/fixtures/README.md b/crates/pipe-exec-layer-ext-v2/execute/fixtures/README.md new file mode 100644 index 000000000..201960b83 --- /dev/null +++ b/crates/pipe-exec-layer-ext-v2/execute/fixtures/README.md @@ -0,0 +1,39 @@ +# Gravity Genesis Fixture Regeneration + +This directory is the source of truth for the checked-in +`../gravity_hardfork.json` fixture used by the Gravity pipe, Prague, system +transaction, BLS, and hardfork integration tests. + +`test_genesis.toml` pins the contracts repository ref and the reth-side +hardfork knobs. `validator_genesis.json` is the compact `GenesisConfig` input +for the contracts repo `genesis-tool`; it is intentionally committed so schema +changes in `genesis-tool` fail loudly in CI instead of silently changing the +fixture shape. + +Regenerate locally with an existing contracts checkout: + +```bash +crates/pipe-exec-layer-ext-v2/execute/fixtures/regen.sh \ + --contracts-dir /path/to/gravity_chain_core_contracts +``` + +The script creates an isolated git worktree at the ref pinned in +`test_genesis.toml`, so it does not modify the supplied contracts checkout. +Without `--contracts-dir`, it clones the pinned contracts ref into a temporary +directory. Full regeneration requires `forge`, `cargo`, `npm`, `git`, and +`python3`. + +To verify the committed fixture without rewriting it: + +```bash +crates/pipe-exec-layer-ext-v2/execute/fixtures/regen.sh --check +``` + +CI caches the generated `gravity_hardfork.json` artifact by hashing this +directory. A cache hit restores the artifact and compares it to the committed +fixture; a cache miss regenerates it with Foundry, Node dependencies, and the +standalone contracts `genesis-tool`. + +`../gravity.json` remains a separate legacy fixture for tests that do not need +the hardfork schedule. Keep it committed until those tests are migrated to a +second regeneration profile. diff --git a/crates/pipe-exec-layer-ext-v2/execute/fixtures/regen.sh b/crates/pipe-exec-layer-ext-v2/execute/fixtures/regen.sh new file mode 100755 index 000000000..9d699d0e3 --- /dev/null +++ b/crates/pipe-exec-layer-ext-v2/execute/fixtures/regen.sh @@ -0,0 +1,249 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: regen.sh [--check] [--contracts-dir PATH] [--work-dir PATH] [--print-metadata] + +Regenerates ../gravity_hardfork.json from test_genesis.toml and +validator_genesis.json using the pinned gravity_chain_core_contracts ref. + +Options: + --check Generate into a temp file and compare with the committed fixture. + --contracts-dir PATH Use an existing contracts checkout as the git object source. + The script creates an isolated worktree and does not edit PATH. + --work-dir PATH Use PATH for temporary clones, worktrees, and generated files. + --print-metadata Print parsed TOML metadata and exit. +USAGE +} + +MODE="write" +CONTRACTS_SOURCE_DIR="" +WORK_DIR="" +PRINT_METADATA=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --check) + MODE="check" + shift + ;; + --contracts-dir) + CONTRACTS_SOURCE_DIR="$2" + shift 2 + ;; + --work-dir) + WORK_DIR="$2" + shift 2 + ;; + --print-metadata) + PRINT_METADATA=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +FIXTURE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXECUTE_DIR="$(cd "$FIXTURE_DIR/.." && pwd)" +TEST_GENESIS_TOML="$FIXTURE_DIR/test_genesis.toml" +VALIDATOR_GENESIS_JSON="$FIXTURE_DIR/validator_genesis.json" + +if [[ -z "$WORK_DIR" ]]; then + WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/gravity-genesis-fixture.XXXXXX")" + CLEAN_WORK_DIR=1 +else + mkdir -p "$WORK_DIR" + WORK_DIR="$(cd "$WORK_DIR" && pwd)" + CLEAN_WORK_DIR=0 +fi + +METADATA_ENV="$WORK_DIR/metadata.env" +python3 - "$TEST_GENESIS_TOML" "$FIXTURE_DIR" > "$METADATA_ENV" <<'PY' +import json +import os +import shlex +import sys +import tomllib + +toml_path, fixture_dir = sys.argv[1:3] +with open(toml_path, "rb") as f: + data = tomllib.load(f) + +deps = data["dependencies"]["genesis_contracts"] +genesis = data["genesis"] +output = data.get("output", {}) +hardforks = genesis.get("hardforks", {}) + +output_path = output.get("path", "../gravity_hardfork.json") +if not os.path.isabs(output_path): + output_path = os.path.normpath(os.path.join(fixture_dir, output_path)) + +values = { + "CONTRACTS_REPO": deps["repo"], + "CONTRACTS_REF": deps["ref"], + "OUTPUT_PATH": output_path, + "CHAIN_ID": str(genesis["chain_id"]), + "GENESIS_TIMESTAMP": str(genesis["timestamp"]), + "GRAVITY_MIN_BASE_FEE": "" if "gravity_min_base_fee" not in genesis else str(genesis["gravity_min_base_fee"]), + "HARDFORKS_JSON": json.dumps(hardforks, separators=(",", ":")), +} + +for key, value in values.items(): + print(f"{key}={shlex.quote(value)}") +PY + +# shellcheck disable=SC1090 +. "$METADATA_ENV" + +if [[ "$PRINT_METADATA" == "1" ]]; then + sed -n '1,120p' "$METADATA_ENV" + exit 0 +fi + +ADDED_WORKTREE=0 +CONTRACTS_DIR="$WORK_DIR/gravity_chain_core_contracts" +GENERATED_BASE="$WORK_DIR/genesis.base.json" +GENERATED_FINAL="$WORK_DIR/gravity_hardfork.generated.json" +TARGET_PATH="$OUTPUT_PATH" +if [[ "$MODE" == "check" ]]; then + TARGET_WRITE_PATH="$GENERATED_FINAL" +else + TARGET_WRITE_PATH="$TARGET_PATH" +fi + +cleanup() { + if [[ "$ADDED_WORKTREE" == "1" && -n "$CONTRACTS_SOURCE_DIR" ]]; then + git -C "$CONTRACTS_SOURCE_DIR" worktree remove --force "$CONTRACTS_DIR" >/dev/null 2>&1 || true + fi + if [[ "$CLEAN_WORK_DIR" == "1" ]]; then + rm -rf "$WORK_DIR" + fi +} +trap cleanup EXIT + +require_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "required command not found: $1" >&2 + exit 1 + fi +} + +require_cmd cargo +require_cmd forge +require_cmd git +require_cmd npm +require_cmd python3 + +prepare_contracts_checkout() { + rm -rf "$CONTRACTS_DIR" + + if [[ -n "$CONTRACTS_SOURCE_DIR" ]]; then + CONTRACTS_SOURCE_DIR="$(cd "$CONTRACTS_SOURCE_DIR" && pwd)" + git -C "$CONTRACTS_SOURCE_DIR" rev-parse --git-dir >/dev/null + git -C "$CONTRACTS_SOURCE_DIR" worktree add --detach "$CONTRACTS_DIR" "$CONTRACTS_REF" + if [[ -d "$CONTRACTS_SOURCE_DIR/node_modules" && ! -e "$CONTRACTS_DIR/node_modules" ]]; then + ln -s "$CONTRACTS_SOURCE_DIR/node_modules" "$CONTRACTS_DIR/node_modules" + fi + ADDED_WORKTREE=1 + return + fi + + git init "$CONTRACTS_DIR" + git -C "$CONTRACTS_DIR" remote add origin "$CONTRACTS_REPO" + git -C "$CONTRACTS_DIR" fetch --depth 1 origin "$CONTRACTS_REF" + git -C "$CONTRACTS_DIR" checkout --detach FETCH_HEAD +} + +generate_base_genesis() { + cd "$CONTRACTS_DIR" + + if [[ ! -d node_modules/forge-std || ! -d node_modules/@openzeppelin/contracts ]]; then + if [[ -f package-lock.json ]]; then + npm ci --ignore-scripts + else + npm install --ignore-scripts + fi + fi + + rm -rf out output account_alloc.json genesis.json + + forge build + python3 scripts/helpers/extract_bytecode.py --out-dir out --output-dir out + + mkdir -p output + cargo run --release --manifest-path genesis-tool/Cargo.toml -- \ + --log-file output/genesis_generation.log \ + generate \ + --byte-code-dir out \ + --config-file "$VALIDATOR_GENESIS_JSON" \ + --output output + + python3 scripts/helpers/combine_account_alloc.py output/genesis_contracts.json output/genesis_accounts.json + python3 scripts/helpers/fix_hex_length.py account_alloc.json + python3 scripts/helpers/genesis_generate.py \ + --template genesis-tool/config/genesis_template.json \ + --account-alloc account_alloc.json \ + --config-file "$VALIDATOR_GENESIS_JSON" \ + --output "$GENERATED_BASE" +} + +apply_reth_overrides() { + mkdir -p "$(dirname "$TARGET_WRITE_PATH")" + python3 - \ + "$TEST_GENESIS_TOML" \ + "$GENERATED_BASE" \ + "$TARGET_WRITE_PATH" <<'PY' +import json +import sys +import tomllib + +toml_path, input_path, output_path = sys.argv[1:4] +with open(toml_path, "rb") as f: + test_genesis = tomllib.load(f) +with open(input_path, "r") as f: + genesis = json.load(f) + +config = genesis.setdefault("config", {}) +settings = test_genesis["genesis"] + +config["chainId"] = settings["chain_id"] +genesis["timestamp"] = hex(settings["timestamp"]) + +if "gravity_min_base_fee" in settings: + config["gravityMinBaseFee"] = settings["gravity_min_base_fee"] +else: + config.pop("gravityMinBaseFee", None) + +for key, value in settings.get("hardforks", {}).items(): + config[key] = value + +with open(output_path, "w") as f: + json.dump(genesis, f, indent=2) + f.write("\n") +PY +} + +prepare_contracts_checkout +generate_base_genesis +apply_reth_overrides + +if [[ "$MODE" == "check" ]]; then + if cmp -s "$GENERATED_FINAL" "$TARGET_PATH"; then + echo "gravity_hardfork.json matches regenerated fixture" + else + diff -u "$TARGET_PATH" "$GENERATED_FINAL" || true + echo "gravity_hardfork.json is stale; run $0 to regenerate" >&2 + exit 1 + fi +else + echo "wrote $TARGET_PATH" +fi diff --git a/crates/pipe-exec-layer-ext-v2/execute/fixtures/test_genesis.toml b/crates/pipe-exec-layer-ext-v2/execute/fixtures/test_genesis.toml new file mode 100644 index 000000000..f2827cfa2 --- /dev/null +++ b/crates/pipe-exec-layer-ext-v2/execute/fixtures/test_genesis.toml @@ -0,0 +1,16 @@ +[dependencies.genesis_contracts] +repo = "https://github.com/Galxe/gravity_chain_core_contracts.git" +ref = "gravity-testnet-v1.0.0" + +[output] +path = "../gravity_hardfork.json" + +[genesis] +chain_id = 7771625 +timestamp = 1687223762 + +[genesis.hardforks] +alphaTime = 5 +betaBlock = 10 +gammaBlock = 20 +deltaBlock = 25 diff --git a/crates/pipe-exec-layer-ext-v2/execute/fixtures/validator_genesis.json b/crates/pipe-exec-layer-ext-v2/execute/fixtures/validator_genesis.json new file mode 100644 index 000000000..4248a7874 --- /dev/null +++ b/crates/pipe-exec-layer-ext-v2/execute/fixtures/validator_genesis.json @@ -0,0 +1,92 @@ +{ + "_comment": "Single node genesis config for gravity_chain_core_contracts Genesis.initialize(GenesisInitParams)", + "validatorConfig": { + "_comment": "ValidatorConfig.initialize params", + "minimumBond": "1000000000000000000", + "maximumBond": "1000000000000000000000000", + "unbondingDelayMicros": 604800000000, + "allowValidatorSetChange": true, + "votingPowerIncreaseLimitPct": 20, + "maxValidatorSetSize": "100", + "autoEvictEnabled": false, + "autoEvictThreshold": "0" + }, + "stakingConfig": { + "_comment": "StakingConfig.initialize params - for governance staking", + "minimumStake": "1000000000000000000", + "lockupDurationMicros": 86400000000, + "unbondingDelayMicros": 86400000000, + "minimumProposalStake": "10000000000000000000" + }, + "governanceConfig": { + "_comment": "GovernanceConfig.initialize params", + "minVotingThreshold": "1000000000000000000", + "requiredProposerStake": "10000000000000000000", + "votingDurationMicros": 604800000000 + }, + "epochIntervalMicros": 7200000000, + "majorVersion": 1, + "consensusConfig": "0x0301010a00000000000000280000000000000001010000000a000000000000000100010200000000000000000020000000000000", + "executionConfig": "0x00", + "randomnessConfig": { + "_comment": "RandomnessConfig - variant: 0=Off, 1=V2", + "variant": 0, + "configV2": { + "secrecyThreshold": 0, + "reconstructionThreshold": 0, + "fastPathSecrecyThreshold": 0 + } + }, + "oracleConfig": { + "_comment": "NativeOracle.initialize - sourceType 1 = JWK, 0 = Blockchain", + "sourceTypes": [ + 1 + ], + "callbacks": [ + "0x00000000000000000000000000000001625F4001" + ], + "bridgeConfig": { + "deploy": true, + "trustedBridge": "0x3fc870008B1cc26f3614F14a726F8077227CA2c3" + }, + "tasks": [ + { + "sourceType": 0, + "sourceId": 11155111, + "taskName": "events", + "config": "gravity://0/11155111/events?contract=0x0f761B1B3c1aC9232C9015A7276692560aD6a05F&eventSignature=0x5646e682c7d994bf11f5a2c8addb60d03c83cda3b65025a826346589df43406e&fromBlock=10201260" + } + ] + }, + "jwkConfig": { + "_comment": "JWKManager.initialize - issuers and jwks", + "issuers": [ + "0x68747470733a2f2f6163636f756e74732e676f6f676c652e636f6d" + ], + "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" + } + ] + ] + }, + "validators": [ + { + "operator": "0x6e2021ee24e2430da0f5bb9c2ae6c586bf3e0a0f", + "owner": "0x6e2021ee24e2430da0f5bb9c2ae6c586bf3e0a0f", + "stakeAmount": "20000000000000000000000", + "moniker": "validator-1", + "consensusPubkey": "0x851d41932d866f5fabed6673898e15473e6a0adcf5033d2c93816c6b115c85ad3451e0bac61d570d5ed9f23e1e7f77c4", + "consensusPop": "0x", + "networkAddresses": "/ip4/127.0.0.1/tcp/2024/noise-ik/2d86b40a1d692c0749a0a0426e2021ee24e2430da0f5bb9c2ae6c586bf3e0a0f/handshake/0", + "fullnodeAddresses": "/ip4/127.0.0.1/tcp/2024/noise-ik/2d86b40a1d692c0749a0a0426e2021ee24e2430da0f5bb9c2ae6c586bf3e0a0f/handshake/0", + "votingPower": "20000000000000000000000" + } + ], + "chainId": 7771625 +} diff --git a/crates/pipe-exec-layer-ext-v2/execute/tests/HARDFORK_TESTING.md b/crates/pipe-exec-layer-ext-v2/execute/tests/HARDFORK_TESTING.md index a0ea4fa0d..2b96785f2 100644 --- a/crates/pipe-exec-layer-ext-v2/execute/tests/HARDFORK_TESTING.md +++ b/crates/pipe-exec-layer-ext-v2/execute/tests/HARDFORK_TESTING.md @@ -11,7 +11,7 @@ Genesis (v1.0.0 old contracts) → run N blocks → gammaBlock triggers apply_ga ``` 1. **Build an old-version Genesis**: Check out a historical tag (e.g. `gravity-testnet-v1.0.0`) from the contracts repo, then run `scripts/generate_genesis_single.sh` to produce a `genesis.json` containing legacy contract bytecodes. -2. **Inject hardfork config**: Add `gravityHardforks.gammaBlock` to the genesis JSON's `config` object. +2. **Inject hardfork config**: Add Gravity hardfork activation fields to the genesis JSON's `config` object (`alphaTime`, `betaBlock`, `gammaBlock`, `deltaBlock`, ...). 3. **Boot a reth node**: Start a single-node reth instance using the modified genesis. 4. **Push blocks via MockConsensus**: Use `PipeExecLayerApi` to push empty blocks across the `gammaBlock` boundary. 5. **Verify bytecode replacement**: @@ -24,7 +24,9 @@ Genesis (v1.0.0 old contracts) → run N blocks → gammaBlock triggers apply_ga | File | Description | |------|-------------| | `gravity_hardfork_test.rs` | Integration test code | -| `gravity_hardfork.json` | Legacy genesis with `gammaBlock` injected | +| `gravity_hardfork.json` | Legacy genesis with Gravity hardfork activation fields injected | +| `fixtures/test_genesis.toml` | Reproducible fixture source of truth: contracts ref plus reth-side hardfork knobs | +| `fixtures/regen.sh` | Regenerates `gravity_hardfork.json` with the standalone contracts `genesis-tool` | | `HARDFORK_TESTING.md` | This document | Core code paths: @@ -32,7 +34,7 @@ Core code paths: | File | Description | |------|-------------| | `crates/chainspec/src/gravity.rs` | `GravityHardfork` enum (Alpha/Beta/Gamma) | -| `crates/chainspec/src/spec.rs` | Parses `gravityHardforks.gammaBlock` from genesis JSON | +| `crates/chainspec/src/spec.rs` | Parses Gravity hardfork fields such as `alphaTime`, `betaBlock`, `gammaBlock`, and `deltaBlock` from genesis JSON | | `crates/chainspec/src/api.rs` | `gamma_transitions_at_block()` trait method | | `crates/ethereum/evm/src/hardfork/gamma.rs` | Gamma bytecode constants and addresses | | `crates/ethereum/evm/src/parallel_execute.rs` | `apply_gamma()` bytecode replacement logic | @@ -50,11 +52,12 @@ cd /tmp/gcc-v1.0.0 && npm install # 3. Generate genesis bash scripts/generate_genesis_single.sh -# 4. Inject gammaBlock +# 4. Inject hardfork activation fields python3 -c " import json with open('genesis.json') as f: g = json.load(f) -g['config']['gravityHardforks'] = {'gammaBlock': 20} +g['config']['gammaBlock'] = 20 +g['config']['deltaBlock'] = 25 with open('gravity_hardfork.json', 'w') as f: json.dump(g, f, indent=2) " @@ -78,8 +81,8 @@ RUSTFLAGS="--cfg tokio_unstable" cargo test --test gravity_hardfork_test -- --no ### 1. Genesis build requires node_modules `generate_genesis_single.sh` depends on `node_modules` (forge-std, openzeppelin). After checking out an old tag, you **must** run `npm install` first — otherwise Forge compilation will fail. -### 2. `gravityHardforks` must be a top-level config key -reth's `alloy_genesis` handles unknown config fields via `#[serde(flatten)]` into `extra_fields`. This means `gravityHardforks` must be a top-level key inside the `config` object — it cannot be nested under any other field. +### 2. Gravity hardfork fields must be top-level config keys +reth's `alloy_genesis` handles unknown config fields via `#[serde(flatten)]` into `extra_fields`. This means fields such as `alphaTime`, `betaBlock`, `gammaBlock`, and `deltaBlock` must be top-level keys inside the `config` object — they cannot be nested under another field. ### 3. Contracts that didn't exist in the old genesis Contracts added after v1.0.0 (e.g. `OracleRequestQueue` at 0x1625F4002) won't be present in the legacy genesis. `apply_gamma()` already handles this by checking `if let Some(ref info)` and skipping missing accounts. The test must tolerate this as well. @@ -104,7 +107,7 @@ Using a hypothetical "Delta" hardfork as an example: ``` crates/chainspec/src/gravity.rs → Add Delta variant to GravityHardfork enum -crates/chainspec/src/spec.rs → Parse gravityHardforks.deltaBlock +crates/chainspec/src/spec.rs -> Parse config.deltaBlock crates/chainspec/src/api.rs → Add delta_transitions_at_block() crates/ethereum/evm/src/hardfork/delta.rs → New file with updated bytecode constants crates/ethereum/evm/src/parallel_execute.rs → Add apply_delta() call