Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
598 changes: 299 additions & 299 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ unexpected_cfgs = { level = "allow", check-cfg = ["cfg(mirai)"] }
aptos-consensus = { path = "./aptos-core/consensus" }

# from aptos =======================
gaptos = { git = "https://github.com/Galxe/gravity-aptos.git", rev = "6c778c7" }
gaptos = { git = "https://github.com/Galxe/gravity-aptos.git", rev = "26b925bfedec52ba03380b3ce3d989d49365b23c" }
aptos-executor-types = { path = "dependencies/aptos-executor-types" }
aptos-executor = { path = "dependencies/aptos-executor" }
api = { path = "./crates/api" }
Expand Down
2 changes: 1 addition & 1 deletion bin/gravity_node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ serde = "^1.0.226"
bincode = "1.3"
time = "0.3.36"
anyhow = "1.0.87"
greth = { git = "https://github.com/Galxe/gravity-reth", rev = "ada67c0" }
greth = { git = "https://github.com/Galxe/gravity-reth", rev = "35994424a45416a033061bfa900ee702f2647804" }
reqwest = "0.12.9"
alloy-primitives = { version = "=1.3.1", default-features = false, features = ["map-foldhash"] }
alloy-eips = { version = "^1.0.37", default-features = false }
Expand Down
29 changes: 29 additions & 0 deletions gravity_e2e/cluster_test_cases/hardfork_test/cluster.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Gravity Cluster Configuration - Hardfork Test Suite
# Single node for hardfork upgrade testing

[cluster]
name = "gravity-devnet-hardfork"
base_dir = "/tmp/gravity-cluster-hardfork"

[build]
binary_path = "../target/quick-release/gravity_node"

[genesis_source]
genesis_path = "./artifacts/genesis.json"
waypoint_path = "./artifacts/waypoint.txt"

[[nodes]]
id = "node1"
role = "genesis"
host = "127.0.0.1"
p2p_port = 6184
vfn_port = 6194
rpc_port = 8549
metrics_port = 9005
inspection_port = 10004
https_port = 1028
authrpc_port = 8555
reth_p2p_port = 12028

[faucet_init]
num_accounts = 100
67 changes: 67 additions & 0 deletions gravity_e2e/cluster_test_cases/hardfork_test/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""
Pytest configuration and fixtures for Hardfork + Bridge E2E tests.

Provides:
- mock_anvil_metadata: parsed metadata from hooks.py's pre-loaded MockAnvil
- bridge_verify_timeout: configurable timeout for NativeMinted polling
"""

import json
import logging
import os
import sys
from pathlib import Path

_current_dir = Path(__file__).resolve().parent
# Add gravity_e2e parent to path
_gravity_e2e_parent = _current_dir
while _gravity_e2e_parent.name != "gravity_e2e" or not (_gravity_e2e_parent / "gravity_e2e").is_dir():
_gravity_e2e_parent = _gravity_e2e_parent.parent
if _gravity_e2e_parent == _gravity_e2e_parent.parent:
break
if str(_gravity_e2e_parent) not in sys.path:
sys.path.insert(0, str(_gravity_e2e_parent))

import pytest

LOG = logging.getLogger(__name__)


def pytest_addoption(parser):
"""Add bridge-specific command line options."""
parser.addoption(
"--bridge-verify-timeout",
action="store",
default="120",
help="Timeout in seconds for verifying NativeMinted events (default: 120)",
)


@pytest.fixture(scope="session")
def bridge_verify_timeout(request) -> int:
"""Timeout for verifying all NativeMinted events."""
return int(request.config.getoption("--bridge-verify-timeout"))


@pytest.fixture(scope="module")
def mock_anvil_metadata() -> dict:
"""
Read MockAnvil metadata written by hooks.py.

Returns dict with: port, rpc_url, bridge_count, amount,
recipient, sender_address, portal_address, nonces, finalized_block.
"""
metadata_file = _current_dir / "mock_anvil_metadata.json"
if not metadata_file.exists():
pytest.skip(
"mock_anvil_metadata.json not found — "
"MockAnvil was not started by hooks.py"
)

metadata = json.loads(metadata_file.read_text())
LOG.info(
f"[fixture] Read MockAnvil metadata: "
f"{metadata['bridge_count']} events, "
f"finalized_block={metadata['finalized_block']}"
)
return metadata
87 changes: 87 additions & 0 deletions gravity_e2e/cluster_test_cases/hardfork_test/genesis.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Gravity Genesis Configuration - Hardfork Test Suite
# Uses v1.0.0 contracts for genesis (pre-gamma bytecode)
# gammaBlock is injected into genesis.json by hooks.py

[dependencies.genesis_contracts]
repo = "https://github.com/Galxe/gravity_chain_core_contracts.git"
ref = "gravity-testnet-v1.0.0"

# Genesis validators with stake and voting power
[[genesis_validators]]
id = "node1"
address = "0xAEd2a948892475F800A337427B3275D190EA3e94"
host = "127.0.0.1"
p2p_port = 6184
vfn_port = 6194
stake_amount = "10000000000000000000"
voting_power = "10000000000000000000"
consensus_pop = "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"

[genesis]
chain_id = 1337
epoch_interval_micros = 60000000 # 60 seconds - short epoch for hardfork testing
major_version = 1
consensus_config = "0x0301010a00000000000000280000000000000001010000000a000000000000000100010200000000000000000020000000000000"
execution_config = "0x00"
initial_locked_until_micros = 1798848000000000

[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 = "0"

[genesis.staking_config]
minimum_stake = "1000000000000000000"
lockup_duration_micros = 86400000000
unbonding_delay_micros = 86400000000
minimum_proposal_stake = "10000000000000000000"

[genesis.governance_config]
min_voting_threshold = "1000000000000000000"
required_proposer_stake = "10000000000000000000"
voting_duration_micros = 604800000000

[genesis.randomness_config]
variant = 1
secrecy_threshold = 9223372036854775808
reconstruction_threshold = 12297829382473033728
fast_path_secrecy_threshold = 12297829382473033728

# Oracle config: source_types includes Blockchain (0) for bridge
[genesis.oracle_config]
source_types = [1]
callbacks = ["0x00000000000000000000000000000001625F2018"]

# Bridge config: deploy GBridgeReceiver at genesis
[genesis.oracle_config.bridge_config]
deploy = true
# GBridgeSender deterministic address on Anvil/MockAnvil (deployer nonce=2)
trusted_bridge = "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"
trusted_source_id = 31337

# Oracle task: watch Anvil/MockAnvil (chainId 31337) for MessageSent events
[[genesis.oracle_config.tasks]]
source_type = 0
source_id = 31337
task_name = "anvil"
config = "gravity://0/31337/events?contract=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512&eventSignature=0x5646e682c7d994bf11f5a2c8addb60d03c83cda3b65025a826346589df43406e&fromBlock=0"

# JWK config - Google OIDC provider
[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"
191 changes: 191 additions & 0 deletions gravity_e2e/cluster_test_cases/hardfork_test/hardfork_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"""
Reusable utilities for hardfork E2E testing.

Provides helper functions for:
- Waiting for specific block heights
- Verifying system contract bytecode changes
- Sending light transaction pressure
- Injecting hardfork config into genesis.json

