Skip to content

MEV V2 infrastructure#2533

Draft
shamil-gadelshin wants to merge 4 commits intodevnet-readyfrom
mev-shield-v2-on-initialize
Draft

MEV V2 infrastructure#2533
shamil-gadelshin wants to merge 4 commits intodevnet-readyfrom
mev-shield-v2-on-initialize

Conversation

@shamil-gadelshin
Copy link
Copy Markdown
Collaborator

This PR introduces infrastructure for the upcoming MEV Shield V2 feature. The idea is to accumulate encrypted extrinsics in a queue and decrypt them in the next block during the on_initialize phase. Encryption and decryption are
outside the scope of this PR and abstracted via the ExtrinsicDecryptor trait. Protective limits include encrypted call size (const), queue size, weight for extrinsic execution, and lifetime in the queue.

PR features

  • Add on_initialize hook to process pending encrypted extrinsics with configurable weight limits, extrinsic lifetime, and queue size
  • Add store_encrypted extrinsic for queuing encrypted calls for deferred execution
  • Add three root-gated configuration extrinsics: set_max_pending_extrinsics_number, set_on_initialize_weight, and set_stored_extrinsic_lifetime
  • Introduce ExtrinsicDecryptor trait for decrypting stored extrinsics before dispatch, with expiration, weight budgeting, and event logging

Details

New Storage Items

  • PendingExtrinsics — map of queued encrypted extrinsics awaiting execution
  • NextPendingExtrinsicIndex / PendingExtrinsicCount — auto-increment index and live count for the queue
  • MaxPendingExtrinsicsLimit — configurable max queue depth (default: 100)
  • OnInitializeWeight — configurable max weight budget for on_initialize processing (default: 500B ref_time, capped at 2T)
  • ExtrinsicLifetime — configurable max age in blocks before expiration (default: 10)

New Extrinsics

  • store_encrypted — signed extrinsic to queue an encrypted call
  • set_max_pending_extrinsics_number — root-only, sets queue limit
  • set_on_initialize_weight — root-only, sets weight budget with absolute max guard
  • set_stored_extrinsic_lifetime — root-only, sets expiration window

Processing Logic (on_initialize)

Iterates pending extrinsics in insertion order. For each entry:

  1. Expired entries are removed and ExtrinsicExpired is emitted
  2. Decryption failures are removed and ExtrinsicDecodeFailed is emitted
  3. If dispatching would exceed the weight budget, processing stops (ExtrinsicPostponed)
  4. Otherwise, the call is dispatched from the submitter's signed origin

Other Changes

  • Added RuntimeCall and ExtrinsicDecryptor associated types to pallet_shield::Config
  • Disambiguated T::RuntimeCall in check_mortality.rs to resolve ambiguity between frame_system::Config and pallet_shield::Config
  • Comprehensive tests for all new extrinsics, storage limits, expiration, weight budgeting, and edge cases

Unit Test Plan

  • Unit tests for store_encrypted(success, queue full, event emission)
  • Unit tests for set_max_pending_extrinsics_number (root-only, value persistence)
  • Unit tests for set_on_initialize_weight (root-only, absolute max rejection)
  • Unit tests for set_stored_extrinsic_lifetime (root-only, value persistence)
  • Unit tests for on_initialize processing (dispatch, expiration, weight limits, decode failures)

Benchmarks

To be added after the initial review.

@shamil-gadelshin shamil-gadelshin self-assigned this Mar 24, 2026
@shamil-gadelshin shamil-gadelshin added the skip-cargo-audit This PR fails cargo audit but needs to be merged anyway label Mar 24, 2026
Comment on lines +168 to +172
/// Storage map for encrypted extrinsics to be executed in on_initialize.
/// Uses u32 index for O(1) insertion and removal.
#[pallet::storage]
pub type PendingExtrinsics<T: Config> =
StorageMap<_, Identity, u32, PendingExtrinsic<T>, OptionQuery>;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is maybe one of the rare case where we could use a CountedStorageMap

}

#[test]
fn on_initialize_handles_dispatch_failure() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

test name is on_initialize_handles_dispatch_failure but we check success on multiple calls?

Copy link
Copy Markdown
Collaborator

@l0r1s l0r1s left a comment

Choose a reason for hiding this comment

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

Overall the design is good. My only concern is that the inner call is never charged for, the user gets free execution for whatever is inside. This could probably be fixed by implementing an additional check when trying to execute the call similar to what the ChargeTransactionPayment extension does and reject it if fee can't be paid, could even be done through a DispatchExtension.

Copy link
Copy Markdown
Contributor

@JohnReedV JohnReedV left a comment

Choose a reason for hiding this comment

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

Looks good to me. Didn't see anything in addition to Loris’s comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-cargo-audit This PR fails cargo audit but needs to be merged anyway

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants