Skip to content

feat(hardfork): add TestnetOwnerFix for Longevity StakePool ownership - #431

Merged
nekomoto911 merged 8 commits into
Galxe:mainfrom
nekomoto911:feat/testnet-owner-fix
Aug 26, 2026
Merged

feat(hardfork): add TestnetOwnerFix for Longevity StakePool ownership#431
nekomoto911 merged 8 commits into
Galxe:mainfrom
nekomoto911:feat/testnet-owner-fix

Conversation

@nekomoto911

@nekomoto911 nekomoto911 commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Longevity Testnet (chainId = 7771625) genesis StakePools use Aptos-era identity material as owner — EOA-shaped addresses with no recoverable secp256k1 key — so onlyOwner admin paths are stuck.

This adds GravityHardfork::TestnetOwnerFix (testnetOwnerFixTime): on the activation block, the pipe injects four forced top-level transferOwnership(new_owner) txs with from = old_owner (after the SYSTEM_CALLER system-tx prefix, before user txs), writes them into the block body with TransactionSenders = old_owner, and starts Ownable2Step (pendingOwner = new).

  • Scope: Longevity Testnet only (chain_id gate + fail-closed genesis key). Mainnet / other chains are no-ops.
  • Gas: Reuses existing Alpha system-tx levers (gas_price = 0 + transact_system_txn basefee/balance disable). No new gas predicate.
  • One-shot: gated with transitions_at_timestamp(current, parent) (same pattern as Alpha / EIP-2935), not is_fork_active.

Merge gate for this PR: after testnetOwnerFixTime, all four Longevity StakePools have pendingOwner() == <ceremony new EOA>. That is proven by the gravity-sdk suite testnet_owner_fix_rolling against this greth head — not by a greth-in-tree activation e2e.

Implementation notes

  • Migration table + calldata helpers live in crates/ethereum/evm/src/hardfork/testnet_owner_fix.rs (no slot precheck / no StakePool.bin).
  • Protocol system txs (metadata / DKG / JWK) return unfinished results via SystemTxnExecutionOutcome; one TestnetOwnerFix seam in execute_ordered_block appends forced transfers for both Continue and EpochChanged, then assembles the block.
  • SystemTxnResult carries an explicit sender so FixOwner does not hardcode SYSTEM_CALLER on body insert.

Non-goals / accepted tradeoffs

The items below are known, accepted gaps for a Longevity-only one-shot hardfork. They are not required for the migration state root or for the merge gate above.

1. No greth activation-path integration test

  • Why not: A greth test that runs the real activation block needs production StakePool runtime/genesis fixtures we deliberately removed in the slim reimpl. Not worth the fixture surface for a chain_id-gated, single-fire fork.
  • Instead: greth keeps thin gate unit tests (wrong chain / unscheduled / post-activation no-op). End-to-end proof is sdk testnet_owner_fix_rolling (four pendingOwners, binary rolling upgrade, post-restart canonical replay). Latest local PASS on greth a09d1381: activationBlock 3552.
  • The earlier claim that reth-evm-ethereum covers real StakePool pendingOwner == new is obsolete and removed from the test plan below.

2. eth_getTransactionByHash (and similar eth_*) may show wrong from after cache miss

  • Fact: Block body / TransactionSenders correctly store from = old_owner. After RPC cache eviction or restart, sender recovery can ignore persisted senders, fail on the zero signature, and fall back to SYSTEM_CALLER.
  • Why not fix here: Needs shared eth RPC / sender-recovery changes (or FixOwner-only exceptions) for four one-shot testnet txs. Consensus and on-chain pendingOwner do not depend on eth_* from.
  • Accepted: eth_* by-hash from can disagree with body senders after prune; operators should read contract state (or body senders), not rely on recovered from for these four txs.

3. debug_* / trace_* replay is not faithful for FixOwner txs

  • Fact: Forced txs are stored with gas_price = 0 and sender = old_owner. Replay only special-cases SYSTEM_CALLER for basefee/balance, so block traces can fail on those txs.
  • Why not fix here: A permanent debug/trace bypass for a testnet-only activation block is not justified.
  • Accepted: Do not require prune-after debug/trace byte-equality on the activation block for merge.

RPC fidelity for these four txs, if ever required, is a separate follow-up — not a merge blocker for the pipe migration.

Test plan

  • cargo test -p reth-chainspec — parse / fail-closed testnetOwnerFixTime
  • cargo test -p reth-evm-ethereum testnet_owner_fix — ABI / calldata only (no StakePool runtime e2e)
  • cargo test -p reth-pipe-exec-layer-ext-v2 --lib testnet_owner_fix — wrong-chain / unscheduled / post-activation no-op gates
  • gravity-sdk testnet_owner_fix_rolling @ greth a09d1381 — four pendingOwners PASS (activationBlock 3552; post-restart catch-up)
  • Testnet upgrade.json / runbook scheduling (ops follow-up)

Inject forced transferOwnership txs (from=old_owner) on Longevity
Testnet only after the SYSTEM_CALLER prefix, so genesis pools can start
Ownable2Step without recoverable keys. No RPC debug/trace special-casing.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a77e3f7320

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +39 to +43
if !chain_spec
.gravity_hardforks()
.is_fork_active_at_timestamp(GravityHardfork::TestnetOwnerFix, block_timestamp)
{
return Vec::new();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop rerunning the owner fix after activation

Because this checks whether the fork is active rather than whether this is its activation transition, the precheck runs on every later block. After a replacement owner legitimately completes another Ownable2Step handoff, that pool has neither owner == new_owner nor pendingOwner == new_owner; the next block therefore reports a partial migration or unexpected owner and panics, permanently halting the chain. Execute the migration only on the activation boundary or record a durable completion marker independent of current ownership.

Useful? React with 👍 / 👎.

Backtick CamelCase identifiers in hardfork docs so -D warnings clippy
passes. Ignore known EngineNodeLauncher SelectNextSome teardown panic in
the eip7702 pipe panic hook — P-12 assertions already passed before it.
Remove PoolSnapshot/PrecheckDecision/precheck_migration, slot reads,
ParallelExecutor::storage()/Executor::storage(), and StakePool.bin.
Keep Longevity gate + forced transferOwnership inject; revert panics.
Consolidate pipe call sites behind inject_testnet_owner_fix.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0bc6974969

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +89 to +90
let tx_env = Recovered::new_unchecked(txn.clone(), row.old_owner).into_tx_env();
let execution_result = executor.transact_system_txn(evm_env, Vec::new(), tx_env)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve gas exemptions when replaying forced transfers

When Alpha is active and the block base fee is nonzero, these transactions use gas_price = 0 but are recorded with old_owner rather than SYSTEM_CALLER; canonical execution succeeds only because transact_system_txn disables base-fee and balance checks for the active fork. The RPC replay paths in rpc-eth-api/src/helpers/trace.rs and rpc/src/debug.rs enable those checks only when is_gravity_system_caller(tx.signer()), so tracing a forced transfer—or any later transaction whose replay crosses it—fails with GasPriceLessThanBasefee instead of reproducing the block. Extend replay classification to recognize these fork transactions or otherwise make their persisted representation replayable.

Useful? React with 👍 / 👎.

Align with Alpha/EIP-2935 one-shot: inject only on the Longevity block
that crosses testnetOwnerFixTime. Prevents post-activation blocks from
re-injecting four transferOwnership txs after precheck removal.
That branch only exists for historical pre-Alpha epoch-change assembly.
OwnerFix is Longevity-only and always post-Alpha; keep inject on the
Alpha-active DKG path, metadata NewEpoch, and Continue.
…ange

Only the one-shot activation block yields those four results; reserving
them on every Alpha-active DKG epoch-change path was leftover noise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e1593f7e61

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

);
let tx_env = Recovered::new_unchecked(txn.clone(), row.old_owner).into_tx_env();
let execution_result = executor.transact_system_txn(evm_env, Vec::new(), tx_env)?;
Ok(SystemTxnResult { result: execution_result, txn, sender: row.old_owner })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve forced-transfer senders outside the RPC cache

When this transaction is loaded after the RPC cache is evicted or the node restarts, transaction_by_hash reconstructs its sender from the transaction itself; new_system_call_txn uses the same zero signature as metadata transactions, so recovery fails and the existing fallback reports SYSTEM_CALLER, not row.old_owner. Consequently eth_getTransactionByHash and other transaction-source consumers expose the wrong from address for every forced transfer loaded from disk. Persist or reconstruct the migration sender in that lookup path rather than relying only on the SystemTxnResult sidecar.

Useful? React with 👍 / 👎.

@nekomoto911

Copy link
Copy Markdown
Collaborator Author

Review notes:

  1. The active migration path is currently untested. The PR test plan promises a real StakePool runtime assertion (pendingOwner == new), but crates/ethereum/evm/src/hardfork/testnet_owner_fix.rs only checks the ABI selector/calldata length, while the pipe tests only cover no-op branches. Please add an integration test that executes the activation block and verifies all four state transitions, transaction ordering, and recorded senders. This is important because a call to a missing/wrong runtime can still return success without changing pendingOwner, so ExecutionResult::is_success() alone does not prove the migration happened.

  2. The change in crates/pipe-exec-layer-ext-v2/execute/tests/gravity_eip7702_test.rs is unrelated to TestnetOwnerFix and changes the test process's global panic semantics. Matching a panic by text can hide a real background-thread failure with the same message. Please revert it here or move the teardown-flake fix to a focused PR with its own validation.

Non-blocking documentation cleanup:

  • onchain_config/metadata_txn.rs still says positions 1+ are only validator transactions, but OwnerFix transactions now use those positions too.
  • The “old is_fork_active gate” comment in testnet_owner_fix.rs describes change history rather than the current one-shot invariant.

Drop the unrelated SelectNextSome ignore from gravity_eip7702_test —
OwnerFix S1 stays on sdk smoke. Refresh metadata_txn / one-shot test
comments to match current inject layout.
@nekomoto911

Copy link
Copy Markdown
Collaborator Author

Addressed / decided on the review notes above:

  1. Active-path greth integration test — won't add in this PR.
    Delivery gate for S1 (pendingOwner == new on all four pools; activation-block ordering observed in smoke) is the existing gravity-sdk Longevity rolling suite (testnet_owner_fix_rolling). Greth keeps thin no-op gate unit tests + the one-shot transitions_at_timestamp invariant; we are not expanding greth pipe e2e for this Longevity-only fix.

  2. gravity_eip7702_test panic-hook change — reverted (b9128a71a4). Unrelated teardown flake handling will not ride along with TestnetOwnerFix.

Doc cleanup:

  • metadata_txn.rs insert docs now mention OwnerFix positions (not only validators).
  • One-shot test comment no longer narrates the old is_fork_active gate.

@nekomoto911

nekomoto911 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

Suggestion: Minimize TestnetOwnerFix Pipe Invasiveness

Goal: consolidate TestnetOwnerFix into a single call site in pipe execution, without introducing a generic trait/registry, while preserving existing block body, receipt, sender, gas, and one-shot consensus semantics.

Design Constraints

  • Enable only via chain_id == 7771625 and the testnetOwnerFixTime timestamp transition; do not add a feature flag, environment variable, or node config.
  • Preserve the body/receipt/sender/state-root semantics of the four forced txs, and fail closed on failure.
  • Do not introduce a migration trait/registry with a single implementor; mainnet/private chains should only pay a cheap chain-id no-op check.

Suggested Refactor

  1. Change SystemTxnExecutionOutcome::EpochChanged to carry unfinished system_results and validators. The metadata/DKG branches should return execution results only — they must not inject OwnerFix, take_bundle, or assemble the final block. Pre-Alpha DKG should still keep its historical result set.
  2. Compute system_tx_gas_price once in execute_ordered_block. After protocol system txs run, call testnet_owner_fix::execute_forced_transfers(...) exactly once:
    • EpochChanged: append OwnerFix results, then take_bundle and return via system_txns_into_executed_ordered_block_result.
    • Continue: continue executing user txs, then insert OwnerFix body/receipts after the protocol system prefix.
  3. Delete the inject_testnet_owner_fix thin wrapper, the two EpochChanged-specific inject sites, the parent_timestamp pierce into execute_system_transactions, and the duplicated gas-price calculation.
  4. Keep the protocol-required pieces: SystemTxnResult.sender, system gas budget, cumulative receipt adjustment, and gravity-event scan boundaries.
  5. Move the gravity_eip7702_test.rs panic-hook change out of this PR to avoid mixing unrelated behavior.

Testing

  • Keep wrong-chain, unscheduled, and post-activation no-op coverage, and verify the gate returns before reading executor state.
  • Add an activation test against production genesis with the real StakePool runtime/storage, asserting the four pendingOwner values, sender, nonce, and successful receipts.
  • Add Continue and EpochChanged assembly regressions covering tx order, gas/cumulative receipts, epoch blocks with no user txs, and the final state bundle.

