Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.
This repository was archived by the owner on Jul 10, 2026. It is now read-only.

MEV Shield: ≤8-block mortal era anchored at the finalized head makes submit_encrypted intermittently rejected as "Transaction is outdated" under finality lag #3395

Description

@argonmining

Summary

commit_timelocked_weights_extrinsic(..., mev_protection=True) is intermittently rejected at pool admission with Transaction is outdated (InvalidTransaction::Stale) on testnet. The root cause appears to be that the MEV Shield outer submit_encrypted extrinsic combines a chain-enforced short mortal era (≤ 8 blocks) with an era anchor taken from the finalized head, so any moment where best_head - finalized_head >= period produces a transaction that is dead on arrival — pure timing luck, independent of caller behaviour.

Environment

  • bittensor 10.5.0, async-substrate-interface 2.0.4, bittensor-wallet 4.1.0
  • Python 3.12, Linux
  • Network: test (wss://test.finney.opentensor.ai:443)
  • Caller: a subnet validator publishing CRv4 timelocked weights once per tempo

Mechanism (SDK source trace, pinned versions above)

  1. bittensor/core/extrinsics/asyncex/mev_shield.py::submit_encrypted_extrinsic resolves the era period via resolve_mev_shield_period(period), which caps at MAX_MEV_SHIELD_PERIOD = 8 (bittensor/core/settings.py). Both the inner signed extrinsic and the outer submit_encrypted call use this era (era = {"period": effective_period}, and the outer path passes period=effective_period into sign_and_send_extrinsic).
  2. async_substrate_interface/async_substrate.py::create_signed_extrinsic anchors a mortal era that has no explicit current/phase at the finalized head:
    era["current"] = await self.get_block_number(
        await self.get_chain_finalised_head()
    )
  3. The resulting validity window is roughly [finalized_head, finalized_head + 8). Transaction-pool mortality is checked against the node's current best head, so whenever finality lags the best head by ≥ ~8 blocks (slow GRANDPA rounds — we see this in spikes on testnet), the extrinsic is already expired when it reaches the pool and is rejected InvalidTransaction::Stale, surfaced by the SDK as Transaction is outdated.

A MEV-shield NextKey rotation landing on the block boundary compounds the window, but the era anchoring alone reproduces it.

Observed behaviour

  • Intermittent ExtrinsicResponse(success=False, message="Transaction is outdated", ...) from commit_timelocked_weights_extrinsic under raise_error=False; the same call succeeds moments later with no input change.
  • Correlated with testnet finality-lag spikes; never observed while best - finalized < 8.
  • Re-submitting after > 1 block (~13 s) with a fresh era anchor / nonce / NextKey succeeds. (The rejection consumes no nonce, and sign_and_send_extrinsic correctly clears the account nonce cache on the SubstrateRequestException path, so a bounded caller-side retry is safe — that is what we shipped downstream as a mitigation on July 6.)

Why this is worth fixing in the SDK

An era of ≤ 8 blocks is chain policy for MEV Shield and makes sense; anchoring it at the finalized head does not — mortality is evaluated against the best chain, so the effective validity window shrinks by however far finality is lagging, reaching zero exactly in the degraded conditions where validators most need weight commits to land. Every consumer of mev_protection=True inherits this without any way to opt out.

Suggested direction

Any of these would resolve it:

  1. Anchor the mortal era for MEV Shield submissions at the current best head (chain_getHeader) instead of the finalized head, or
  2. expose the era anchor as a parameter on create_signed_extrinsic / submit_encrypted_extrinsic so callers can choose, or
  3. have sign_and_send_extrinsic retry once with a fresh era anchor on InvalidTransaction::Stale for mortal-era extrinsics (the nonce cache is already invalidated on that path, so this is safe).

Happy to provide raw logs of the rejections (timestamps + block numbers) if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions