You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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.
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).
async_substrate_interface/async_substrate.py::create_signed_extrinsic anchors a mortal era that has no explicit current/phase at the finalized head:
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:
Anchor the mortal era for MEV Shield submissions at the current best head (chain_getHeader) instead of the finalized head, or
expose the era anchor as a parameter on create_signed_extrinsic / submit_encrypted_extrinsic so callers can choose, or
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.
Summary
commit_timelocked_weights_extrinsic(..., mev_protection=True)is intermittently rejected at pool admission withTransaction is outdated(InvalidTransaction::Stale) on testnet. The root cause appears to be that the MEV Shield outersubmit_encryptedextrinsic combines a chain-enforced short mortal era (≤ 8 blocks) with an era anchor taken from the finalized head, so any moment wherebest_head - finalized_head >= periodproduces a transaction that is dead on arrival — pure timing luck, independent of caller behaviour.Environment
bittensor10.5.0,async-substrate-interface2.0.4,bittensor-wallet4.1.0test(wss://test.finney.opentensor.ai:443)Mechanism (SDK source trace, pinned versions above)
bittensor/core/extrinsics/asyncex/mev_shield.py::submit_encrypted_extrinsicresolves the era period viaresolve_mev_shield_period(period), which caps atMAX_MEV_SHIELD_PERIOD = 8(bittensor/core/settings.py). Both the inner signed extrinsic and the outersubmit_encryptedcall use this era (era = {"period": effective_period}, and the outer path passesperiod=effective_periodintosign_and_send_extrinsic).async_substrate_interface/async_substrate.py::create_signed_extrinsicanchors a mortal era that has no explicitcurrent/phaseat the finalized head:[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 rejectedInvalidTransaction::Stale, surfaced by the SDK asTransaction is outdated.A MEV-shield
NextKeyrotation landing on the block boundary compounds the window, but the era anchoring alone reproduces it.Observed behaviour
ExtrinsicResponse(success=False, message="Transaction is outdated", ...)fromcommit_timelocked_weights_extrinsicunderraise_error=False; the same call succeeds moments later with no input change.best - finalized < 8.sign_and_send_extrinsiccorrectly clears the account nonce cache on theSubstrateRequestExceptionpath, 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=Trueinherits this without any way to opt out.Suggested direction
Any of these would resolve it:
chain_getHeader) instead of the finalized head, orcreate_signed_extrinsic/submit_encrypted_extrinsicso callers can choose, orsign_and_send_extrinsicretry once with a fresh era anchor onInvalidTransaction::Stalefor 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.