These utilities are designed to be reusable across different hardfork tests
(gamma, delta, epsilon, etc.).
"""

import asyncio
import json
import logging
import time
from pathlib import Path
from typing import Dict, List, Optional, Tuple

from eth_account import Account
from web3 import Web3

LOG = logging.getLogger(__name__)


# ── Gamma hardfork system contract addresses ─────────────────────────
# These are the fixed system contract addresses upgraded by the gamma hardfork.
# Source: gravity-reth/crates/ethereum/evm/src/hardfork/gamma.rs GAMMA_SYSTEM_UPGRADES
GAMMA_SYSTEM_CONTRACTS = {
"StakingConfig": "0x00000000000000000000000000000001625F1001",
"ValidatorConfig": "0x00000000000000000000000000000001625F1002",
"GovernanceConfig": "0x00000000000000000000000000000001625F1004",
"Staking": "0x00000000000000000000000000000001625F2000",
"ValidatorManagement": "0x00000000000000000000000000000001625F2001",
"Reconfiguration": "0x00000000000000000000000000000001625F2003",
"Blocker": "0x00000000000000000000000000000001625F2004",
"PerformanceTracker": "0x00000000000000000000000000000001625F2005",
"Governance": "0x00000000000000000000000000000001625F3000",
"NativeOracle": "0x00000000000000000000000000000001625F4000",
"OracleRequestQueue": "0x00000000000000000000000000000001625F4002",
}


async def wait_for_block(
w3: Web3, target_block: int, timeout: int = 300, poll_interval: float = 1.0
) -> bool:
"""
Wait until the node reaches a specific block height.
Returns True if target reached, False on timeout.
"""
start = time.monotonic()
while time.monotonic() - start < timeout:
try:
current = w3.eth.block_number
if current >= target_block:
LOG.info(f"✅ Reached block {current} (target: {target_block})")
return True
if int(time.monotonic() - start) % 10 == 0 and int(time.monotonic() - start) > 0:
LOG.info(f" ⏳ Current block: {current}, waiting for {target_block}...")
except Exception:
pass
await asyncio.sleep(poll_interval)
LOG.error(f"❌ Timed out waiting for block {target_block} (timeout={timeout}s)")
return False


async def wait_for_blocks_after(
w3: Web3, start_block: int, delta: int, timeout: int = 120
) -> bool:
"""
Wait for `delta` more blocks after `start_block`.
"""
target = start_block + delta
LOG.info(f"Waiting for {delta} blocks after {start_block} (target: {target})...")
return await wait_for_block(w3, target, timeout=timeout)


def get_contract_code_hashes(
w3: Web3, addresses: Dict[str, str]
) -> Dict[str, Optional[str]]:
"""
Get code hashes for a set of contract addresses.
Returns {name: hex_code_hash_or_None}.
"""
result = {}
for name, addr in addresses.items():
try:
code = w3.eth.get_code(Web3.to_checksum_address(addr))
if code and len(code) > 0:
code_hash = Web3.keccak(code).hex()
result[name] = code_hash
else:
result[name] = None
except Exception as e:
LOG.warning(f" Failed to get code for {name} ({addr}): {e}")
result[name] = None
return result


def snapshot_system_contracts(w3: Web3) -> Dict[str, Optional[str]]:
"""
Take a snapshot of code hashes for all gamma system contracts.
"""
return get_contract_code_hashes(w3, GAMMA_SYSTEM_CONTRACTS)


def compare_snapshots(
before: Dict[str, Optional[str]], after: Dict[str, Optional[str]]
) -> Tuple[List[str], List[str], List[str]]:
"""
Compare two contract code hash snapshots.
Returns (changed, unchanged, missing).
"""
changed = []
unchanged = []
missing = []
for name in before:
if before[name] is None and after.get(name) is None:
missing.append(name)
elif before[name] != after.get(name):
changed.append(name)
else:
unchanged.append(name)
return changed, unchanged, missing


async def send_eth_transfers(
w3: Web3, sender_account, num_txns: int = 10, amount_wei: int = 1000
) -> Tuple[int, int]:
"""
Send simple ETH transfers as light pressure.
Returns (successful_count, failed_count).
"""
success_count = 0
fail_count = 0
chain_id = w3.eth.chain_id

for i in range(num_txns):
try:
receiver = Account.create()
nonce = w3.eth.get_transaction_count(sender_account.address)

tx = {
"to": receiver.address,
"value": amount_wei,
"gas": 21000,
"gasPrice": w3.eth.gas_price,
"nonce": nonce,
"chainId": chain_id,
}

signed = sender_account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=30)

if receipt["status"] == 1:
success_count += 1
else:
fail_count += 1
LOG.warning(f" TX {i} reverted")
except Exception as e:
fail_count += 1
LOG.warning(f" TX {i} failed: {e}")

LOG.info(f" Transfers: {success_count} ok, {fail_count} failed (out of {num_txns})")
return success_count, fail_count


def inject_hardfork_config(
genesis_path: Path, hardfork_name: str, block_number: int
):
"""
Inject a gravity hardfork block number into genesis.json.
Generic function for any hardfork (gamma, delta, etc.).
"""
with open(genesis_path) as f:
genesis = json.load(f)

if "config" not in genesis:
genesis["config"] = {}
if "gravityHardforks" not in genesis["config"]:
genesis["config"]["gravityHardforks"] = {}

key = f"{hardfork_name}Block"
genesis["config"]["gravityHardforks"][key] = block_number

with open(genesis_path, "w") as f:
json.dump(genesis, f, indent=2)

LOG.info(f"Injected {key}={block_number} into {genesis_path}")
Loading
Loading