This leaves a single TestnetOwnerFix execution seam in lib.rs; other chains never enter the EVM for this path and do not take on pluggable-framework complexity.

EpochChanged now returns unfinished protocol system results; OwnerFix,
take_bundle, and epoch-block assembly happen once in execute_ordered_block.
Drops the inject wrapper, dual EpochChanged call sites, parent_timestamp
piercing, and duplicate system_tx_gas_price calculation.
@ByteYue

ByteYue commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Reviewed latest head a09d1381.

The consensus migration path itself looks sound: the one-shot gate now uses transitions_at_timestamp, the unrelated panic-hook change is gone, and the forced transfers are assembled at a single seam. I also checked the public Longevity RPC: all four pool addresses have runtime code, each current owner() matches MIGRATION_TABLE, each pendingOwner() is zero, and debug_traceCall shows every proposed call writing slot 1 to the expected new owner while incrementing the old-owner nonce from 0 to 1.

I still do not think this is ready to approve:

  1. Activation-path coverage is missing. The current tests cover two chainspec parsing cases, one ABI-selector case, and three no-op gates. They do not execute the activation block or assert the four state transitions, transaction ordering, recorded senders, receipts, gas accounting, or Continue/EpochChanged assembly. The PR test plan still says reth-evm-ethereum covers a real StakePool runtime and pendingOwner == new, but that test was removed by the slimming refactor. For a one-shot fail-closed hardfork, please either add the greth activation-path test or link an exact gravity-sdk commit and reproducible CI artifact tied to this reth head.

  2. Disk/cache-miss sender recovery is still wrong. SystemTxnResult.sender records old_owner, but LoadTransaction::transaction_by_hash ignores persisted TransactionSenders, tries to recover the zero signature, and falls back to SYSTEM_CALLER. Therefore eth_getTransactionByHash can report a different from after cache eviction or restart. The existing inline comment at discussion_r3859269791 remains valid.

  3. RPC trace/debug replay is still not faithful. The forced transactions persist with gas_price = 0 and sender = old_owner, while replay only disables base-fee/balance checks for SYSTEM_CALLER. Block-family traces can fail at the forced transfer and traces of later transactions cross the same failure. Combined with item 2, single-transaction replay may instead execute under the wrong caller. The existing inline comment at discussion_r3859207380 remains valid.

Local rerun on this head passed all currently matching tests (2 + 1 + 3), and CI is green, but those tests do not exercise the migration path above. My verdict is request changes until there is reproducible activation evidence; if RPC correctness is part of the delivery scope, items 2 and 3 should also be fixed before merge.

@nekomoto911

Copy link
Copy Markdown
Collaborator Author

Thanks for the re-review on a09d1381.

I've updated the PR description with an explicit Non-goals / accepted tradeoffs section. Short answers to your three points:

  1. Activation-path coverage — We are not adding a greth-in-tree activation e2e (that would reintroduce StakePool runtime/genesis fixtures we deliberately cut). Merge gate is on-chain pendingOwner() == ceremony new for all four pools, proven by gravity-sdk testnet_owner_fix_rolling against this head (a09d1381, local PASS, activationBlock 3552). Greth keeps the thin gate unit tests only. The obsolete test-plan line that claimed reth-evm-ethereum covered real StakePool pendingOwner == new has been removed from the body.

  2. eth_getTransactionByHash / cache-miss from — Agreed this can disagree with persisted TransactionSenders after eviction/restart. Accepted for this PR: fixing it means shared eth RPC sender-recovery changes for four one-shot Longevity txs; consensus and pendingOwner do not depend on eth_* from. Called out separately in the PR body (this is eth_*, not only debug/trace).

  3. debug/trace replay — Same acceptance: gas_price = 0 + sender = old_owner is intentional for canonical execution via transact_system_txn; we are not adding FixOwner-specific debug/trace bypasses in this PR.

Happy to discuss a follow-up RPC PR if product later requires eth_* from stability or prune-after trace fidelity for these four txs — not treating either as a merge blocker for the pipe migration.

@nekomoto911
nekomoto911 merged commit 0930e3a into Galxe:main Aug 26, 2026
33 checks passed
nekomoto911 added a commit to nekomoto911/gravity-sdk that referenced this pull request Aug 27, 2026
Pin greth / reth-primitives-traits to the merged TestnetOwnerFix commit
from Galxe/gravity-reth#431 (0930e3a09d).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants