From 8e1418888d606f49577fbd8a5023e54ae7a433fe Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:23:38 -0800 Subject: [PATCH 01/30] initial impl --- Cargo.lock | 65 ++++ Cargo.toml | 3 + node/Cargo.toml | 15 + node/src/lib.rs | 1 + node/src/main.rs | 1 + node/src/mev_shield/author.rs | 423 +++++++++++++++++++++++++ node/src/mev_shield/mod.rs | 2 + node/src/mev_shield/proposer.rs | 527 ++++++++++++++++++++++++++++++++ node/src/service.rs | 66 ++++ pallets/mev-shield/Cargo.toml | 71 +++++ pallets/mev-shield/src/lib.rs | 473 ++++++++++++++++++++++++++++ runtime/Cargo.toml | 3 + runtime/src/lib.rs | 23 ++ scripts/localnet.sh | 2 + 14 files changed, 1675 insertions(+) create mode 100644 node/src/mev_shield/author.rs create mode 100644 node/src/mev_shield/mod.rs create mode 100644 node/src/mev_shield/proposer.rs create mode 100644 pallets/mev-shield/Cargo.toml create mode 100644 pallets/mev-shield/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d242b5922f..a5ff63cb86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6020,6 +6020,15 @@ dependencies = [ "serde", ] +[[package]] +name = "hybrid-array" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "0.14.32" @@ -6766,6 +6775,16 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "kem" +version = "0.3.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8645470337db67b01a7f966decf7d0bafedbae74147d33e641c67a91df239f" +dependencies = [ + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "keystream" version = "1.0.0" @@ -7787,6 +7806,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ml-kem" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97befee0c869cb56f3118f49d0f9bb68c9e3f380dec23c1100aedc4ec3ba239a" +dependencies = [ + "hybrid-array", + "kem", + "rand_core 0.6.4", + "sha3", +] + [[package]] name = "mmr-gadget" version = "46.0.0" @@ -8159,7 +8190,10 @@ checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" name = "node-subtensor" version = "4.0.0-dev" dependencies = [ + "anyhow", "async-trait", + "blake2 0.10.6", + "chacha20poly1305", "clap", "fc-api", "fc-aura", @@ -8179,19 +8213,26 @@ dependencies = [ "frame-system-rpc-runtime-api", "futures", "hex", + "hkdf", "jsonrpsee", "log", "memmap2 0.9.8", + "ml-kem", "node-subtensor-runtime", "num-traits", "pallet-commitments", "pallet-drand", + "pallet-mev-shield", + "pallet-subtensor", "pallet-subtensor-swap-rpc", "pallet-subtensor-swap-runtime-api", "pallet-transaction-payment", "pallet-transaction-payment-rpc", "pallet-transaction-payment-rpc-runtime-api", + "parity-scale-codec", "polkadot-sdk", + "rand 0.8.5", + "rand_core 0.9.3", "sc-basic-authorship", "sc-chain-spec", "sc-chain-spec-derive", @@ -8219,6 +8260,7 @@ dependencies = [ "sc-transaction-pool-api", "serde", "serde_json", + "sha2 0.10.9", "sp-api", "sp-block-builder", "sp-blockchain", @@ -8244,6 +8286,8 @@ dependencies = [ "subtensor-custom-rpc", "subtensor-custom-rpc-runtime-api", "subtensor-runtime-common", + "tokio", + "x25519-dalek", ] [[package]] @@ -8292,6 +8336,7 @@ dependencies = [ "pallet-grandpa", "pallet-hotfix-sufficients", "pallet-insecure-randomness-collective-flip", + "pallet-mev-shield", "pallet-multisig", "pallet-nomination-pools", "pallet-nomination-pools-runtime-api", @@ -9956,6 +10001,26 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-mev-shield" +version = "0.0.1" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-aura", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-consensus-aura", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "sp-weights", + "subtensor-macros", +] + [[package]] name = "pallet-migrations" version = "11.0.0" diff --git a/Cargo.toml b/Cargo.toml index 6139004914..e003665ccb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -285,6 +285,9 @@ sha2 = { version = "0.10.8", default-features = false } rand_chacha = { version = "0.3.1", default-features = false } tle = { git = "https://github.com/ideal-lab5/timelock", rev = "5416406cfd32799e31e1795393d4916894de4468", default-features = false } +pallet-mev-shield = { path = "pallets/mev-shield", default-features = false } +ml-kem = { version = "0.2.0", default-features = true } + # Primitives [profile.release] diff --git a/node/Cargo.toml b/node/Cargo.toml index 12a4b71189..4602662ab4 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -114,6 +114,21 @@ fc-aura.workspace = true fp-consensus.workspace = true num-traits = { workspace = true, features = ["std"] } +# Mev Shield +pallet-mev-shield.workspace = true +tokio = { version = "1.38", features = ["time"] } +x25519-dalek = "2" +hkdf = "0.12" +chacha20poly1305 = { version = "0.10", features = ["std"] } +codec.workspace = true +rand.workspace = true +sha2.workspace = true +anyhow.workspace = true +pallet-subtensor.workspace = true +ml-kem.workspace = true +rand_core = "0.9.3" +blake2 = "0.10.6" + # Local Dependencies node-subtensor-runtime = { workspace = true, features = ["std"] } subtensor-runtime-common = { workspace = true, features = ["std"] } diff --git a/node/src/lib.rs b/node/src/lib.rs index c447a07309..c5e7a90c43 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -6,3 +6,4 @@ pub mod consensus; pub mod ethereum; pub mod rpc; pub mod service; +pub mod mev_shield; diff --git a/node/src/main.rs b/node/src/main.rs index 64f25acc67..3aac9b0d9f 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -12,6 +12,7 @@ mod consensus; mod ethereum; mod rpc; mod service; +mod mev_shield; fn main() -> sc_cli::Result<()> { command::run() diff --git a/node/src/mev_shield/author.rs b/node/src/mev_shield/author.rs new file mode 100644 index 0000000000..26cb713144 --- /dev/null +++ b/node/src/mev_shield/author.rs @@ -0,0 +1,423 @@ +//! MEV-shield author-side helpers: ML‑KEM-768 ephemeral keys + timed key announcement. + +use std::{sync::{Arc, Mutex}, time::Duration}; + +use futures::StreamExt; +use sc_client_api::HeaderBackend; +use sc_transaction_pool_api::TransactionSource; +use sp_api::ProvideRuntimeApi; +use sp_core::{sr25519, blake2_256}; +use sp_runtime::traits::Block as BlockT; +use sp_runtime::KeyTypeId; +use tokio::time::sleep; +use blake2::Blake2b512; + +use hkdf::Hkdf; +use sha2::Sha256; +use chacha20poly1305::{XChaCha20Poly1305, KeyInit, aead::{Aead, Payload}, XNonce}; + +use node_subtensor_runtime as runtime; // alias for easier type access +use runtime::{RuntimeCall, UncheckedExtrinsic}; + +use ml_kem::{MlKem768, KemCore, EncodedSizeUser}; +use rand::rngs::OsRng; + +/// Parameters controlling time windows inside the slot (milliseconds). +#[derive(Clone)] +pub struct TimeParams { + pub slot_ms: u64, // e.g., 12_000 + pub announce_at_ms: u64, // 7_000 + pub decrypt_window_ms: u64 // 3_000 +} + +/// Holds the current/next ML‑KEM keypairs and their 32‑byte fingerprints. +#[derive(Clone)] +pub struct MevShieldKeys { + pub current_sk: Vec, // ML‑KEM secret key bytes (encoded form) + pub current_pk: Vec, // ML‑KEM public key bytes (encoded form) + pub current_fp: [u8; 32], // blake2_256(pk) + pub next_sk: Vec, + pub next_pk: Vec, + pub next_fp: [u8; 32], + pub epoch: u64, +} + +impl MevShieldKeys { + pub fn new(epoch: u64) -> Self { + let (sk, pk) = MlKem768::generate(&mut OsRng); + + // Bring EncodedSizeUser into scope so as_bytes() is available + let sk_bytes = sk.as_bytes(); + let pk_bytes = pk.as_bytes(); + let sk_slice: &[u8] = sk_bytes.as_ref(); + let pk_slice: &[u8] = pk_bytes.as_ref(); + + let current_sk = sk_slice.to_vec(); + let current_pk = pk_slice.to_vec(); + let current_fp = blake2_256(pk_slice); + + let (nsk, npk) = MlKem768::generate(&mut OsRng); + let nsk_bytes = nsk.as_bytes(); + let npk_bytes = npk.as_bytes(); + let nsk_slice: &[u8] = nsk_bytes.as_ref(); + let npk_slice: &[u8] = npk_bytes.as_ref(); + let next_sk = nsk_slice.to_vec(); + let next_pk = npk_slice.to_vec(); + let next_fp = blake2_256(npk_slice); + + Self { current_sk, current_pk, current_fp, next_sk, next_pk, next_fp, epoch } + } + + pub fn roll_for_next_slot(&mut self) { + // Move next -> current + self.current_sk = core::mem::take(&mut self.next_sk); + self.current_pk = core::mem::take(&mut self.next_pk); + self.current_fp = self.next_fp; + + // Generate fresh next + let (nsk, npk) = MlKem768::generate(&mut OsRng); + let nsk_bytes = nsk.as_bytes(); + let npk_bytes = npk.as_bytes(); + let nsk_slice: &[u8] = nsk_bytes.as_ref(); + let npk_slice: &[u8] = npk_bytes.as_ref(); + self.next_sk = nsk_slice.to_vec(); + self.next_pk = npk_slice.to_vec(); + self.next_fp = blake2_256(npk_slice); + + self.epoch = self.epoch.saturating_add(1); + } +} + +/// Shared context state. +#[derive(Clone)] +pub struct MevShieldContext { + pub keys: Arc>, + pub timing: TimeParams, +} + +/// Derive AEAD key directly from the 32‑byte ML‑KEM shared secret. +/// This matches the FFI exactly: AEAD key = shared secret bytes. +pub fn derive_aead_key(ss: &[u8]) -> [u8; 32] { + let mut key = [0u8; 32]; + let n = ss.len().min(32); + key[..n].copy_from_slice(&ss[..n]); + key +} + +/// Plain XChaCha20-Poly1305 decrypt helper +pub fn aead_decrypt( + key: [u8; 32], + nonce24: [u8; 24], + ciphertext: &[u8], + aad: &[u8], +) -> Option> { + let aead = XChaCha20Poly1305::new((&key).into()); + aead.decrypt(XNonce::from_slice(&nonce24), Payload { msg: ciphertext, aad }).ok() +} + +const AURA_KEY_TYPE: KeyTypeId = KeyTypeId(*b"aura"); + +/// Start background tasks: +/// - per-slot ML‑KEM key rotation +/// - at ~announce_at_ms announce the *next* key **bytes** on chain, +/// signed by the local block author (Aura authority), not an env var. +pub fn spawn_author_tasks( + task_spawner: &sc_service::SpawnTaskHandle, + client: std::sync::Arc, + pool: std::sync::Arc, + keystore: sp_keystore::KeystorePtr, + initial_epoch: u64, + timing: TimeParams, +) -> MevShieldContext +where + B: sp_runtime::traits::Block, + // Need block import notifications and headers. + C: sc_client_api::HeaderBackend + + sc_client_api::BlockchainEvents + + Send + + Sync + + 'static, + Pool: sc_transaction_pool_api::TransactionPool + Send + Sync + 'static, + // We submit an OpaqueExtrinsic into the pool. + B::Extrinsic: From, +{ + let ctx = MevShieldContext { + keys: std::sync::Arc::new(std::sync::Mutex::new(MevShieldKeys::new(initial_epoch))), + timing: timing.clone(), + }; + + // Pick the local Aura authority key that actually authors blocks on this node. + // We just grab the first Aura sr25519 key in the keystore. + let aura_keys: Vec = keystore.sr25519_public_keys(AURA_KEY_TYPE); + let local_aura_pub: Option = aura_keys.get(0).cloned(); + + if local_aura_pub.is_none() { + log::warn!( + target: "mev-shield", + "spawn_author_tasks: no local Aura sr25519 key in keystore; \ + this node will NOT announce MEV‑Shield keys" + ); + return ctx; + } + + let local_aura_pub = local_aura_pub.expect("checked is_some; qed"); + + // Clone handles for the async task. + let ctx_clone = ctx.clone(); + let client_clone = client.clone(); + let pool_clone = pool.clone(); + let keystore_clone = keystore.clone(); + + // Slot tick / key-announce loop (roll at end of slot). + task_spawner.spawn( + "mev-shield-keys-and-announce", + None, + async move { + use futures::StreamExt; + use sp_consensus::BlockOrigin; + + let mut import_stream = client_clone.import_notification_stream(); + let mut local_nonce: u32 = 0; + + while let Some(notif) = import_stream.next().await { + // ✅ Only act on blocks that *this node* authored. + if notif.origin != BlockOrigin::Own { + continue; + } + + // This block is the start of a slot for which we are the author. + let (epoch_now, curr_pk_len, next_pk_len) = { + let k = ctx_clone.keys.lock().unwrap(); + (k.epoch, k.current_pk.len(), k.next_pk.len()) + }; + + log::info!( + target: "mev-shield", + "Slot start (local author): epoch={} (pk sizes: curr={}B, next={}B)", + epoch_now, curr_pk_len, next_pk_len + ); + + // Wait until the announce window in this slot. + tokio::time::sleep(std::time::Duration::from_millis(timing.announce_at_ms)).await; + + // Read the *next* key we intend to use for the following epoch. + let (next_pk, next_epoch) = { + let k = ctx_clone.keys.lock().unwrap(); + (k.next_pk.clone(), k.epoch.saturating_add(1)) + }; + + // Submit announce_next_key once, signed with the local Aura authority + // (the same identity that authors this block). + match submit_announce_extrinsic::( + client_clone.clone(), + pool_clone.clone(), + keystore_clone.clone(), + local_aura_pub.clone(), + next_pk.clone(), + next_epoch, + timing.announce_at_ms, + local_nonce, + ) + .await + { + Ok(()) => { + local_nonce = local_nonce.saturating_add(1); + } + Err(e) => { + let msg = format!("{e:?}"); + // If the nonce is stale, bump once and retry. + if msg.contains("InvalidTransaction::Stale") || msg.contains("Stale") { + if submit_announce_extrinsic::( + client_clone.clone(), + pool_clone.clone(), + keystore_clone.clone(), + local_aura_pub.clone(), + next_pk, + next_epoch, + timing.announce_at_ms, + local_nonce.saturating_add(1), + ) + .await + .is_ok() + { + local_nonce = local_nonce.saturating_add(2); + } else { + log::warn!( + target: "mev-shield", + "announce_next_key retry failed after stale nonce: {e:?}" + ); + } + } else { + log::warn!( + target: "mev-shield", + "announce_next_key submit error: {e:?}" + ); + } + } + } + + // Sleep the remainder of the slot (includes decrypt window). + let tail = timing.slot_ms.saturating_sub(timing.announce_at_ms); + tokio::time::sleep(std::time::Duration::from_millis(tail)).await; + + // Roll keys for the next slot / epoch. + { + let mut k = ctx_clone.keys.lock().unwrap(); + k.roll_for_next_slot(); + log::info!( + target: "mev-shield", + "Rolled ML‑KEM key at slot boundary (local author): new epoch={}", + k.epoch + ); + } + } + } + ); + + ctx +} + + +/// Build & submit the signed `announce_next_key` extrinsic OFF-CHAIN, +/// using the local Aura authority key stored in the keystore. +pub async fn submit_announce_extrinsic( + client: std::sync::Arc, + pool: std::sync::Arc, + keystore: sp_keystore::KeystorePtr, + aura_pub: sp_core::sr25519::Public, // local Aura authority public key + next_public_key: Vec, // full ML‑KEM pubkey bytes (expected 1184B) + epoch: u64, + at_ms: u64, + nonce: u32, // nonce for CheckNonce extension +) -> anyhow::Result<()> +where + B: sp_runtime::traits::Block, + // Only need best/genesis from the client + C: sc_client_api::HeaderBackend + Send + Sync + 'static, + Pool: sc_transaction_pool_api::TransactionPool + Send + Sync + 'static, + // Convert to the pool's extrinsic type + B::Extrinsic: From, + // Allow generic conversion of block hash to bytes for H256 + B::Hash: AsRef<[u8]>, +{ + use node_subtensor_runtime as runtime; + use runtime::{RuntimeCall, UncheckedExtrinsic, SignedPayload}; + + use sc_transaction_pool_api::TransactionSource; + use sp_core::H256; + use sp_runtime::{ + AccountId32, MultiSignature, generic::Era, BoundedVec, + traits::{ConstU32, TransactionExtension} + }; + use sp_runtime::codec::Encode; + + // Helper: map a generic Block hash to H256 without requiring Into + fn to_h256>(h: H) -> H256 { + let bytes = h.as_ref(); + let mut out = [0u8; 32]; + let n = bytes.len().min(32); + out[32 - n..].copy_from_slice(&bytes[bytes.len() - n..]); + H256(out) + } + + // 0) Bounded public key (max 2 KiB) as required by the pallet. + type MaxPk = ConstU32<2048>; + let public_key: BoundedVec = + BoundedVec::try_from(next_public_key) + .map_err(|_| anyhow::anyhow!("public key too long (>2048 bytes)"))?; + + // 1) The runtime call carrying **full public key bytes**. + let call = RuntimeCall::MevShield( + pallet_mev_shield::Call::announce_next_key { + public_key, + epoch, + at_ms, + } + ); + + // 2) Extensions tuple (must match your runtime's `type TransactionExtensions`). + type Extra = runtime::TransactionExtensions; + let extra: Extra = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckEra::::from(Era::Immortal), + // Use the passed-in nonce here: + node_subtensor_runtime::check_nonce::CheckNonce::::from(nonce).into(), + frame_system::CheckWeight::::new(), + node_subtensor_runtime::transaction_payment_wrapper::ChargeTransactionPaymentWrapper::::new( + pallet_transaction_payment::ChargeTransactionPayment::::from(0u64) + ), + pallet_subtensor::transaction_extension::SubtensorTransactionExtension::::new(), + pallet_drand::drand_priority::DrandPriority::::new(), + frame_metadata_hash_extension::CheckMetadataHash::::new(false), + ); + + // 3) Implicit (AdditionalSigned) values. + type Implicit = >::Implicit; + + let info = client.info(); + let genesis_h256: H256 = to_h256(info.genesis_hash); + + let implicit: Implicit = ( + (), // CheckNonZeroSender + runtime::VERSION.spec_version, // CheckSpecVersion + runtime::VERSION.transaction_version, // CheckTxVersion + genesis_h256, // CheckGenesis + genesis_h256, // CheckEra (Immortal) + (), // CheckNonce (additional part) + (), // CheckWeight + (), // ChargeTransactionPaymentWrapper (additional part) + (), // SubtensorTransactionExtension (additional part) + (), // DrandPriority + None, // CheckMetadataHash (disabled) + ); + + // 4) Build the exact signable payload. + let payload: SignedPayload = SignedPayload::from_raw( + call.clone(), + extra.clone(), + implicit.clone(), + ); + + let raw_payload = payload.encode(); + + // Sign with the local Aura key from the keystore (synchronous `Keystore` API). + let sig_opt = keystore + .sr25519_sign(AURA_KEY_TYPE, &aura_pub, &raw_payload) + .map_err(|e| anyhow::anyhow!("keystore sr25519_sign error: {e:?}"))?; + let sig = sig_opt + .ok_or_else(|| anyhow::anyhow!("keystore sr25519_sign returned None for Aura key"))?; + + let signature: MultiSignature = sig.into(); + + // 5) Assemble and submit (also log the extrinsic hash for observability). + let who: AccountId32 = aura_pub.into(); + let address = sp_runtime::MultiAddress::Id(who); + + let uxt: UncheckedExtrinsic = UncheckedExtrinsic::new_signed( + call, + address, + signature, + extra, + ); + + let xt_bytes = uxt.encode(); + let xt_hash = sp_core::hashing::blake2_256(&xt_bytes); + + let opaque: sp_runtime::OpaqueExtrinsic = uxt.into(); + let xt: ::Extrinsic = opaque.into(); + + pool.submit_one(info.best_hash, TransactionSource::Local, xt).await?; + + log::info!( + target: "mev-shield", + "announce_next_key submitted: xt=0x{}, epoch={}, nonce={}", + hex::encode(xt_hash), + epoch, + nonce + ); + + Ok(()) +} diff --git a/node/src/mev_shield/mod.rs b/node/src/mev_shield/mod.rs new file mode 100644 index 0000000000..12db84dcd2 --- /dev/null +++ b/node/src/mev_shield/mod.rs @@ -0,0 +1,2 @@ +pub mod author; +pub mod proposer; \ No newline at end of file diff --git a/node/src/mev_shield/proposer.rs b/node/src/mev_shield/proposer.rs new file mode 100644 index 0000000000..4bc181906a --- /dev/null +++ b/node/src/mev_shield/proposer.rs @@ -0,0 +1,527 @@ +//! Last-3s reveal injector: decrypt buffered wrappers and submit unsigned `execute_revealed`. + +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + time::Duration, +}; + +use codec::{Decode, Encode}; +use futures::StreamExt; +use sc_service::SpawnTaskHandle; +use sc_transaction_pool_api::{TransactionPool, TransactionSource}; +use sp_core::H256; +use sp_runtime::{ + generic::Era, + traits::Block as BlockT, + MultiAddress, + MultiSignature, + OpaqueExtrinsic, +}; +use tokio::time::sleep; + +use node_subtensor_runtime as runtime; +use runtime::RuntimeCall; + +use super::author::{aead_decrypt, derive_aead_key, MevShieldContext}; + +use ml_kem::{Ciphertext, Encoded, EncodedSizeUser, MlKem768, MlKem768Params}; +use ml_kem::kem::{Decapsulate, DecapsulationKey}; + +/// Buffer of wrappers per-slot. +#[derive(Default, Clone)] +struct WrapperBuffer { + by_id: HashMap< + H256, + ( + Vec, // ciphertext blob + u64, // key_epoch + sp_runtime::AccountId32, // wrapper author (fee payer) + ), + >, +} + +impl WrapperBuffer { + fn upsert( + &mut self, + id: H256, + key_epoch: u64, + author: sp_runtime::AccountId32, + ciphertext: Vec, + ) { + self.by_id.insert(id, (ciphertext, key_epoch, author)); + } + + /// Drain only wrappers whose `key_epoch` matches the given `epoch`. + /// - Wrappers with `key_epoch > epoch` are kept for future decrypt windows. + /// - Wrappers with `key_epoch < epoch` are considered stale and dropped. + fn drain_for_epoch( + &mut self, + epoch: u64, + ) -> Vec<(H256, u64, sp_runtime::AccountId32, Vec)> { + let mut ready = Vec::new(); + let mut kept_future = 0usize; + let mut dropped_past = 0usize; + + self.by_id.retain(|id, (ct, key_epoch, who)| { + if *key_epoch == epoch { + // Ready to process now; remove from buffer. + ready.push((*id, *key_epoch, who.clone(), ct.clone())); + false + } else if *key_epoch > epoch { + // Not yet reveal time; keep for future epochs. + kept_future += 1; + true + } else { + // key_epoch < epoch => stale / missed reveal window; drop. + dropped_past += 1; + log::info!( + target: "mev-shield", + "revealer: dropping stale wrapper id=0x{} key_epoch={} < curr_epoch={}", + hex::encode(id.as_bytes()), + *key_epoch, + epoch + ); + false + } + }); + + log::info!( + target: "mev-shield", + "revealer: drain_for_epoch(epoch={}): ready={}, kept_future={}, dropped_past={}", + epoch, + ready.len(), + kept_future, + dropped_past + ); + + ready + } +} + +/// Start a background worker that: +/// Start a background worker that: +/// • watches imported blocks and captures `MevShield::submit_encrypted` +/// • buffers those wrappers, +/// • ~last `decrypt_window_ms` of the slot: decrypt & submit unsigned `execute_revealed` +/// **only for wrappers whose `key_epoch` equals the current ML‑KEM epoch.** +pub fn spawn_revealer( + task_spawner: &SpawnTaskHandle, + client: Arc, + pool: Arc, + ctx: MevShieldContext, +) where + B: sp_runtime::traits::Block, + C: sc_client_api::HeaderBackend + + sc_client_api::BlockchainEvents + + sc_client_api::BlockBackend + + Send + + Sync + + 'static, + Pool: TransactionPool + Send + Sync + 'static, +{ + use codec::{Decode, Encode}; + use sp_runtime::traits::SaturatedConversion; + + type Address = sp_runtime::MultiAddress; + type RUnchecked = node_subtensor_runtime::UncheckedExtrinsic; + + let buffer: Arc> = Arc::new(Mutex::new(WrapperBuffer::default())); + + // ── 1) buffer wrappers ─────────────────────────────────────── + { + let client = Arc::clone(&client); + let buffer = Arc::clone(&buffer); + task_spawner.spawn( + "mev-shield-buffer-wrappers", + None, + async move { + log::info!(target: "mev-shield", "buffer-wrappers task started"); + let mut import_stream = client.import_notification_stream(); + + while let Some(notif) = import_stream.next().await { + let at_hash = notif.hash; + + log::info!(target: "mev-shield", + "imported block hash={:?} origin={:?}", + at_hash, notif.origin + ); + + match client.block_body(at_hash) { + Ok(Some(body)) => { + log::info!(target: "mev-shield", + " block has {} extrinsics", body.len() + ); + + for (idx, opaque_xt) in body.into_iter().enumerate() { + let encoded = opaque_xt.encode(); + log::info!(target: "mev-shield", + " [xt #{idx}] opaque len={} bytes", encoded.len() + ); + + let uxt: RUnchecked = match RUnchecked::decode(&mut &encoded[..]) { + Ok(u) => u, + Err(e) => { + log::info!(target: "mev-shield", + " [xt #{idx}] failed to decode UncheckedExtrinsic: {:?}", e + ); + continue; + } + }; + + log::info!(target: "mev-shield", + " [xt #{idx}] decoded call: {:?}", &uxt.0.function + ); + + let author_opt: Option = + match &uxt.0.preamble { + sp_runtime::generic::Preamble::Signed(addr, _sig, _ext) => { + match addr.clone() { + Address::Id(acc) => Some(acc), + Address::Address32(bytes) => + Some(sp_runtime::AccountId32::new(bytes)), + _ => None, + } + } + _ => None, + }; + let Some(author) = author_opt else { + log::info!(target: "mev-shield", + " [xt #{idx}] not a Signed(AccountId32) extrinsic; skipping" + ); + continue; + }; + + if let node_subtensor_runtime::RuntimeCall::MevShield( + pallet_mev_shield::Call::submit_encrypted { + key_epoch, + commitment, + ciphertext, + .. + } + ) = &uxt.0.function + { + let payload = (author.clone(), *commitment, ciphertext).encode(); + let id = H256(sp_core::hashing::blake2_256(&payload)); + + log::info!(target: "mev-shield", + " [xt #{idx}] buffered submit_encrypted: id=0x{}, key_epoch={}, author={}, ct_len={}, commitment={:?}", + hex::encode(id.as_bytes()), key_epoch, author, ciphertext.len(), commitment + ); + + buffer.lock().unwrap().upsert( + id, *key_epoch, author, ciphertext.to_vec(), + ); + } + } + } + Ok(None) => log::info!(target: "mev-shield", + " block_body returned None for hash={:?}", at_hash + ), + Err(e) => log::info!(target: "mev-shield", + " block_body error for hash={:?}: {:?}", at_hash, e + ), + } + } + }, + ); + } + + // ── 2) last-3s revealer ───────────────────────────────────── + { + let client = Arc::clone(&client); + let pool = Arc::clone(&pool); + let buffer = Arc::clone(&buffer); + let ctx = ctx.clone(); + + task_spawner.spawn( + "mev-shield-last-3s-revealer", + None, + async move { + log::info!(target: "mev-shield", "last-3s-revealer task started"); + + loop { + let tail = ctx.timing.slot_ms.saturating_sub(ctx.timing.decrypt_window_ms); + log::info!(target: "mev-shield", + "revealer: sleeping {} ms before decrypt window (slot_ms={}, decrypt_window_ms={})", + tail, ctx.timing.slot_ms, ctx.timing.decrypt_window_ms + ); + tokio::time::sleep(Duration::from_millis(tail)).await; + + // Snapshot the *current* ML‑KEM secret and epoch. + let (curr_sk_bytes, curr_epoch, curr_pk_len, next_pk_len, sk_hash) = { + let k = ctx.keys.lock().unwrap(); + let sk_hash = sp_core::hashing::blake2_256(&k.current_sk); + ( + k.current_sk.clone(), + k.epoch, + k.current_pk.len(), + k.next_pk.len(), + sk_hash, + ) + }; + + log::info!(target: "mev-shield", + "revealer: decrypt window start. epoch={} sk_len={} sk_hash=0x{} curr_pk_len={} next_pk_len={}", + curr_epoch, curr_sk_bytes.len(), hex::encode(sk_hash), curr_pk_len, next_pk_len + ); + + // Only process wrappers whose key_epoch == curr_epoch. + let drained: Vec<(H256, u64, sp_runtime::AccountId32, Vec)> = { + let mut buf = buffer.lock().unwrap(); + buf.drain_for_epoch(curr_epoch) + }; + + log::info!(target: "mev-shield", + "revealer: drained {} buffered wrappers for current epoch={}", + drained.len(), curr_epoch + ); + + let mut to_submit: Vec<(H256, node_subtensor_runtime::RuntimeCall)> = Vec::new(); + + for (id, key_epoch, author, blob) in drained.into_iter() { + log::info!(target: "mev-shield", + "revealer: candidate id=0x{} key_epoch={} (curr_epoch={}) author={} blob_len={}", + hex::encode(id.as_bytes()), key_epoch, curr_epoch, author, blob.len() + ); + + if blob.len() < 2 { + log::info!(target: "mev-shield", + " id=0x{}: blob too short (<2 bytes)", hex::encode(id.as_bytes()) + ); + continue; + } + let kem_len = u16::from_le_bytes([blob[0], blob[1]]) as usize; + if blob.len() < 2 + kem_len + 24 { + log::info!(target: "mev-shield", + " id=0x{}: blob too short (kem_len={}, total={})", + hex::encode(id.as_bytes()), kem_len, blob.len() + ); + continue; + } + let kem_ct_bytes = &blob[2 .. 2 + kem_len]; + let nonce_bytes = &blob[2 + kem_len .. 2 + kem_len + 24]; + let aead_body = &blob[2 + kem_len + 24 ..]; + + let kem_ct_hash = sp_core::hashing::blake2_256(kem_ct_bytes); + let aead_body_hash = sp_core::hashing::blake2_256(aead_body); + log::info!(target: "mev-shield", + " id=0x{}: kem_len={} kem_ct_hash=0x{} nonce=0x{} aead_body_len={} aead_body_hash=0x{}", + hex::encode(id.as_bytes()), kem_len, + hex::encode(kem_ct_hash), + hex::encode(nonce_bytes), + aead_body.len(), + hex::encode(aead_body_hash), + ); + + // Rebuild DecapsulationKey and decapsulate. + let enc_sk = match Encoded::>::try_from(&curr_sk_bytes[..]) { + Ok(e) => e, + Err(e) => { + log::info!(target: "mev-shield", + " id=0x{}: DecapsulationKey::try_from(sk_bytes) failed (len={}, err={:?})", + hex::encode(id.as_bytes()), curr_sk_bytes.len(), e + ); + continue; + } + }; + let sk = DecapsulationKey::::from_bytes(&enc_sk); + + let ct = match Ciphertext::::try_from(kem_ct_bytes) { + Ok(c) => c, + Err(e) => { + log::info!(target: "mev-shield", + " id=0x{}: Ciphertext::try_from failed: {:?}", + hex::encode(id.as_bytes()), e + ); + continue; + } + }; + + let ss = match sk.decapsulate(&ct) { + Ok(s) => s, + Err(_) => { + log::info!(target: "mev-shield", + " id=0x{}: ML‑KEM decapsulate() failed", + hex::encode(id.as_bytes()) + ); + continue; + } + }; + + let ss_bytes: &[u8] = ss.as_ref(); + if ss_bytes.len() != 32 { + log::info!(target: "mev-shield", + " id=0x{}: shared secret len={} != 32; skipping", + hex::encode(id.as_bytes()), ss_bytes.len() + ); + continue; + } + let mut ss32 = [0u8; 32]; + ss32.copy_from_slice(ss_bytes); + + let ss_hash = sp_core::hashing::blake2_256(&ss32); + let aead_key = crate::mev_shield::author::derive_aead_key(&ss32); + let key_hash = sp_core::hashing::blake2_256(&aead_key); + + log::info!(target: "mev-shield", + " id=0x{}: decapsulated shared_secret_len=32 shared_secret_hash=0x{}", + hex::encode(id.as_bytes()), hex::encode(ss_hash) + ); + log::info!(target: "mev-shield", + " id=0x{}: derived AEAD key hash=0x{} (direct-from-ss)", + hex::encode(id.as_bytes()), hex::encode(key_hash) + ); + + let mut nonce24 = [0u8; 24]; + nonce24.copy_from_slice(nonce_bytes); + + log::info!(target: "mev-shield", + " id=0x{}: attempting AEAD decrypt nonce=0x{} ct_len={}", + hex::encode(id.as_bytes()), hex::encode(nonce24), aead_body.len() + ); + + let plaintext = match crate::mev_shield::author::aead_decrypt( + aead_key, + nonce24, + aead_body, + &[], + ) { + Some(pt) => pt, + None => { + log::info!(target: "mev-shield", + " id=0x{}: AEAD decrypt FAILED with direct-from-ss key; ct_hash=0x{}", + hex::encode(id.as_bytes()), + hex::encode(aead_body_hash), + ); + continue; + } + }; + + log::info!(target: "mev-shield", + " id=0x{}: AEAD decrypt OK, plaintext_len={}", + hex::encode(id.as_bytes()), plaintext.len() + ); + + // Decode plaintext layout… + type RuntimeNonce = ::Nonce; + + if plaintext.len() < 32 + 4 + 1 + 1 + 64 { + log::info!(target: "mev-shield", + " id=0x{}: plaintext too short ({}) for expected layout", + hex::encode(id.as_bytes()), plaintext.len() + ); + continue; + } + + let signer_raw = &plaintext[0..32]; + let nonce_le = &plaintext[32..36]; + let _mortality_byte = plaintext[36]; + + let sig_off = plaintext.len() - 65; + let call_bytes = &plaintext[37 .. sig_off]; + let sig_kind = plaintext[sig_off]; + let sig_raw = &plaintext[sig_off + 1 ..]; + + let signer = sp_runtime::AccountId32::new( + <[u8; 32]>::try_from(signer_raw).expect("signer_raw is 32 bytes; qed"), + ); + let raw_nonce_u32 = u32::from_le_bytes( + <[u8; 4]>::try_from(nonce_le).expect("nonce_le is 4 bytes; qed"), + ); + let account_nonce: RuntimeNonce = raw_nonce_u32.saturated_into(); + let mortality = Era::Immortal; + + let inner_call: node_subtensor_runtime::RuntimeCall = + match Decode::decode(&mut &call_bytes[..]) { + Ok(c) => c, + Err(e) => { + log::info!(target: "mev-shield", + " id=0x{}: failed to decode RuntimeCall (len={}): {:?}", + hex::encode(id.as_bytes()), call_bytes.len(), e + ); + continue; + } + }; + + let signature: MultiSignature = if sig_kind == 0x01 && sig_raw.len() == 64 { + let mut raw = [0u8; 64]; + raw.copy_from_slice(sig_raw); + MultiSignature::from(sp_core::sr25519::Signature::from_raw(raw)) + } else { + log::info!(target: "mev-shield", + " id=0x{}: unsupported signature format kind=0x{:02x}, len={}", + hex::encode(id.as_bytes()), sig_kind, sig_raw.len() + ); + continue; + }; + + log::info!(target: "mev-shield", + " id=0x{}: decrypted wrapper: signer={}, nonce={}, call={:?}", + hex::encode(id.as_bytes()), signer, raw_nonce_u32, inner_call + ); + + let reveal = node_subtensor_runtime::RuntimeCall::MevShield( + pallet_mev_shield::Call::execute_revealed { + id, + signer: signer.clone(), + nonce: account_nonce, + mortality, + call: Box::new(inner_call), + signature, + } + ); + + to_submit.push((id, reveal)); + } + + // Submit locally. + let at = client.info().best_hash; + log::info!(target: "mev-shield", + "revealer: submitting {} execute_revealed calls at best_hash={:?}", + to_submit.len(), at + ); + + for (id, call) in to_submit.into_iter() { + let uxt: node_subtensor_runtime::UncheckedExtrinsic = + node_subtensor_runtime::UncheckedExtrinsic::new_bare(call); + let xt_bytes = uxt.encode(); + + log::info!(target: "mev-shield", + " id=0x{}: encoded UncheckedExtrinsic len={}", + hex::encode(id.as_bytes()), xt_bytes.len() + ); + + match OpaqueExtrinsic::from_bytes(&xt_bytes) { + Ok(opaque) => { + match pool.submit_one(at, TransactionSource::Local, opaque).await { + Ok(_) => { + let xt_hash = sp_core::hashing::blake2_256(&xt_bytes); + log::info!(target: "mev-shield", + " id=0x{}: submit_one(execute_revealed) OK, xt_hash=0x{}", + hex::encode(id.as_bytes()), hex::encode(xt_hash) + ); + } + Err(e) => { + log::info!(target: "mev-shield", + " id=0x{}: submit_one(execute_revealed) FAILED: {:?}", + hex::encode(id.as_bytes()), e + ); + } + } + } + Err(e) => { + log::info!(target: "mev-shield", + " id=0x{}: OpaqueExtrinsic::from_bytes failed: {:?}", + hex::encode(id.as_bytes()), e + ); + } + } + } + + tokio::time::sleep(Duration::from_millis(ctx.timing.decrypt_window_ms)).await; + } + }, + ); + } +} diff --git a/node/src/service.rs b/node/src/service.rs index 2ef1904f08..1098faec73 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -28,12 +28,18 @@ use std::{cell::RefCell, path::Path}; use std::{sync::Arc, time::Duration}; use substrate_prometheus_endpoint::Registry; +use crate::mev_shield::{author, proposer}; use crate::cli::Sealing; use crate::client::{FullBackend, FullClient, HostFunctions, RuntimeExecutor}; use crate::ethereum::{ BackendType, EthConfiguration, FrontierBackend, FrontierPartialComponents, StorageOverride, StorageOverrideHandler, db_config_dir, new_frontier_partial, spawn_frontier_tasks, }; +use sp_core::twox_128; +use sc_client_api::StorageKey; +use node_subtensor_runtime::opaque::BlockId; +use sc_client_api::HeaderBackend; +use sc_client_api::StorageProvider; const LOG_TARGET: &str = "node-service"; @@ -534,6 +540,66 @@ where ) .await; + // ==== MEV-SHIELD HOOKS ==== + if role.is_authority() { + // Use the same slot duration the consensus layer reports. + let slot_duration_ms: u64 = consensus_mechanism + .slot_duration(&client)? + .as_millis() as u64; + + // Time windows, runtime constants (7s / 2s grace / last 3s). + let timing = author::TimeParams { + slot_ms: slot_duration_ms, + announce_at_ms: 7_000, + decrypt_window_ms: 3_000, + }; + + // --- imports needed for raw storage read & SCALE decode + use codec::Decode; // parity-scale-codec re-export + use sp_core::{storage::StorageKey, twox_128}; + + // Initialize author‑side epoch from chain storage + let initial_epoch: u64 = { + // Best block hash (H256). NOTE: this method expects H256 by value (not BlockId). + let best = client.info().best_hash; + + // Storage key for pallet "MevShield", item "Epoch": + // final key = twox_128(b"MevShield") ++ twox_128(b"Epoch") + let mut key_bytes = Vec::with_capacity(32); + key_bytes.extend_from_slice(&twox_128(b"MevShield")); + key_bytes.extend_from_slice(&twox_128(b"Epoch")); + let key = StorageKey(key_bytes); + + // Read raw storage at `best` + match client.storage(best, &key) { + Ok(Some(raw_bytes)) => { + // `raw_bytes` is sp_core::storage::StorageData(pub Vec) + // Decode with SCALE: pass a &mut &[u8] over the inner Vec. + u64::decode(&mut &raw_bytes.0[..]).unwrap_or(0) + } + _ => 0, + } + }; + + // Start author-side tasks with the *actual* epoch. + let mev_ctx = author::spawn_author_tasks::( + &task_manager.spawn_handle(), + client.clone(), + transaction_pool.clone(), + keystore_container.keystore(), + initial_epoch, + timing.clone(), + ); + + // Start last-3s revealer (decrypt -> execute_revealed). + proposer::spawn_revealer::( + &task_manager.spawn_handle(), + client.clone(), + transaction_pool.clone(), + mev_ctx.clone(), + ); + } + if role.is_authority() { // manual-seal authorship if let Some(sealing) = sealing { diff --git a/pallets/mev-shield/Cargo.toml b/pallets/mev-shield/Cargo.toml new file mode 100644 index 0000000000..b22a4b174c --- /dev/null +++ b/pallets/mev-shield/Cargo.toml @@ -0,0 +1,71 @@ +[package] +name = "pallet-mev-shield" +description = "FRAME pallet for opt-in, per-block ephemeral-key encrypted transactions, MEV-shielded." +authors = ["Subtensor Contributors "] +version = "0.0.1" +license = "Unlicense" +edition.workspace = true +homepage = "https://github.com/opentensor/subtensor" +repository = "https://github.com/opentensor/subtensor" +publish = false + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { workspace = true, features = ["derive", "max-encoded-len"] } +scale-info = { workspace = true, features = ["derive"] } + +subtensor-macros.workspace = true + +# FRAME core +frame-support.workspace = true +frame-system.workspace = true +frame-benchmarking = { workspace = true, optional = true } + +# Substrate primitives +sp-core.workspace = true +sp-runtime.workspace = true +sp-io.workspace = true +sp-std.workspace = true +sp-weights.workspace = true + +# Pallets used in Config +pallet-timestamp.workspace = true +pallet-aura.workspace = true +sp-consensus-aura.workspace = true + +[dev-dependencies] + +[features] +default = [] + +std = [ + "codec/std", + "scale-info/std", + + "frame-support/std", + "frame-system/std", + "frame-benchmarking?/std", + + "sp-core/std", + "sp-runtime/std", + "sp-io/std", + "sp-std/std", + "sp-weights/std", + + "pallet-timestamp/std", +] + +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] + +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/mev-shield/src/lib.rs b/pallets/mev-shield/src/lib.rs new file mode 100644 index 0000000000..1aecf27f27 --- /dev/null +++ b/pallets/mev-shield/src/lib.rs @@ -0,0 +1,473 @@ +// pallets/mev-shield/src/lib.rs +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{ + dispatch::{DispatchClass, GetDispatchInfo, Pays, PostDispatchInfo}, + pallet_prelude::*, + traits::{ConstU32, Currency}, + weights::Weight, + }; + use frame_system::pallet_prelude::*; + use sp_runtime::{ + AccountId32, MultiSignature, RuntimeDebug, + traits::{Dispatchable, Hash, Verify, Zero, BadOrigin}, + }; + use sp_std::{marker::PhantomData, prelude::*}; + use subtensor_macros::freeze_struct; + use sp_consensus_aura::sr25519::AuthorityId as AuraAuthorityId; + use sp_core::ByteArray; + + // ------------------------------------------------------------------------- + // Origin helper: ensure the signer is an Aura authority (no session/authorship). + // ------------------------------------------------------------------------- + // + // This checks: + // 1) origin is Signed(AccountId32) + // 2) AccountId32 bytes map to an Aura AuthorityId + // 3) that AuthorityId is a member of pallet_aura::Authorities + // + // NOTE: Assumes AccountId32 corresponds to sr25519 public key bytes (typical for Substrate). +/// Ensure that the origin is `Signed` by an account whose bytes map to the current +/// Aura `AuthorityId` and that this id is present in `pallet_aura::Authorities`. +pub struct EnsureAuraAuthority(PhantomData); + +pub trait AuthorityOriginExt { + type AccountId; + + fn ensure_validator(origin: Origin) -> Result; +} + +impl AuthorityOriginExt> for EnsureAuraAuthority +where + T: frame_system::Config + + pallet_aura::Config, +{ + type AccountId = AccountId32; + + fn ensure_validator(origin: OriginFor) -> Result { + // Require a signed origin. + let who: AccountId32 = frame_system::ensure_signed(origin)?; + + // Convert the raw 32 bytes of the AccountId into an Aura AuthorityId. + // This uses `ByteArray::from_slice` to avoid any `sp_application_crypto` imports. + let aura_id = + ::from_slice(who.as_ref()).map_err(|_| BadOrigin)?; + + // Check the current authority set. + let is_validator = pallet_aura::Authorities::::get() + .into_iter() + .any(|id| id == aura_id); + + if is_validator { + Ok(who) + } else { + Err(BadOrigin) + } + } +} + + // ----------------- Types ----------------- + + /// AEAD‑independent commitment over the revealed payload. + /// We commit to `(signer, nonce, mortality, encoded_call)` (see `execute_revealed`). + #[freeze_struct("62e25176827ab9b")] + #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub struct Submission { + pub author: AccountId, // fee payer (wrapper submitter) + pub key_epoch: u64, // epoch for which this was encrypted + pub commitment: Hash, // chain hash over (signer, nonce, mortality, call) + pub ciphertext: BoundedVec>, + pub payload_version: u16, // upgrade path + pub submitted_in: BlockNumber, + pub submitted_at: Moment, + pub max_weight: Weight, // upper bound user is prepaying + } + + /// Ephemeral key **fingerprint** used by off-chain code to verify the ML‑KEM pubkey it received via gossip. + /// + /// **Important:** `key` is **not** the ML‑KEM public key itself (those are ~1.1 KiB). + /// We publish a 32‑byte `blake2_256(pk_bytes)` fingerprint instead to keep storage/events small. + #[freeze_struct("daa971a48d20a3d9")] + #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub struct EphemeralPubKey { + /// Full Kyber768 public key bytes (length must be exactly 1184). + pub public_key: BoundedVec>, + /// For traceability across announcements/rolls. + pub epoch: u64, + } + + // ----------------- Config ----------------- + + #[pallet::config] + pub trait Config: + // System event type and AccountId32 + frame_system::Config>> + // Timestamp is used by the pallet. + + pallet_timestamp::Config + // 🔴 We read the Aura authority set (no session/authorship needed). + + pallet_aura::Config + { + /// Allow dispatch of revealed inner calls. + type RuntimeCall: Parameter + + sp_runtime::traits::Dispatchable< + RuntimeOrigin = Self::RuntimeOrigin, + PostInfo = PostDispatchInfo + > + + GetDispatchInfo; + + /// Who may announce the next ephemeral key. + /// + /// In your runtime set: + /// type AuthorityOrigin = pallet_mev_shield::EnsureAuraAuthority; + /// + /// This ensures the signer is a current Aura authority. + type AuthorityOrigin: AuthorityOriginExt; + + /// Parameters exposed on-chain for light clients / UI (ms). + #[pallet::constant] + type SlotMs: Get; + #[pallet::constant] + type AnnounceAtMs: Get; // e.g., 7000 + #[pallet::constant] + type GraceMs: Get; // e.g., 2000 (old key valid until 9s) + #[pallet::constant] + type DecryptWindowMs: Get; // e.g., 3000 (last 3s) + + /// Currency for fees (wrapper pays normal tx fee via regular machinery). + type Currency: Currency; + } + + #[pallet::pallet] + pub struct Pallet(_); + + // ----------------- Storage ----------------- + + #[pallet::storage] + pub type CurrentKey = StorageValue<_, EphemeralPubKey, OptionQuery>; + + #[pallet::storage] + pub type NextKey = StorageValue<_, EphemeralPubKey, OptionQuery>; + + #[pallet::storage] + pub type Epoch = StorageValue<_, u64, ValueQuery>; + + /// All encrypted submissions live here until executed or discarded. + #[pallet::storage] + pub type Submissions = StorageMap< + _, + Blake2_128Concat, + T::Hash, // id = hash(author, commitment, ciphertext) + Submission, T::Moment, T::Hash>, + OptionQuery, + >; + + /// Mark a submission id as consumed (executed or invalidated). + #[pallet::storage] + pub type Consumed = StorageMap<_, Blake2_128Concat, T::Hash, (), OptionQuery>; + + // ----------------- Events & Errors ----------------- + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Next ML‑KEM public key bytes announced. + NextKeyAnnounced { + public_key: Vec, // full Kyber768 public key (1184 bytes) + epoch: u64, + at_ms: u64, + }, + /// Current key rolled to the next (happens on_initialize of new block). + KeyRolled { + public_key: Vec, // full Kyber768 public key (1184 bytes) + epoch: u64 + }, + /// Encrypted wrapper accepted. + EncryptedSubmitted { id: T::Hash, who: T::AccountId, epoch: u64 }, + /// Decrypted call executed. + DecryptedExecuted { id: T::Hash, signer: T::AccountId, actual_weight: Weight }, + /// Decrypted execution rejected (mismatch, overweight, bad sig, etc.). + DecryptedRejected { id: T::Hash, reason: u8 }, + } + + #[pallet::error] + pub enum Error { + BadEpoch, + AlreadyConsumed, + MissingSubmission, + CommitmentMismatch, + SignatureInvalid, + WeightTooHigh, + NonceMismatch, + BadPublicKeyLen, + } + + // ----------------- Hooks ----------------- + + #[pallet::hooks] + impl Hooks> for Pallet { + /// Roll the keys once per block (current <= next). + fn on_initialize(_n: BlockNumberFor) -> Weight { + if let Some(next) = >::take() { + >::put(&next); + >::mutate(|e| *e = next.epoch); + + // Emit event with the full public key bytes (convert BoundedVec -> Vec for the event). + Self::deposit_event(Event::::KeyRolled { + public_key: next.public_key.to_vec(), + epoch: next.epoch, + }); + } + // small constant cost + T::DbWeight::get().reads_writes(1, 2) + } + } + + // ----------------- Calls ----------------- + + #[pallet::call] + impl Pallet { + /// Validators announce the *next* ephemeral **ML‑KEM** public key bytes. + /// Origin is restricted to the PoA validator set (Aura authorities). + #[pallet::call_index(0)] + #[pallet::weight( + Weight::from_parts(5_000, 0) + .saturating_add(T::DbWeight::get().reads(0_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + )] + pub fn announce_next_key( + origin: OriginFor, + public_key: BoundedVec>, + epoch: u64, + at_ms: u64, + ) -> DispatchResult { + // ✅ Only a current Aura validator may call this (signed account ∈ Aura authorities) + T::AuthorityOrigin::ensure_validator(origin)?; + + // Enforce Kyber768 pk length (1184 bytes). + ensure!(public_key.len() == 1184, Error::::BadPublicKeyLen); + + NextKey::::put(EphemeralPubKey { public_key: public_key.clone(), epoch }); + + // Emit full bytes in the event (convert to Vec for simplicity). + Self::deposit_event(Event::NextKeyAnnounced { + public_key: public_key.to_vec(), + epoch, + at_ms, + }); + Ok(()) + } + + /// Users submit encrypted wrapper paying the normal fee. + /// `commitment = blake2_256( SCALE( (signer, nonce, mortality, call) ) )` + /// + /// Ciphertext format (see module docs): `[u16 kem_len][kem_ct][nonce24][aead_ct]` + #[pallet::call_index(1)] + #[pallet::weight({ + let w = Weight::from_parts(ciphertext.len() as u64, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) // Epoch::get + .saturating_add(T::DbWeight::get().writes(1_u64)); // Submissions::insert + w + })] + pub fn submit_encrypted( + origin: OriginFor, + key_epoch: u64, + commitment: T::Hash, + ciphertext: BoundedVec>, + payload_version: u16, + max_weight: Weight, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!( + key_epoch == Epoch::::get() || key_epoch + 1 == Epoch::::get(), + Error::::BadEpoch + ); + + let now = pallet_timestamp::Pallet::::get(); + let id: T::Hash = T::Hashing::hash_of(&(who.clone(), commitment, &ciphertext)); + let sub = Submission::, T::Moment, T::Hash> { + author: who.clone(), + key_epoch, + commitment, + ciphertext, + payload_version, + submitted_in: >::block_number(), + submitted_at: now, + max_weight, + }; + ensure!( + !Submissions::::contains_key(id), + Error::::AlreadyConsumed + ); + Submissions::::insert(id, sub); + Self::deposit_event(Event::EncryptedSubmitted { + id, + who, + epoch: key_epoch, + }); + Ok(()) + } + + /// Executed by the block author (unsigned) in the last ~3s. + /// The caller provides the plaintext (signed) and we: + /// - check commitment + /// - check signature + /// - check nonce (and increment) + /// - ensure weight <= max_weight + /// - dispatch the call as the signer (fee-free here; fee already paid by wrapper) + #[pallet::call_index(2)] + #[pallet::weight( + Weight::from_parts(10_000, 0) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + )] + pub fn execute_revealed( + origin: OriginFor, + id: T::Hash, + signer: T::AccountId, // AccountId32 due to Config bound + nonce: T::Nonce, + mortality: sp_runtime::generic::Era, // same type used in signed extrinsics + call: Box<::RuntimeCall>, + signature: MultiSignature, + ) -> DispatchResultWithPostInfo { + ensure_none(origin)?; + ensure!( + !Consumed::::contains_key(id), + Error::::AlreadyConsumed + ); + let Some(sub) = Submissions::::take(id) else { + return Err(Error::::MissingSubmission.into()); + }; + + // 1) Commitment check (encode by-ref to avoid Clone bound on RuntimeCall) + let payload_bytes = (signer.clone(), nonce, mortality.clone(), call.as_ref()).encode(); + let recomputed: T::Hash = T::Hashing::hash_of(&payload_bytes); + ensure!(sub.commitment == recomputed, Error::::CommitmentMismatch); + + // 2) Signature check over the same payload (domain separated) + let genesis = frame_system::Pallet::::block_hash(BlockNumberFor::::zero()); + let mut msg = b"mev-shield:v1".to_vec(); + msg.extend_from_slice(genesis.as_ref()); + msg.extend_from_slice(&payload_bytes); + ensure!( + signature.verify(msg.as_slice(), &signer), + Error::::SignatureInvalid + ); + + // 3) Nonce check & bump (we mimic system signed extension behavior) + let acc = frame_system::Pallet::::account_nonce(&signer); + ensure!(acc == nonce, Error::::NonceMismatch); + frame_system::Pallet::::inc_account_nonce(&signer); + + // 4) Dispatch inner call from signer; enforce max_weight guard + let info = call.get_dispatch_info(); + let required = info.call_weight.saturating_add(info.extension_weight); + + let leq = required.ref_time() <= sub.max_weight.ref_time() + && required.proof_size() <= sub.max_weight.proof_size(); + ensure!(leq, Error::::WeightTooHigh); + + let origin_signed = frame_system::RawOrigin::Signed(signer.clone()).into(); + let res = (*call).dispatch(origin_signed); + + // Mark as consumed regardless of outcome (user already paid wrapper fee) + Consumed::::insert(id, ()); + + match res { + Ok(post) => { + let actual = post.actual_weight.unwrap_or(required); + Self::deposit_event(Event::DecryptedExecuted { + id, + signer, + actual_weight: actual, + }); + Ok(PostDispatchInfo { + actual_weight: Some(actual), + pays_fee: Pays::No, + }) + } + Err(_e) => { + Self::deposit_event(Event::DecryptedRejected { id, reason: 1 }); + Ok(PostDispatchInfo { + actual_weight: Some(required), + pays_fee: Pays::No, + }) + } + } + } + } + + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + + fn validate_unsigned( + _source: sp_runtime::transaction_validity::TransactionSource, + call: &Self::Call, + ) -> sp_runtime::transaction_validity::TransactionValidity { + use sp_runtime::transaction_validity::{InvalidTransaction, ValidTransaction}; + + match call { + // This is the only unsigned entry point. + Call::execute_revealed { id, .. } => { + // Mark this unsigned tx as valid for the pool & block builder. + // + // IMPORTANT: + // - We *do* want it in the local pool so that the block author + // can include it in the next block. + // - We *do not* want it to be gossiped, otherwise the cleartext + // MEV‑shielded call leaks to the network. + // + // `propagate(false)` keeps it strictly local. + ValidTransaction::with_tag_prefix("mev-shield-exec") + .priority(u64::MAX) // always prefer executes when present + .longevity(1) // only for the very next block + .and_provides(id) // crucial: at least one tag + .propagate(false) // 👈 no gossip / no mempool MEV + .build() + } + + // Any other unsigned call from this pallet is invalid. + _ => InvalidTransaction::Call.into(), + } + } + } + + + + // #[pallet::validate_unsigned] + // impl ValidateUnsigned for Pallet { + // type Call = Call; + + // fn validate_unsigned( + // _source: sp_runtime::transaction_validity::TransactionSource, + // call: &Self::Call, + // ) -> sp_runtime::transaction_validity::TransactionValidity { + // use sp_runtime::transaction_validity::{InvalidTransaction, ValidTransaction}; + + // match call { + // // This is the only unsigned entry point. + // Call::execute_revealed { id, .. } => { + // // Mark this unsigned tx as valid for the pool & block builder. + // // - no source check: works for pool, block building, and block import + // // - propagate(true): gossip so *whoever* authors next block sees it + // // - provides(id): lets the pool deduplicate by this id + // ValidTransaction::with_tag_prefix("mev-shield-exec") + // .priority(u64::MAX) // always prefer executes when present + // .longevity(1) // only for the very next block + // .and_provides(id) // crucial: at least one tag + // .propagate(true) // <-- changed from false to true + // .build() + // } + + // // Any other unsigned call from this pallet is invalid. + // _ => InvalidTransaction::Call.into(), + // } + // } + // } +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 9760ac1b53..bd687e4427 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -149,6 +149,9 @@ ark-serialize = { workspace = true, features = ["derive"] } # Crowdloan pallet-crowdloan.workspace = true +# Mev Shield +pallet-mev-shield.workspace = true + ethereum.workspace = true [dev-dependencies] diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 42f35cbc80..5d95458a6e 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -28,6 +28,7 @@ use frame_support::{ use frame_system::{EnsureRoot, EnsureRootWithSuccess, EnsureSigned}; use pallet_commitments::{CanCommit, OnMetadataCommitment}; use pallet_grandpa::{AuthorityId as GrandpaId, fg_primitives}; +pub use pallet_mev_shield; use pallet_registry::CanRegisterIdentity; use pallet_subtensor::rpc_info::{ delegate_info::DelegateInfo, @@ -119,6 +120,27 @@ impl frame_system::offchain::SigningTypes for Runtime { type Signature = Signature; } +impl pallet_mev_shield::Config for Runtime { + type RuntimeCall = RuntimeCall; + type SlotMs = MevShieldSlotMs; + type AnnounceAtMs = MevShieldAnnounceAtMs; + type GraceMs = MevShieldGraceMs; + type DecryptWindowMs = MevShieldDecryptWindowMs; + type Currency = Balances; + type AuthorityOrigin = pallet_mev_shield::EnsureAuraAuthority; +} + +parameter_types! { + /// Milliseconds per slot; use the chain’s configured slot duration. + pub const MevShieldSlotMs: u64 = SLOT_DURATION; + /// Emit the *next* ephemeral public key event at 7s. + pub const MevShieldAnnounceAtMs: u64 = 7_000; + /// Old key remains accepted until 9s (2s grace). + pub const MevShieldGraceMs: u64 = 2_000; + /// Last 3s of the slot reserved for decrypt+execute. + pub const MevShieldDecryptWindowMs: u64 = 3_000; +} + impl frame_system::offchain::CreateTransactionBase for Runtime where RuntimeCall: From, @@ -1569,6 +1591,7 @@ construct_runtime!( Crowdloan: pallet_crowdloan = 27, Swap: pallet_subtensor_swap = 28, Contracts: pallet_contracts = 29, + MevShield: pallet_mev_shield = 30, } ); diff --git a/scripts/localnet.sh b/scripts/localnet.sh index 1b96baa19b..d97a3e65ca 100755 --- a/scripts/localnet.sh +++ b/scripts/localnet.sh @@ -139,6 +139,8 @@ if [ $BUILD_ONLY -eq 0 ]; then trap 'pkill -P $$' EXIT SIGINT SIGTERM ( + # env MEV_SHIELD_ANNOUNCE_ACCOUNT_SEED='//Alice' RUST_LOG="${RUST_LOG:-info,mev-shield=debug}" "${alice_start[@]}" 2>&1 & + # env MEV_SHIELD_ANNOUNCE_ACCOUNT_SEED='//Bob' RUST_LOG="${RUST_LOG:-info,mev-shield=debug}" "${bob_start[@]}" 2>&1 ("${alice_start[@]}" 2>&1) & ("${bob_start[@]}" 2>&1) wait From acdb63142e731e52bd3261b0e19c5995f4fe03f8 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:03:59 -0800 Subject: [PATCH 02/30] finish pallet impl --- node/src/mev_shield/author.rs | 4 - node/src/service.rs | 54 +++--- pallets/mev-shield/src/lib.rs | 351 ++++++++++++++-------------------- 3 files changed, 171 insertions(+), 238 deletions(-) diff --git a/node/src/mev_shield/author.rs b/node/src/mev_shield/author.rs index 26cb713144..f01319b4d1 100644 --- a/node/src/mev_shield/author.rs +++ b/node/src/mev_shield/author.rs @@ -215,7 +215,6 @@ where local_aura_pub.clone(), next_pk.clone(), next_epoch, - timing.announce_at_ms, local_nonce, ) .await @@ -234,7 +233,6 @@ where local_aura_pub.clone(), next_pk, next_epoch, - timing.announce_at_ms, local_nonce.saturating_add(1), ) .await @@ -287,7 +285,6 @@ pub async fn submit_announce_extrinsic( aura_pub: sp_core::sr25519::Public, // local Aura authority public key next_public_key: Vec, // full ML‑KEM pubkey bytes (expected 1184B) epoch: u64, - at_ms: u64, nonce: u32, // nonce for CheckNonce extension ) -> anyhow::Result<()> where @@ -331,7 +328,6 @@ where pallet_mev_shield::Call::announce_next_key { public_key, epoch, - at_ms, } ); diff --git a/node/src/service.rs b/node/src/service.rs index 1098faec73..d492c37d1e 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -40,6 +40,7 @@ use sc_client_api::StorageKey; use node_subtensor_runtime::opaque::BlockId; use sc_client_api::HeaderBackend; use sc_client_api::StorageProvider; +use codec::Decode; const LOG_TARGET: &str = "node-service"; @@ -540,49 +541,38 @@ where ) .await; - // ==== MEV-SHIELD HOOKS ==== + // ==== MEV-SHIELD HOOKS ==== + let mut mev_timing: Option = None; + if role.is_authority() { - // Use the same slot duration the consensus layer reports. let slot_duration_ms: u64 = consensus_mechanism .slot_duration(&client)? .as_millis() as u64; - // Time windows, runtime constants (7s / 2s grace / last 3s). - let timing = author::TimeParams { + // Time windows (7s announce / last 3s decrypt). + let timing = crate::mev_shield::author::TimeParams { slot_ms: slot_duration_ms, announce_at_ms: 7_000, decrypt_window_ms: 3_000, }; - - // --- imports needed for raw storage read & SCALE decode - use codec::Decode; // parity-scale-codec re-export - use sp_core::{storage::StorageKey, twox_128}; + mev_timing = Some(timing.clone()); // Initialize author‑side epoch from chain storage let initial_epoch: u64 = { - // Best block hash (H256). NOTE: this method expects H256 by value (not BlockId). let best = client.info().best_hash; - - // Storage key for pallet "MevShield", item "Epoch": - // final key = twox_128(b"MevShield") ++ twox_128(b"Epoch") let mut key_bytes = Vec::with_capacity(32); key_bytes.extend_from_slice(&twox_128(b"MevShield")); key_bytes.extend_from_slice(&twox_128(b"Epoch")); let key = StorageKey(key_bytes); - // Read raw storage at `best` match client.storage(best, &key) { - Ok(Some(raw_bytes)) => { - // `raw_bytes` is sp_core::storage::StorageData(pub Vec) - // Decode with SCALE: pass a &mut &[u8] over the inner Vec. - u64::decode(&mut &raw_bytes.0[..]).unwrap_or(0) - } + Ok(Some(raw_bytes)) => u64::decode(&mut &raw_bytes.0[..]).unwrap_or(0), _ => 0, } }; - // Start author-side tasks with the *actual* epoch. - let mev_ctx = author::spawn_author_tasks::( + // Start author-side tasks with the epoch. + let mev_ctx = crate::mev_shield::author::spawn_author_tasks::( &task_manager.spawn_handle(), client.clone(), transaction_pool.clone(), @@ -592,7 +582,7 @@ where ); // Start last-3s revealer (decrypt -> execute_revealed). - proposer::spawn_revealer::( + crate::mev_shield::proposer::spawn_revealer::( &task_manager.spawn_handle(), client.clone(), transaction_pool.clone(), @@ -614,7 +604,6 @@ where telemetry.as_ref(), commands_stream, )?; - log::info!("Manual Seal Ready"); return Ok(task_manager); } @@ -628,6 +617,25 @@ where ); let slot_duration = consensus_mechanism.slot_duration(&client)?; + + let start_fraction: f32 = { + let (slot_ms, decrypt_ms) = mev_timing + .as_ref() + .map(|t| (t.slot_ms, t.decrypt_window_ms)) + .unwrap_or((slot_duration.as_millis() as u64, 3_000)); + + let guard_ms: u64 = 200; // small cushion so reveals hit the pool first + let after_decrypt_ms = slot_ms + .saturating_sub(decrypt_ms) + .saturating_add(guard_ms); + + // Clamp into (0.5 .. 0.98] to give the proposer enough time + let mut f = (after_decrypt_ms as f32) / (slot_ms as f32); + if f < 0.50 { f = 0.50; } + if f > 0.98 { f = 0.98; } + f + }; + let create_inherent_data_providers = move |_, ()| async move { CM::create_inherent_data_providers(slot_duration) }; @@ -645,7 +653,7 @@ where force_authoring, backoff_authoring_blocks, keystore: keystore_container.keystore(), - block_proposal_slot_portion: SlotProportion::new(2f32 / 3f32), + block_proposal_slot_portion: SlotProportion::new(start_fraction), max_block_proposal_slot_portion: None, telemetry: telemetry.as_ref().map(|x| x.handle()), }, diff --git a/pallets/mev-shield/src/lib.rs b/pallets/mev-shield/src/lib.rs index 1aecf27f27..0d64e6f73c 100644 --- a/pallets/mev-shield/src/lib.rs +++ b/pallets/mev-shield/src/lib.rs @@ -7,7 +7,7 @@ pub use pallet::*; pub mod pallet { use super::*; use frame_support::{ - dispatch::{DispatchClass, GetDispatchInfo, Pays, PostDispatchInfo}, + dispatch::{GetDispatchInfo, PostDispatchInfo}, pallet_prelude::*, traits::{ConstU32, Currency}, weights::Weight, @@ -15,89 +15,70 @@ pub mod pallet { use frame_system::pallet_prelude::*; use sp_runtime::{ AccountId32, MultiSignature, RuntimeDebug, - traits::{Dispatchable, Hash, Verify, Zero, BadOrigin}, + traits::{BadOrigin, Dispatchable, Hash, Verify, Zero, SaturatedConversion}, + DispatchErrorWithPostInfo, }; use sp_std::{marker::PhantomData, prelude::*}; use subtensor_macros::freeze_struct; use sp_consensus_aura::sr25519::AuthorityId as AuraAuthorityId; use sp_core::ByteArray; + use codec::Encode; - // ------------------------------------------------------------------------- - // Origin helper: ensure the signer is an Aura authority (no session/authorship). - // ------------------------------------------------------------------------- - // - // This checks: - // 1) origin is Signed(AccountId32) - // 2) AccountId32 bytes map to an Aura AuthorityId - // 3) that AuthorityId is a member of pallet_aura::Authorities - // - // NOTE: Assumes AccountId32 corresponds to sr25519 public key bytes (typical for Substrate). -/// Ensure that the origin is `Signed` by an account whose bytes map to the current -/// Aura `AuthorityId` and that this id is present in `pallet_aura::Authorities`. -pub struct EnsureAuraAuthority(PhantomData); - -pub trait AuthorityOriginExt { - type AccountId; - - fn ensure_validator(origin: Origin) -> Result; -} + /// Origin helper: ensure the signer is an Aura authority (no session/authorship). + pub struct EnsureAuraAuthority(PhantomData); + + pub trait AuthorityOriginExt { + type AccountId; + + fn ensure_validator(origin: Origin) -> Result; + } + + impl AuthorityOriginExt> for EnsureAuraAuthority + where + T: frame_system::Config + + pallet_aura::Config, + { + type AccountId = AccountId32; + + fn ensure_validator(origin: OriginFor) -> Result { + let who: AccountId32 = frame_system::ensure_signed(origin)?; + + let aura_id = + ::from_slice(who.as_ref()).map_err(|_| BadOrigin)?; -impl AuthorityOriginExt> for EnsureAuraAuthority -where - T: frame_system::Config - + pallet_aura::Config, -{ - type AccountId = AccountId32; - - fn ensure_validator(origin: OriginFor) -> Result { - // Require a signed origin. - let who: AccountId32 = frame_system::ensure_signed(origin)?; - - // Convert the raw 32 bytes of the AccountId into an Aura AuthorityId. - // This uses `ByteArray::from_slice` to avoid any `sp_application_crypto` imports. - let aura_id = - ::from_slice(who.as_ref()).map_err(|_| BadOrigin)?; - - // Check the current authority set. - let is_validator = pallet_aura::Authorities::::get() - .into_iter() - .any(|id| id == aura_id); - - if is_validator { - Ok(who) - } else { - Err(BadOrigin) + let is_validator = pallet_aura::Authorities::::get() + .into_iter() + .any(|id| id == aura_id); + + if is_validator { + Ok(who) + } else { + Err(BadOrigin) + } } } -} // ----------------- Types ----------------- /// AEAD‑independent commitment over the revealed payload. - /// We commit to `(signer, nonce, mortality, encoded_call)` (see `execute_revealed`). - #[freeze_struct("62e25176827ab9b")] + #[freeze_struct("6c00690caddfeb78")] #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct Submission { - pub author: AccountId, // fee payer (wrapper submitter) - pub key_epoch: u64, // epoch for which this was encrypted - pub commitment: Hash, // chain hash over (signer, nonce, mortality, call) + pub author: AccountId, + pub key_epoch: u64, + pub commitment: Hash, pub ciphertext: BoundedVec>, - pub payload_version: u16, // upgrade path + pub payload_version: u16, pub submitted_in: BlockNumber, pub submitted_at: Moment, - pub max_weight: Weight, // upper bound user is prepaying + pub max_weight: Weight, } - /// Ephemeral key **fingerprint** used by off-chain code to verify the ML‑KEM pubkey it received via gossip. - /// - /// **Important:** `key` is **not** the ML‑KEM public key itself (those are ~1.1 KiB). - /// We publish a 32‑byte `blake2_256(pk_bytes)` fingerprint instead to keep storage/events small. - #[freeze_struct("daa971a48d20a3d9")] + /// Ephemeral key fingerprint used by off-chain code to verify the ML‑KEM pubkey. + #[freeze_struct("4e13d24516013712")] #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct EphemeralPubKey { - /// Full Kyber768 public key bytes (length must be exactly 1184). pub public_key: BoundedVec>, - /// For traceability across announcements/rolls. pub epoch: u64, } @@ -105,14 +86,10 @@ where #[pallet::config] pub trait Config: - // System event type and AccountId32 frame_system::Config>> - // Timestamp is used by the pallet. + pallet_timestamp::Config - // 🔴 We read the Aura authority set (no session/authorship needed). + pallet_aura::Config { - /// Allow dispatch of revealed inner calls. type RuntimeCall: Parameter + sp_runtime::traits::Dispatchable< RuntimeOrigin = Self::RuntimeOrigin, @@ -120,25 +97,17 @@ where > + GetDispatchInfo; - /// Who may announce the next ephemeral key. - /// - /// In your runtime set: - /// type AuthorityOrigin = pallet_mev_shield::EnsureAuraAuthority; - /// - /// This ensures the signer is a current Aura authority. type AuthorityOrigin: AuthorityOriginExt; - /// Parameters exposed on-chain for light clients / UI (ms). #[pallet::constant] type SlotMs: Get; #[pallet::constant] - type AnnounceAtMs: Get; // e.g., 7000 + type AnnounceAtMs: Get; #[pallet::constant] - type GraceMs: Get; // e.g., 2000 (old key valid until 9s) + type GraceMs: Get; #[pallet::constant] - type DecryptWindowMs: Get; // e.g., 3000 (last 3s) + type DecryptWindowMs: Get; - /// Currency for fees (wrapper pays normal tx fee via regular machinery). type Currency: Currency; } @@ -156,48 +125,32 @@ where #[pallet::storage] pub type Epoch = StorageValue<_, u64, ValueQuery>; - /// All encrypted submissions live here until executed or discarded. #[pallet::storage] pub type Submissions = StorageMap< _, Blake2_128Concat, - T::Hash, // id = hash(author, commitment, ciphertext) + T::Hash, Submission, T::Moment, T::Hash>, OptionQuery, >; - /// Mark a submission id as consumed (executed or invalidated). - #[pallet::storage] - pub type Consumed = StorageMap<_, Blake2_128Concat, T::Hash, (), OptionQuery>; - // ----------------- Events & Errors ----------------- #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - /// Next ML‑KEM public key bytes announced. - NextKeyAnnounced { - public_key: Vec, // full Kyber768 public key (1184 bytes) - epoch: u64, - at_ms: u64, - }, - /// Current key rolled to the next (happens on_initialize of new block). - KeyRolled { - public_key: Vec, // full Kyber768 public key (1184 bytes) - epoch: u64 - }, /// Encrypted wrapper accepted. EncryptedSubmitted { id: T::Hash, who: T::AccountId, epoch: u64 }, /// Decrypted call executed. - DecryptedExecuted { id: T::Hash, signer: T::AccountId, actual_weight: Weight }, - /// Decrypted execution rejected (mismatch, overweight, bad sig, etc.). - DecryptedRejected { id: T::Hash, reason: u8 }, + DecryptedExecuted { id: T::Hash, signer: T::AccountId }, + /// Decrypted execution rejected. + DecryptedRejected { id: T::Hash, reason: DispatchErrorWithPostInfo }, } #[pallet::error] pub enum Error { BadEpoch, - AlreadyConsumed, + SubmissionAlreadyExists, MissingSubmission, CommitmentMismatch, SignatureInvalid, @@ -210,19 +163,11 @@ where #[pallet::hooks] impl Hooks> for Pallet { - /// Roll the keys once per block (current <= next). fn on_initialize(_n: BlockNumberFor) -> Weight { if let Some(next) = >::take() { >::put(&next); >::mutate(|e| *e = next.epoch); - - // Emit event with the full public key bytes (convert BoundedVec -> Vec for the event). - Self::deposit_event(Event::::KeyRolled { - public_key: next.public_key.to_vec(), - epoch: next.epoch, - }); } - // small constant cost T::DbWeight::get().reads_writes(1, 2) } } @@ -231,8 +176,6 @@ where #[pallet::call] impl Pallet { - /// Validators announce the *next* ephemeral **ML‑KEM** public key bytes. - /// Origin is restricted to the PoA validator set (Aura authorities). #[pallet::call_index(0)] #[pallet::weight( Weight::from_parts(5_000, 0) @@ -243,34 +186,34 @@ where origin: OriginFor, public_key: BoundedVec>, epoch: u64, - at_ms: u64, ) -> DispatchResult { - // ✅ Only a current Aura validator may call this (signed account ∈ Aura authorities) + // Only a current Aura validator may call this (signed account ∈ Aura authorities) T::AuthorityOrigin::ensure_validator(origin)?; - // Enforce Kyber768 pk length (1184 bytes). - ensure!(public_key.len() == 1184, Error::::BadPublicKeyLen); + const MAX_KYBER768_PK_LENGTH: usize = 1184; + ensure!(public_key.len() == MAX_KYBER768_PK_LENGTH, Error::::BadPublicKeyLen); NextKey::::put(EphemeralPubKey { public_key: public_key.clone(), epoch }); - // Emit full bytes in the event (convert to Vec for simplicity). - Self::deposit_event(Event::NextKeyAnnounced { - public_key: public_key.to_vec(), - epoch, - at_ms, - }); Ok(()) } /// Users submit encrypted wrapper paying the normal fee. - /// `commitment = blake2_256( SCALE( (signer, nonce, mortality, call) ) )` /// - /// Ciphertext format (see module docs): `[u16 kem_len][kem_ct][nonce24][aead_ct]` + /// Commitment semantics: + /// + /// ```text + /// raw_payload = + /// signer (32B) || nonce (u32 LE) || mortality_byte || SCALE(call) + /// commitment = blake2_256(raw_payload) + /// ``` + /// + /// Ciphertext format: `[u16 kem_len][kem_ct][nonce24][aead_ct]` #[pallet::call_index(1)] #[pallet::weight({ let w = Weight::from_parts(ciphertext.len() as u64, 0) - .saturating_add(T::DbWeight::get().reads(1_u64)) // Epoch::get - .saturating_add(T::DbWeight::get().writes(1_u64)); // Submissions::insert + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)); w })] pub fn submit_encrypted( @@ -301,7 +244,7 @@ where }; ensure!( !Submissions::::contains_key(id), - Error::::AlreadyConsumed + Error::::SubmissionAlreadyExists ); Submissions::::insert(id, sub); Self::deposit_event(Event::EncryptedSubmitted { @@ -312,13 +255,7 @@ where Ok(()) } - /// Executed by the block author (unsigned) in the last ~3s. - /// The caller provides the plaintext (signed) and we: - /// - check commitment - /// - check signature - /// - check nonce (and increment) - /// - ensure weight <= max_weight - /// - dispatch the call as the signer (fee-free here; fee already paid by wrapper) + /// Executed by the block author. #[pallet::call_index(2)] #[pallet::weight( Weight::from_parts(10_000, 0) @@ -328,27 +265,30 @@ where pub fn execute_revealed( origin: OriginFor, id: T::Hash, - signer: T::AccountId, // AccountId32 due to Config bound + signer: T::AccountId, nonce: T::Nonce, - mortality: sp_runtime::generic::Era, // same type used in signed extrinsics + mortality: sp_runtime::generic::Era, call: Box<::RuntimeCall>, signature: MultiSignature, ) -> DispatchResultWithPostInfo { ensure_none(origin)?; - ensure!( - !Consumed::::contains_key(id), - Error::::AlreadyConsumed - ); + let Some(sub) = Submissions::::take(id) else { return Err(Error::::MissingSubmission.into()); }; - // 1) Commitment check (encode by-ref to avoid Clone bound on RuntimeCall) - let payload_bytes = (signer.clone(), nonce, mortality.clone(), call.as_ref()).encode(); - let recomputed: T::Hash = T::Hashing::hash_of(&payload_bytes); + let payload_bytes = Self::build_raw_payload_bytes( + &signer, + nonce, + &mortality, + call.as_ref(), + ); + + // 1) Commitment check against on-chain stored commitment. + let recomputed: T::Hash = T::Hashing::hash(&payload_bytes); ensure!(sub.commitment == recomputed, Error::::CommitmentMismatch); - // 2) Signature check over the same payload (domain separated) + // 2) Signature check over the same payload. let genesis = frame_system::Pallet::::block_hash(BlockNumberFor::::zero()); let mut msg = b"mev-shield:v1".to_vec(); msg.extend_from_slice(genesis.as_ref()); @@ -358,12 +298,12 @@ where Error::::SignatureInvalid ); - // 3) Nonce check & bump (we mimic system signed extension behavior) + // 3) Nonce check & bump. let acc = frame_system::Pallet::::account_nonce(&signer); ensure!(acc == nonce, Error::::NonceMismatch); frame_system::Pallet::::inc_account_nonce(&signer); - // 4) Dispatch inner call from signer; enforce max_weight guard + // 4) Dispatch inner call from signer; enforce max_weight guard. let info = call.get_dispatch_info(); let required = info.call_weight.saturating_add(info.extension_weight); @@ -374,24 +314,20 @@ where let origin_signed = frame_system::RawOrigin::Signed(signer.clone()).into(); let res = (*call).dispatch(origin_signed); - // Mark as consumed regardless of outcome (user already paid wrapper fee) - Consumed::::insert(id, ()); - match res { Ok(post) => { let actual = post.actual_weight.unwrap_or(required); Self::deposit_event(Event::DecryptedExecuted { id, signer, - actual_weight: actual, }); Ok(PostDispatchInfo { actual_weight: Some(actual), pays_fee: Pays::No, }) } - Err(_e) => { - Self::deposit_event(Event::DecryptedRejected { id, reason: 1 }); + Err(e) => { + Self::deposit_event(Event::DecryptedRejected { id, reason: e }); Ok(PostDispatchInfo { actual_weight: Some(required), pays_fee: Pays::No, @@ -402,72 +338,65 @@ where } - #[pallet::validate_unsigned] - impl ValidateUnsigned for Pallet { - type Call = Call; - - fn validate_unsigned( - _source: sp_runtime::transaction_validity::TransactionSource, - call: &Self::Call, - ) -> sp_runtime::transaction_validity::TransactionValidity { - use sp_runtime::transaction_validity::{InvalidTransaction, ValidTransaction}; - - match call { - // This is the only unsigned entry point. - Call::execute_revealed { id, .. } => { - // Mark this unsigned tx as valid for the pool & block builder. - // - // IMPORTANT: - // - We *do* want it in the local pool so that the block author - // can include it in the next block. - // - We *do not* want it to be gossiped, otherwise the cleartext - // MEV‑shielded call leaks to the network. - // - // `propagate(false)` keeps it strictly local. - ValidTransaction::with_tag_prefix("mev-shield-exec") - .priority(u64::MAX) // always prefer executes when present - .longevity(1) // only for the very next block - .and_provides(id) // crucial: at least one tag - .propagate(false) // 👈 no gossip / no mempool MEV - .build() - } - - // Any other unsigned call from this pallet is invalid. - _ => InvalidTransaction::Call.into(), - } - } + impl Pallet { + /// Build the raw payload bytes used for both: + /// - `commitment = blake2_256(raw_payload)` + /// - signature message (after domain separation). + /// + /// Layout: + /// signer (32B) || nonce (u32 LE) || mortality_byte || SCALE(call) + fn build_raw_payload_bytes( + signer: &T::AccountId, + nonce: T::Nonce, + mortality: &sp_runtime::generic::Era, + call: &::RuntimeCall, + ) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(signer.as_ref()); + + // We canonicalise nonce to u32 LE for the payload. + let n_u32: u32 = nonce.saturated_into(); + out.extend_from_slice(&n_u32.to_le_bytes()); + + // Simple 1-byte mortality code to match the off-chain layout. + let m_byte: u8 = match mortality { + sp_runtime::generic::Era::Immortal => 0, + _ => 1, + }; + out.push(m_byte); + + // Append SCALE-encoded call. + out.extend(call.encode()); + + out } + } + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + + fn validate_unsigned( + _source: sp_runtime::transaction_validity::TransactionSource, + call: &Self::Call, + ) -> sp_runtime::transaction_validity::TransactionValidity { + use sp_runtime::transaction_validity::{ + InvalidTransaction, + ValidTransaction, + }; + match call { + Call::execute_revealed { id, .. } => { + ValidTransaction::with_tag_prefix("mev-shield-exec") + .priority(u64::MAX) + .longevity(64) // High because of propagate(false) + .and_provides(id) // dedupe by wrapper id + .propagate(false) // CRITICAL: no gossip, stays on author only + .build() + } - // #[pallet::validate_unsigned] - // impl ValidateUnsigned for Pallet { - // type Call = Call; - - // fn validate_unsigned( - // _source: sp_runtime::transaction_validity::TransactionSource, - // call: &Self::Call, - // ) -> sp_runtime::transaction_validity::TransactionValidity { - // use sp_runtime::transaction_validity::{InvalidTransaction, ValidTransaction}; - - // match call { - // // This is the only unsigned entry point. - // Call::execute_revealed { id, .. } => { - // // Mark this unsigned tx as valid for the pool & block builder. - // // - no source check: works for pool, block building, and block import - // // - propagate(true): gossip so *whoever* authors next block sees it - // // - provides(id): lets the pool deduplicate by this id - // ValidTransaction::with_tag_prefix("mev-shield-exec") - // .priority(u64::MAX) // always prefer executes when present - // .longevity(1) // only for the very next block - // .and_provides(id) // crucial: at least one tag - // .propagate(true) // <-- changed from false to true - // .build() - // } - - // // Any other unsigned call from this pallet is invalid. - // _ => InvalidTransaction::Call.into(), - // } - // } - // } + _ => InvalidTransaction::Call.into(), + } + } + } } From 83e040d2881835de59a277e6310051c484adf8c2 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:26:24 -0800 Subject: [PATCH 03/30] fix comments & add freeze_struct --- Cargo.lock | 1 + node/Cargo.toml | 1 + node/src/mev_shield/author.rs | 72 ++++++++++++--------------------- node/src/mev_shield/proposer.rs | 17 +++----- node/src/service.rs | 9 ++--- 5 files changed, 37 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5ff63cb86..b3529480e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8285,6 +8285,7 @@ dependencies = [ "substrate-prometheus-endpoint", "subtensor-custom-rpc", "subtensor-custom-rpc-runtime-api", + "subtensor-macros", "subtensor-runtime-common", "tokio", "x25519-dalek", diff --git a/node/Cargo.toml b/node/Cargo.toml index 4602662ab4..b32c598fbb 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -136,6 +136,7 @@ subtensor-custom-rpc = { workspace = true, features = ["std"] } subtensor-custom-rpc-runtime-api = { workspace = true, features = ["std"] } pallet-subtensor-swap-rpc = { workspace = true, features = ["std"] } pallet-subtensor-swap-runtime-api = { workspace = true, features = ["std"] } +subtensor-macros.workspace = true [build-dependencies] substrate-build-script-utils.workspace = true diff --git a/node/src/mev_shield/author.rs b/node/src/mev_shield/author.rs index f01319b4d1..f165b7fbe1 100644 --- a/node/src/mev_shield/author.rs +++ b/node/src/mev_shield/author.rs @@ -1,7 +1,4 @@ -//! MEV-shield author-side helpers: ML‑KEM-768 ephemeral keys + timed key announcement. - use std::{sync::{Arc, Mutex}, time::Duration}; - use futures::StreamExt; use sc_client_api::HeaderBackend; use sc_transaction_pool_api::TransactionSource; @@ -11,26 +8,26 @@ use sp_runtime::traits::Block as BlockT; use sp_runtime::KeyTypeId; use tokio::time::sleep; use blake2::Blake2b512; - use hkdf::Hkdf; use sha2::Sha256; use chacha20poly1305::{XChaCha20Poly1305, KeyInit, aead::{Aead, Payload}, XNonce}; - -use node_subtensor_runtime as runtime; // alias for easier type access +use node_subtensor_runtime as runtime; use runtime::{RuntimeCall, UncheckedExtrinsic}; - use ml_kem::{MlKem768, KemCore, EncodedSizeUser}; use rand::rngs::OsRng; +use subtensor_macros::freeze_struct; -/// Parameters controlling time windows inside the slot (milliseconds). +/// Parameters controlling time windows inside the slot. +#[freeze_struct("cb816cf709ea285b")] #[derive(Clone)] pub struct TimeParams { - pub slot_ms: u64, // e.g., 12_000 - pub announce_at_ms: u64, // 7_000 - pub decrypt_window_ms: u64 // 3_000 + pub slot_ms: u64, + pub announce_at_ms: u64, + pub decrypt_window_ms: u64 } /// Holds the current/next ML‑KEM keypairs and their 32‑byte fingerprints. +#[freeze_struct("e4778f8957165b64")] #[derive(Clone)] pub struct MevShieldKeys { pub current_sk: Vec, // ML‑KEM secret key bytes (encoded form) @@ -46,7 +43,6 @@ impl MevShieldKeys { pub fn new(epoch: u64) -> Self { let (sk, pk) = MlKem768::generate(&mut OsRng); - // Bring EncodedSizeUser into scope so as_bytes() is available let sk_bytes = sk.as_bytes(); let pk_bytes = pk.as_bytes(); let sk_slice: &[u8] = sk_bytes.as_ref(); @@ -89,6 +85,7 @@ impl MevShieldKeys { } /// Shared context state. +#[freeze_struct("d04f0903285c319d")] #[derive(Clone)] pub struct MevShieldContext { pub keys: Arc>, @@ -96,7 +93,6 @@ pub struct MevShieldContext { } /// Derive AEAD key directly from the 32‑byte ML‑KEM shared secret. -/// This matches the FFI exactly: AEAD key = shared secret bytes. pub fn derive_aead_key(ss: &[u8]) -> [u8; 32] { let mut key = [0u8; 32]; let n = ss.len().min(32); @@ -119,8 +115,7 @@ const AURA_KEY_TYPE: KeyTypeId = KeyTypeId(*b"aura"); /// Start background tasks: /// - per-slot ML‑KEM key rotation -/// - at ~announce_at_ms announce the *next* key **bytes** on chain, -/// signed by the local block author (Aura authority), not an env var. +/// - at ~announce_at_ms announce the next key bytes on chain, pub fn spawn_author_tasks( task_spawner: &sc_service::SpawnTaskHandle, client: std::sync::Arc, @@ -131,14 +126,12 @@ pub fn spawn_author_tasks( ) -> MevShieldContext where B: sp_runtime::traits::Block, - // Need block import notifications and headers. C: sc_client_api::HeaderBackend + sc_client_api::BlockchainEvents + Send + Sync + 'static, Pool: sc_transaction_pool_api::TransactionPool + Send + Sync + 'static, - // We submit an OpaqueExtrinsic into the pool. B::Extrinsic: From, { let ctx = MevShieldContext { @@ -146,8 +139,6 @@ where timing: timing.clone(), }; - // Pick the local Aura authority key that actually authors blocks on this node. - // We just grab the first Aura sr25519 key in the keystore. let aura_keys: Vec = keystore.sr25519_public_keys(AURA_KEY_TYPE); let local_aura_pub: Option = aura_keys.get(0).cloned(); @@ -155,20 +146,19 @@ where log::warn!( target: "mev-shield", "spawn_author_tasks: no local Aura sr25519 key in keystore; \ - this node will NOT announce MEV‑Shield keys" + this node will NOT announce MEV-Shield keys" ); return ctx; } let local_aura_pub = local_aura_pub.expect("checked is_some; qed"); - // Clone handles for the async task. let ctx_clone = ctx.clone(); let client_clone = client.clone(); let pool_clone = pool.clone(); let keystore_clone = keystore.clone(); - // Slot tick / key-announce loop (roll at end of slot). + // Slot tick / key-announce loop. task_spawner.spawn( "mev-shield-keys-and-announce", None, @@ -180,7 +170,7 @@ where let mut local_nonce: u32 = 0; while let Some(notif) = import_stream.next().await { - // ✅ Only act on blocks that *this node* authored. + // ✅ Only act on blocks that this node authored. if notif.origin != BlockOrigin::Own { continue; } @@ -200,14 +190,13 @@ where // Wait until the announce window in this slot. tokio::time::sleep(std::time::Duration::from_millis(timing.announce_at_ms)).await; - // Read the *next* key we intend to use for the following epoch. + // Read the next key we intend to use for the following epoch. let (next_pk, next_epoch) = { let k = ctx_clone.keys.lock().unwrap(); (k.next_pk.clone(), k.epoch.saturating_add(1)) }; - // Submit announce_next_key once, signed with the local Aura authority - // (the same identity that authors this block). + // Submit announce_next_key once, signed with the local Aura authority that authors this block match submit_announce_extrinsic::( client_clone.clone(), pool_clone.clone(), @@ -254,17 +243,17 @@ where } } - // Sleep the remainder of the slot (includes decrypt window). + // Sleep the remainder of the slot. let tail = timing.slot_ms.saturating_sub(timing.announce_at_ms); tokio::time::sleep(std::time::Duration::from_millis(tail)).await; - // Roll keys for the next slot / epoch. + // Roll keys for the next epoch. { let mut k = ctx_clone.keys.lock().unwrap(); k.roll_for_next_slot(); log::info!( target: "mev-shield", - "Rolled ML‑KEM key at slot boundary (local author): new epoch={}", + "Rolled ML-KEM key at slot boundary (local author): new epoch={}", k.epoch ); } @@ -276,25 +265,21 @@ where } -/// Build & submit the signed `announce_next_key` extrinsic OFF-CHAIN, -/// using the local Aura authority key stored in the keystore. +/// Build & submit the signed `announce_next_key` extrinsic OFF-CHAIN pub async fn submit_announce_extrinsic( client: std::sync::Arc, pool: std::sync::Arc, keystore: sp_keystore::KeystorePtr, - aura_pub: sp_core::sr25519::Public, // local Aura authority public key - next_public_key: Vec, // full ML‑KEM pubkey bytes (expected 1184B) + aura_pub: sp_core::sr25519::Public, + next_public_key: Vec, epoch: u64, - nonce: u32, // nonce for CheckNonce extension + nonce: u32, ) -> anyhow::Result<()> where B: sp_runtime::traits::Block, - // Only need best/genesis from the client C: sc_client_api::HeaderBackend + Send + Sync + 'static, Pool: sc_transaction_pool_api::TransactionPool + Send + Sync + 'static, - // Convert to the pool's extrinsic type B::Extrinsic: From, - // Allow generic conversion of block hash to bytes for H256 B::Hash: AsRef<[u8]>, { use node_subtensor_runtime as runtime; @@ -308,7 +293,7 @@ where }; use sp_runtime::codec::Encode; - // Helper: map a generic Block hash to H256 without requiring Into + // Helper: map a Block hash to H256 fn to_h256>(h: H) -> H256 { let bytes = h.as_ref(); let mut out = [0u8; 32]; @@ -317,13 +302,12 @@ where H256(out) } - // 0) Bounded public key (max 2 KiB) as required by the pallet. type MaxPk = ConstU32<2048>; let public_key: BoundedVec = BoundedVec::try_from(next_public_key) .map_err(|_| anyhow::anyhow!("public key too long (>2048 bytes)"))?; - // 1) The runtime call carrying **full public key bytes**. + // 1) The runtime call carrying public key bytes. let call = RuntimeCall::MevShield( pallet_mev_shield::Call::announce_next_key { public_key, @@ -331,7 +315,6 @@ where } ); - // 2) Extensions tuple (must match your runtime's `type TransactionExtensions`). type Extra = runtime::TransactionExtensions; let extra: Extra = ( frame_system::CheckNonZeroSender::::new(), @@ -339,7 +322,6 @@ where frame_system::CheckTxVersion::::new(), frame_system::CheckGenesis::::new(), frame_system::CheckEra::::from(Era::Immortal), - // Use the passed-in nonce here: node_subtensor_runtime::check_nonce::CheckNonce::::from(nonce).into(), frame_system::CheckWeight::::new(), node_subtensor_runtime::transaction_payment_wrapper::ChargeTransactionPaymentWrapper::::new( @@ -350,7 +332,6 @@ where frame_metadata_hash_extension::CheckMetadataHash::::new(false), ); - // 3) Implicit (AdditionalSigned) values. type Implicit = >::Implicit; let info = client.info(); @@ -370,7 +351,7 @@ where None, // CheckMetadataHash (disabled) ); - // 4) Build the exact signable payload. + // Build the exact signable payload. let payload: SignedPayload = SignedPayload::from_raw( call.clone(), extra.clone(), @@ -379,7 +360,7 @@ where let raw_payload = payload.encode(); - // Sign with the local Aura key from the keystore (synchronous `Keystore` API). + // Sign with the local Aura key. let sig_opt = keystore .sr25519_sign(AURA_KEY_TYPE, &aura_pub, &raw_payload) .map_err(|e| anyhow::anyhow!("keystore sr25519_sign error: {e:?}"))?; @@ -388,7 +369,6 @@ where let signature: MultiSignature = sig.into(); - // 5) Assemble and submit (also log the extrinsic hash for observability). let who: AccountId32 = aura_pub.into(); let address = sp_runtime::MultiAddress::Id(who); diff --git a/node/src/mev_shield/proposer.rs b/node/src/mev_shield/proposer.rs index 4bc181906a..76f5e433d0 100644 --- a/node/src/mev_shield/proposer.rs +++ b/node/src/mev_shield/proposer.rs @@ -1,11 +1,8 @@ -//! Last-3s reveal injector: decrypt buffered wrappers and submit unsigned `execute_revealed`. - use std::{ collections::HashMap, sync::{Arc, Mutex}, time::Duration, }; - use codec::{Decode, Encode}; use futures::StreamExt; use sc_service::SpawnTaskHandle; @@ -17,14 +14,12 @@ use sp_runtime::{ MultiAddress, MultiSignature, OpaqueExtrinsic, + AccountId32, }; use tokio::time::sleep; - use node_subtensor_runtime as runtime; use runtime::RuntimeCall; - use super::author::{aead_decrypt, derive_aead_key, MevShieldContext}; - use ml_kem::{Ciphertext, Encoded, EncodedSizeUser, MlKem768, MlKem768Params}; use ml_kem::kem::{Decapsulate, DecapsulationKey}; @@ -34,9 +29,9 @@ struct WrapperBuffer { by_id: HashMap< H256, ( - Vec, // ciphertext blob - u64, // key_epoch - sp_runtime::AccountId32, // wrapper author (fee payer) + Vec, // ciphertext blob + u64, // key_epoch + AccountId32, // wrapper author ), >, } @@ -46,7 +41,7 @@ impl WrapperBuffer { &mut self, id: H256, key_epoch: u64, - author: sp_runtime::AccountId32, + author: AccountId32, ciphertext: Vec, ) { self.by_id.insert(id, (ciphertext, key_epoch, author)); @@ -99,12 +94,10 @@ impl WrapperBuffer { } } -/// Start a background worker that: /// Start a background worker that: /// • watches imported blocks and captures `MevShield::submit_encrypted` /// • buffers those wrappers, /// • ~last `decrypt_window_ms` of the slot: decrypt & submit unsigned `execute_revealed` -/// **only for wrappers whose `key_epoch` equals the current ML‑KEM epoch.** pub fn spawn_revealer( task_spawner: &SpawnTaskHandle, client: Arc, diff --git a/node/src/service.rs b/node/src/service.rs index d492c37d1e..d1f107928f 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -37,7 +37,6 @@ use crate::ethereum::{ }; use sp_core::twox_128; use sc_client_api::StorageKey; -use node_subtensor_runtime::opaque::BlockId; use sc_client_api::HeaderBackend; use sc_client_api::StorageProvider; use codec::Decode; @@ -542,7 +541,7 @@ where .await; // ==== MEV-SHIELD HOOKS ==== - let mut mev_timing: Option = None; + let mut mev_timing: Option = None; if role.is_authority() { let slot_duration_ms: u64 = consensus_mechanism @@ -550,7 +549,7 @@ where .as_millis() as u64; // Time windows (7s announce / last 3s decrypt). - let timing = crate::mev_shield::author::TimeParams { + let timing = author::TimeParams { slot_ms: slot_duration_ms, announce_at_ms: 7_000, decrypt_window_ms: 3_000, @@ -572,7 +571,7 @@ where }; // Start author-side tasks with the epoch. - let mev_ctx = crate::mev_shield::author::spawn_author_tasks::( + let mev_ctx = author::spawn_author_tasks::( &task_manager.spawn_handle(), client.clone(), transaction_pool.clone(), @@ -582,7 +581,7 @@ where ); // Start last-3s revealer (decrypt -> execute_revealed). - crate::mev_shield::proposer::spawn_revealer::( + proposer::spawn_revealer::( &task_manager.spawn_handle(), client.clone(), transaction_pool.clone(), From a6a381780cadd29371f7b683ed33542073a1171b Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:33:45 -0800 Subject: [PATCH 04/30] fix imports --- node/src/mev_shield/author.rs | 17 ++++------------- node/src/mev_shield/proposer.rs | 11 +++-------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/node/src/mev_shield/author.rs b/node/src/mev_shield/author.rs index f165b7fbe1..4b549d5c9e 100644 --- a/node/src/mev_shield/author.rs +++ b/node/src/mev_shield/author.rs @@ -1,18 +1,9 @@ -use std::{sync::{Arc, Mutex}, time::Duration}; -use futures::StreamExt; -use sc_client_api::HeaderBackend; -use sc_transaction_pool_api::TransactionSource; -use sp_api::ProvideRuntimeApi; -use sp_core::{sr25519, blake2_256}; -use sp_runtime::traits::Block as BlockT; +use std::{sync::{Arc, Mutex}}; +use sp_core::blake2_256; use sp_runtime::KeyTypeId; use tokio::time::sleep; -use blake2::Blake2b512; -use hkdf::Hkdf; -use sha2::Sha256; use chacha20poly1305::{XChaCha20Poly1305, KeyInit, aead::{Aead, Payload}, XNonce}; use node_subtensor_runtime as runtime; -use runtime::{RuntimeCall, UncheckedExtrinsic}; use ml_kem::{MlKem768, KemCore, EncodedSizeUser}; use rand::rngs::OsRng; use subtensor_macros::freeze_struct; @@ -188,7 +179,7 @@ where ); // Wait until the announce window in this slot. - tokio::time::sleep(std::time::Duration::from_millis(timing.announce_at_ms)).await; + sleep(std::time::Duration::from_millis(timing.announce_at_ms)).await; // Read the next key we intend to use for the following epoch. let (next_pk, next_epoch) = { @@ -245,7 +236,7 @@ where // Sleep the remainder of the slot. let tail = timing.slot_ms.saturating_sub(timing.announce_at_ms); - tokio::time::sleep(std::time::Duration::from_millis(tail)).await; + sleep(std::time::Duration::from_millis(tail)).await; // Roll keys for the next epoch. { diff --git a/node/src/mev_shield/proposer.rs b/node/src/mev_shield/proposer.rs index 76f5e433d0..79e335fa90 100644 --- a/node/src/mev_shield/proposer.rs +++ b/node/src/mev_shield/proposer.rs @@ -3,23 +3,18 @@ use std::{ sync::{Arc, Mutex}, time::Duration, }; -use codec::{Decode, Encode}; use futures::StreamExt; use sc_service::SpawnTaskHandle; use sc_transaction_pool_api::{TransactionPool, TransactionSource}; use sp_core::H256; use sp_runtime::{ generic::Era, - traits::Block as BlockT, - MultiAddress, MultiSignature, OpaqueExtrinsic, AccountId32, }; use tokio::time::sleep; -use node_subtensor_runtime as runtime; -use runtime::RuntimeCall; -use super::author::{aead_decrypt, derive_aead_key, MevShieldContext}; +use super::author::MevShieldContext; use ml_kem::{Ciphertext, Encoded, EncodedSizeUser, MlKem768, MlKem768Params}; use ml_kem::kem::{Decapsulate, DecapsulationKey}; @@ -239,7 +234,7 @@ pub fn spawn_revealer( "revealer: sleeping {} ms before decrypt window (slot_ms={}, decrypt_window_ms={})", tail, ctx.timing.slot_ms, ctx.timing.decrypt_window_ms ); - tokio::time::sleep(Duration::from_millis(tail)).await; + sleep(Duration::from_millis(tail)).await; // Snapshot the *current* ML‑KEM secret and epoch. let (curr_sk_bytes, curr_epoch, curr_pk_len, next_pk_len, sk_hash) = { @@ -512,7 +507,7 @@ pub fn spawn_revealer( } } - tokio::time::sleep(Duration::from_millis(ctx.timing.decrypt_window_ms)).await; + sleep(Duration::from_millis(ctx.timing.decrypt_window_ms)).await; } }, ); From 0fca3dbb64475ff7e29691a6b59728ecb16ce014 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:17:16 -0800 Subject: [PATCH 05/30] rename pallet --- Cargo.lock | 44 +++++++++++------------ Cargo.toml | 2 +- node/Cargo.toml | 2 +- node/src/mev_shield/author.rs | 20 +++++------ node/src/mev_shield/proposer.rs | 8 ++--- pallets/{mev-shield => shield}/Cargo.toml | 2 +- pallets/{mev-shield => shield}/src/lib.rs | 0 runtime/Cargo.toml | 2 +- runtime/src/lib.rs | 24 ++++++------- 9 files changed, 52 insertions(+), 52 deletions(-) rename pallets/{mev-shield => shield}/Cargo.toml (98%) rename pallets/{mev-shield => shield}/src/lib.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index b3529480e4..a70fd85b18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8222,7 +8222,7 @@ dependencies = [ "num-traits", "pallet-commitments", "pallet-drand", - "pallet-mev-shield", + "pallet-shield", "pallet-subtensor", "pallet-subtensor-swap-rpc", "pallet-subtensor-swap-runtime-api", @@ -8337,7 +8337,6 @@ dependencies = [ "pallet-grandpa", "pallet-hotfix-sufficients", "pallet-insecure-randomness-collective-flip", - "pallet-mev-shield", "pallet-multisig", "pallet-nomination-pools", "pallet-nomination-pools-runtime-api", @@ -8347,6 +8346,7 @@ dependencies = [ "pallet-safe-mode", "pallet-scheduler", "pallet-session", + "pallet-shield", "pallet-staking", "pallet-staking-reward-curve", "pallet-staking-reward-fn", @@ -10002,26 +10002,6 @@ dependencies = [ "sp-std", ] -[[package]] -name = "pallet-mev-shield" -version = "0.0.1" -dependencies = [ - "frame-benchmarking", - "frame-support", - "frame-system", - "pallet-aura", - "pallet-timestamp", - "parity-scale-codec", - "scale-info", - "sp-consensus-aura", - "sp-core", - "sp-io", - "sp-runtime", - "sp-std", - "sp-weights", - "subtensor-macros", -] - [[package]] name = "pallet-migrations" version = "11.0.0" @@ -10587,6 +10567,26 @@ dependencies = [ "sp-session", ] +[[package]] +name = "pallet-shield" +version = "0.0.1" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-aura", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-consensus-aura", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "sp-weights", + "subtensor-macros", +] + [[package]] name = "pallet-skip-feeless-payment" version = "16.0.0" diff --git a/Cargo.toml b/Cargo.toml index e003665ccb..b70d09681b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -285,7 +285,7 @@ sha2 = { version = "0.10.8", default-features = false } rand_chacha = { version = "0.3.1", default-features = false } tle = { git = "https://github.com/ideal-lab5/timelock", rev = "5416406cfd32799e31e1795393d4916894de4468", default-features = false } -pallet-mev-shield = { path = "pallets/mev-shield", default-features = false } +pallet-shield = { path = "pallets/shield", default-features = false } ml-kem = { version = "0.2.0", default-features = true } # Primitives diff --git a/node/Cargo.toml b/node/Cargo.toml index b32c598fbb..fbeda4f536 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -115,7 +115,7 @@ fp-consensus.workspace = true num-traits = { workspace = true, features = ["std"] } # Mev Shield -pallet-mev-shield.workspace = true +pallet-shield.workspace = true tokio = { version = "1.38", features = ["time"] } x25519-dalek = "2" hkdf = "0.12" diff --git a/node/src/mev_shield/author.rs b/node/src/mev_shield/author.rs index 4b549d5c9e..e543b6fca4 100644 --- a/node/src/mev_shield/author.rs +++ b/node/src/mev_shield/author.rs @@ -18,9 +18,9 @@ pub struct TimeParams { } /// Holds the current/next ML‑KEM keypairs and their 32‑byte fingerprints. -#[freeze_struct("e4778f8957165b64")] +#[freeze_struct("3a83c10877ec1f24")] #[derive(Clone)] -pub struct MevShieldKeys { +pub struct ShieldKeys { pub current_sk: Vec, // ML‑KEM secret key bytes (encoded form) pub current_pk: Vec, // ML‑KEM public key bytes (encoded form) pub current_fp: [u8; 32], // blake2_256(pk) @@ -30,7 +30,7 @@ pub struct MevShieldKeys { pub epoch: u64, } -impl MevShieldKeys { +impl ShieldKeys { pub fn new(epoch: u64) -> Self { let (sk, pk) = MlKem768::generate(&mut OsRng); @@ -76,10 +76,10 @@ impl MevShieldKeys { } /// Shared context state. -#[freeze_struct("d04f0903285c319d")] +#[freeze_struct("62af7d26cf7c1271")] #[derive(Clone)] -pub struct MevShieldContext { - pub keys: Arc>, +pub struct ShieldContext { + pub keys: Arc>, pub timing: TimeParams, } @@ -114,7 +114,7 @@ pub fn spawn_author_tasks( keystore: sp_keystore::KeystorePtr, initial_epoch: u64, timing: TimeParams, -) -> MevShieldContext +) -> ShieldContext where B: sp_runtime::traits::Block, C: sc_client_api::HeaderBackend @@ -125,8 +125,8 @@ where Pool: sc_transaction_pool_api::TransactionPool + Send + Sync + 'static, B::Extrinsic: From, { - let ctx = MevShieldContext { - keys: std::sync::Arc::new(std::sync::Mutex::new(MevShieldKeys::new(initial_epoch))), + let ctx = ShieldContext { + keys: std::sync::Arc::new(std::sync::Mutex::new(ShieldKeys::new(initial_epoch))), timing: timing.clone(), }; @@ -300,7 +300,7 @@ where // 1) The runtime call carrying public key bytes. let call = RuntimeCall::MevShield( - pallet_mev_shield::Call::announce_next_key { + pallet_shield::Call::announce_next_key { public_key, epoch, } diff --git a/node/src/mev_shield/proposer.rs b/node/src/mev_shield/proposer.rs index 79e335fa90..39d0f4fe93 100644 --- a/node/src/mev_shield/proposer.rs +++ b/node/src/mev_shield/proposer.rs @@ -14,7 +14,7 @@ use sp_runtime::{ AccountId32, }; use tokio::time::sleep; -use super::author::MevShieldContext; +use super::author::ShieldContext; use ml_kem::{Ciphertext, Encoded, EncodedSizeUser, MlKem768, MlKem768Params}; use ml_kem::kem::{Decapsulate, DecapsulationKey}; @@ -97,7 +97,7 @@ pub fn spawn_revealer( task_spawner: &SpawnTaskHandle, client: Arc, pool: Arc, - ctx: MevShieldContext, + ctx: ShieldContext, ) where B: sp_runtime::traits::Block, C: sc_client_api::HeaderBackend @@ -181,7 +181,7 @@ pub fn spawn_revealer( }; if let node_subtensor_runtime::RuntimeCall::MevShield( - pallet_mev_shield::Call::submit_encrypted { + pallet_shield::Call::submit_encrypted { key_epoch, commitment, ciphertext, @@ -450,7 +450,7 @@ pub fn spawn_revealer( ); let reveal = node_subtensor_runtime::RuntimeCall::MevShield( - pallet_mev_shield::Call::execute_revealed { + pallet_shield::Call::execute_revealed { id, signer: signer.clone(), nonce: account_nonce, diff --git a/pallets/mev-shield/Cargo.toml b/pallets/shield/Cargo.toml similarity index 98% rename from pallets/mev-shield/Cargo.toml rename to pallets/shield/Cargo.toml index b22a4b174c..fcbb827871 100644 --- a/pallets/mev-shield/Cargo.toml +++ b/pallets/shield/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "pallet-mev-shield" +name = "pallet-shield" description = "FRAME pallet for opt-in, per-block ephemeral-key encrypted transactions, MEV-shielded." authors = ["Subtensor Contributors "] version = "0.0.1" diff --git a/pallets/mev-shield/src/lib.rs b/pallets/shield/src/lib.rs similarity index 100% rename from pallets/mev-shield/src/lib.rs rename to pallets/shield/src/lib.rs diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index bd687e4427..7293841a39 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -150,7 +150,7 @@ ark-serialize = { workspace = true, features = ["derive"] } pallet-crowdloan.workspace = true # Mev Shield -pallet-mev-shield.workspace = true +pallet-shield.workspace = true ethereum.workspace = true diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5d95458a6e..58b69ffcb0 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -28,7 +28,7 @@ use frame_support::{ use frame_system::{EnsureRoot, EnsureRootWithSuccess, EnsureSigned}; use pallet_commitments::{CanCommit, OnMetadataCommitment}; use pallet_grandpa::{AuthorityId as GrandpaId, fg_primitives}; -pub use pallet_mev_shield; +pub use pallet_shield; use pallet_registry::CanRegisterIdentity; use pallet_subtensor::rpc_info::{ delegate_info::DelegateInfo, @@ -120,25 +120,25 @@ impl frame_system::offchain::SigningTypes for Runtime { type Signature = Signature; } -impl pallet_mev_shield::Config for Runtime { +impl pallet_shield::Config for Runtime { type RuntimeCall = RuntimeCall; - type SlotMs = MevShieldSlotMs; - type AnnounceAtMs = MevShieldAnnounceAtMs; - type GraceMs = MevShieldGraceMs; - type DecryptWindowMs = MevShieldDecryptWindowMs; + type SlotMs = ShieldSlotMs; + type AnnounceAtMs = ShieldAnnounceAtMs; + type GraceMs = ShieldGraceMs; + type DecryptWindowMs = ShieldDecryptWindowMs; type Currency = Balances; - type AuthorityOrigin = pallet_mev_shield::EnsureAuraAuthority; + type AuthorityOrigin = pallet_shield::EnsureAuraAuthority; } parameter_types! { /// Milliseconds per slot; use the chain’s configured slot duration. - pub const MevShieldSlotMs: u64 = SLOT_DURATION; + pub const ShieldSlotMs: u64 = SLOT_DURATION; /// Emit the *next* ephemeral public key event at 7s. - pub const MevShieldAnnounceAtMs: u64 = 7_000; + pub const ShieldAnnounceAtMs: u64 = 7_000; /// Old key remains accepted until 9s (2s grace). - pub const MevShieldGraceMs: u64 = 2_000; + pub const ShieldGraceMs: u64 = 2_000; /// Last 3s of the slot reserved for decrypt+execute. - pub const MevShieldDecryptWindowMs: u64 = 3_000; + pub const ShieldDecryptWindowMs: u64 = 3_000; } impl frame_system::offchain::CreateTransactionBase for Runtime @@ -1591,7 +1591,7 @@ construct_runtime!( Crowdloan: pallet_crowdloan = 27, Swap: pallet_subtensor_swap = 28, Contracts: pallet_contracts = 29, - MevShield: pallet_mev_shield = 30, + MevShield: pallet_shield = 30, } ); From 0f711d09412f06ea4c9a5970dd2f063c6e47dcfe Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:37:44 -0800 Subject: [PATCH 06/30] use saturating math --- node/src/mev_shield/author.rs | 99 ++++-- node/src/mev_shield/proposer.rs | 551 ++++++++++++++++++++++++-------- 2 files changed, 494 insertions(+), 156 deletions(-) diff --git a/node/src/mev_shield/author.rs b/node/src/mev_shield/author.rs index e543b6fca4..b99d737fae 100644 --- a/node/src/mev_shield/author.rs +++ b/node/src/mev_shield/author.rs @@ -131,23 +131,23 @@ where }; let aura_keys: Vec = keystore.sr25519_public_keys(AURA_KEY_TYPE); - let local_aura_pub: Option = aura_keys.get(0).cloned(); - - if local_aura_pub.is_none() { - log::warn!( - target: "mev-shield", - "spawn_author_tasks: no local Aura sr25519 key in keystore; \ - this node will NOT announce MEV-Shield keys" - ); - return ctx; - } - let local_aura_pub = local_aura_pub.expect("checked is_some; qed"); + let local_aura_pub = match aura_keys.get(0).cloned() { + Some(k) => k, + None => { + log::warn!( + target: "mev-shield", + "spawn_author_tasks: no local Aura sr25519 key in keystore; \ + this node will NOT announce MEV-Shield keys" + ); + return ctx; + } + }; - let ctx_clone = ctx.clone(); - let client_clone = client.clone(); - let pool_clone = pool.clone(); - let keystore_clone = keystore.clone(); + let ctx_clone = ctx.clone(); + let client_clone = client.clone(); + let pool_clone = pool.clone(); + let keystore_clone = keystore.clone(); // Slot tick / key-announce loop. task_spawner.spawn( @@ -167,9 +167,16 @@ where } // This block is the start of a slot for which we are the author. - let (epoch_now, curr_pk_len, next_pk_len) = { - let k = ctx_clone.keys.lock().unwrap(); - (k.epoch, k.current_pk.len(), k.next_pk.len()) + let (epoch_now, curr_pk_len, next_pk_len) = match ctx_clone.keys.lock() { + Ok(k) => (k.epoch, k.current_pk.len(), k.next_pk.len()), + Err(e) => { + log::warn!( + target: "mev-shield", + "spawn_author_tasks: failed to lock ShieldKeys (poisoned?): {:?}", + e + ); + continue; + } }; log::info!( @@ -182,9 +189,16 @@ where sleep(std::time::Duration::from_millis(timing.announce_at_ms)).await; // Read the next key we intend to use for the following epoch. - let (next_pk, next_epoch) = { - let k = ctx_clone.keys.lock().unwrap(); - (k.next_pk.clone(), k.epoch.saturating_add(1)) + let (next_pk, next_epoch) = match ctx_clone.keys.lock() { + Ok(k) => (k.next_pk.clone(), k.epoch.saturating_add(1)), + Err(e) => { + log::warn!( + target: "mev-shield", + "spawn_author_tasks: failed to lock ShieldKeys for next_pk: {:?}", + e + ); + continue; + } }; // Submit announce_next_key once, signed with the local Aura authority that authors this block @@ -239,14 +253,22 @@ where sleep(std::time::Duration::from_millis(tail)).await; // Roll keys for the next epoch. - { - let mut k = ctx_clone.keys.lock().unwrap(); - k.roll_for_next_slot(); - log::info!( - target: "mev-shield", - "Rolled ML-KEM key at slot boundary (local author): new epoch={}", - k.epoch - ); + match ctx_clone.keys.lock() { + Ok(mut k) => { + k.roll_for_next_slot(); + log::info!( + target: "mev-shield", + "Rolled ML-KEM key at slot boundary (local author): new epoch={}", + k.epoch + ); + } + Err(e) => { + log::warn!( + target: "mev-shield", + "spawn_author_tasks: failed to lock ShieldKeys for roll_for_next_slot: {:?}", + e + ); + } } } } @@ -288,9 +310,24 @@ where fn to_h256>(h: H) -> H256 { let bytes = h.as_ref(); let mut out = [0u8; 32]; + + if bytes.is_empty() { + return H256(out); + } + let n = bytes.len().min(32); - out[32 - n..].copy_from_slice(&bytes[bytes.len() - n..]); - H256(out) + let src_start = bytes.len().saturating_sub(n); + let dst_start = 32usize.saturating_sub(n); + + if let (Some(dst), Some(src)) = + (out.get_mut(dst_start..32), bytes.get(src_start..src_start + n)) + { + dst.copy_from_slice(src); + H256(out) + } else { + // Extremely unlikely; fall back to zeroed H256 if indices are somehow invalid. + H256([0u8; 32]) + } } type MaxPk = ConstU32<2048>; diff --git a/node/src/mev_shield/proposer.rs b/node/src/mev_shield/proposer.rs index 39d0f4fe93..9bb3f309b7 100644 --- a/node/src/mev_shield/proposer.rs +++ b/node/src/mev_shield/proposer.rs @@ -24,8 +24,8 @@ struct WrapperBuffer { by_id: HashMap< H256, ( - Vec, // ciphertext blob - u64, // key_epoch + Vec, // ciphertext blob + u64, // key_epoch AccountId32, // wrapper author ), >, @@ -50,8 +50,8 @@ impl WrapperBuffer { epoch: u64, ) -> Vec<(H256, u64, sp_runtime::AccountId32, Vec)> { let mut ready = Vec::new(); - let mut kept_future = 0usize; - let mut dropped_past = 0usize; + let mut kept_future: usize = 0; + let mut dropped_past: usize = 0; self.by_id.retain(|id, (ct, key_epoch, who)| { if *key_epoch == epoch { @@ -60,11 +60,11 @@ impl WrapperBuffer { false } else if *key_epoch > epoch { // Not yet reveal time; keep for future epochs. - kept_future += 1; + kept_future = kept_future.saturating_add(1); true } else { // key_epoch < epoch => stale / missed reveal window; drop. - dropped_past += 1; + dropped_past = dropped_past.saturating_add(1); log::info!( target: "mev-shield", "revealer: dropping stale wrapper id=0x{} key_epoch={} < curr_epoch={}", @@ -130,35 +130,44 @@ pub fn spawn_revealer( while let Some(notif) = import_stream.next().await { let at_hash = notif.hash; - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", "imported block hash={:?} origin={:?}", at_hash, notif.origin ); match client.block_body(at_hash) { Ok(Some(body)) => { - log::info!(target: "mev-shield", - " block has {} extrinsics", body.len() + log::info!( + target: "mev-shield", + " block has {} extrinsics", + body.len() ); for (idx, opaque_xt) in body.into_iter().enumerate() { let encoded = opaque_xt.encode(); - log::info!(target: "mev-shield", - " [xt #{idx}] opaque len={} bytes", encoded.len() + log::info!( + target: "mev-shield", + " [xt #{idx}] opaque len={} bytes", + encoded.len() ); let uxt: RUnchecked = match RUnchecked::decode(&mut &encoded[..]) { Ok(u) => u, Err(e) => { - log::info!(target: "mev-shield", - " [xt #{idx}] failed to decode UncheckedExtrinsic: {:?}", e + log::info!( + target: "mev-shield", + " [xt #{idx}] failed to decode UncheckedExtrinsic: {:?}", + e ); continue; } }; - log::info!(target: "mev-shield", - " [xt #{idx}] decoded call: {:?}", &uxt.0.function + log::info!( + target: "mev-shield", + " [xt #{idx}] decoded call: {:?}", + &uxt.0.function ); let author_opt: Option = @@ -173,8 +182,10 @@ pub fn spawn_revealer( } _ => None, }; + let Some(author) = author_opt else { - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " [xt #{idx}] not a Signed(AccountId32) extrinsic; skipping" ); continue; @@ -192,22 +203,42 @@ pub fn spawn_revealer( let payload = (author.clone(), *commitment, ciphertext).encode(); let id = H256(sp_core::hashing::blake2_256(&payload)); - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " [xt #{idx}] buffered submit_encrypted: id=0x{}, key_epoch={}, author={}, ct_len={}, commitment={:?}", - hex::encode(id.as_bytes()), key_epoch, author, ciphertext.len(), commitment + hex::encode(id.as_bytes()), + key_epoch, + author, + ciphertext.len(), + commitment ); - buffer.lock().unwrap().upsert( - id, *key_epoch, author, ciphertext.to_vec(), - ); + if let Ok(mut buf) = buffer.lock() { + buf.upsert( + id, + *key_epoch, + author, + ciphertext.to_vec(), + ); + } else { + log::warn!( + target: "mev-shield", + " [xt #{idx}] failed to lock WrapperBuffer; dropping wrapper" + ); + } } } } - Ok(None) => log::info!(target: "mev-shield", - " block_body returned None for hash={:?}", at_hash + Ok(None) => log::info!( + target: "mev-shield", + " block_body returned None for hash={:?}", + at_hash ), - Err(e) => log::info!(target: "mev-shield", - " block_body error for hash={:?}: {:?}", at_hash, e + Err(e) => log::info!( + target: "mev-shield", + " block_body error for hash={:?}: {:?}", + at_hash, + e ), } } @@ -230,72 +261,181 @@ pub fn spawn_revealer( loop { let tail = ctx.timing.slot_ms.saturating_sub(ctx.timing.decrypt_window_ms); - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", "revealer: sleeping {} ms before decrypt window (slot_ms={}, decrypt_window_ms={})", - tail, ctx.timing.slot_ms, ctx.timing.decrypt_window_ms + tail, + ctx.timing.slot_ms, + ctx.timing.decrypt_window_ms ); sleep(Duration::from_millis(tail)).await; - // Snapshot the *current* ML‑KEM secret and epoch. - let (curr_sk_bytes, curr_epoch, curr_pk_len, next_pk_len, sk_hash) = { - let k = ctx.keys.lock().unwrap(); - let sk_hash = sp_core::hashing::blake2_256(&k.current_sk); - ( - k.current_sk.clone(), - k.epoch, - k.current_pk.len(), - k.next_pk.len(), - sk_hash, - ) + // Snapshot the current ML‑KEM secret and epoch + let snapshot_opt = { + match ctx.keys.lock() { + Ok(k) => { + let sk_hash = sp_core::hashing::blake2_256(&k.current_sk); + Some(( + k.current_sk.clone(), + k.epoch, + k.current_pk.len(), + k.next_pk.len(), + sk_hash, + )) + } + Err(e) => { + log::warn!( + target: "mev-shield", + "revealer: failed to lock ShieldKeys (poisoned?): {:?}", + e + ); + None + } + } }; - log::info!(target: "mev-shield", + let (curr_sk_bytes, curr_epoch, curr_pk_len, next_pk_len, sk_hash) = + match snapshot_opt { + Some(v) => v, + None => { + // Skip this decrypt window entirely, without holding any guard. + sleep(Duration::from_millis(ctx.timing.decrypt_window_ms)).await; + continue; + } + }; + + log::info!( + target: "mev-shield", "revealer: decrypt window start. epoch={} sk_len={} sk_hash=0x{} curr_pk_len={} next_pk_len={}", - curr_epoch, curr_sk_bytes.len(), hex::encode(sk_hash), curr_pk_len, next_pk_len + curr_epoch, + curr_sk_bytes.len(), + hex::encode(sk_hash), + curr_pk_len, + next_pk_len ); // Only process wrappers whose key_epoch == curr_epoch. let drained: Vec<(H256, u64, sp_runtime::AccountId32, Vec)> = { - let mut buf = buffer.lock().unwrap(); - buf.drain_for_epoch(curr_epoch) + match buffer.lock() { + Ok(mut buf) => buf.drain_for_epoch(curr_epoch), + Err(e) => { + log::warn!( + target: "mev-shield", + "revealer: failed to lock WrapperBuffer for drain_for_epoch: {:?}", + e + ); + Vec::new() + } + } }; - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", "revealer: drained {} buffered wrappers for current epoch={}", - drained.len(), curr_epoch + drained.len(), + curr_epoch ); let mut to_submit: Vec<(H256, node_subtensor_runtime::RuntimeCall)> = Vec::new(); for (id, key_epoch, author, blob) in drained.into_iter() { - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", "revealer: candidate id=0x{} key_epoch={} (curr_epoch={}) author={} blob_len={}", - hex::encode(id.as_bytes()), key_epoch, curr_epoch, author, blob.len() + hex::encode(id.as_bytes()), + key_epoch, + curr_epoch, + author, + blob.len() ); - if blob.len() < 2 { - log::info!(target: "mev-shield", - " id=0x{}: blob too short (<2 bytes)", hex::encode(id.as_bytes()) - ); - continue; - } - let kem_len = u16::from_le_bytes([blob[0], blob[1]]) as usize; - if blob.len() < 2 + kem_len + 24 { - log::info!(target: "mev-shield", - " id=0x{}: blob too short (kem_len={}, total={})", - hex::encode(id.as_bytes()), kem_len, blob.len() - ); - continue; - } - let kem_ct_bytes = &blob[2 .. 2 + kem_len]; - let nonce_bytes = &blob[2 + kem_len .. 2 + kem_len + 24]; - let aead_body = &blob[2 + kem_len + 24 ..]; + // Safely parse blob: [u16 kem_len][kem_ct][nonce24][aead_ct] + let kem_len: usize = match blob + .get(0..2) + .and_then(|two| <[u8; 2]>::try_from(two).ok()) + { + Some(arr) => u16::from_le_bytes(arr) as usize, + None => { + log::info!( + target: "mev-shield", + " id=0x{}: blob too short or invalid length prefix", + hex::encode(id.as_bytes()) + ); + continue; + } + }; + + let kem_end = match 2usize.checked_add(kem_len) { + Some(v) => v, + None => { + log::info!( + target: "mev-shield", + " id=0x{}: kem_len overflow", + hex::encode(id.as_bytes()) + ); + continue; + } + }; + + let nonce_end = match kem_end.checked_add(24usize) { + Some(v) => v, + None => { + log::info!( + target: "mev-shield", + " id=0x{}: nonce range overflow", + hex::encode(id.as_bytes()) + ); + continue; + } + }; + + let kem_ct_bytes = match blob.get(2..kem_end) { + Some(s) => s, + None => { + log::info!( + target: "mev-shield", + " id=0x{}: blob too short for kem_ct (kem_len={}, total={})", + hex::encode(id.as_bytes()), + kem_len, + blob.len() + ); + continue; + } + }; + + let nonce_bytes = match blob.get(kem_end..nonce_end) { + Some(s) if s.len() == 24 => s, + _ => { + log::info!( + target: "mev-shield", + " id=0x{}: blob too short for 24-byte nonce (kem_len={}, total={})", + hex::encode(id.as_bytes()), + kem_len, + blob.len() + ); + continue; + } + }; + + let aead_body = match blob.get(nonce_end..) { + Some(s) => s, + None => { + log::info!( + target: "mev-shield", + " id=0x{}: blob has no AEAD body", + hex::encode(id.as_bytes()) + ); + continue; + } + }; let kem_ct_hash = sp_core::hashing::blake2_256(kem_ct_bytes); let aead_body_hash = sp_core::hashing::blake2_256(aead_body); - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: kem_len={} kem_ct_hash=0x{} nonce=0x{} aead_body_len={} aead_body_hash=0x{}", - hex::encode(id.as_bytes()), kem_len, + hex::encode(id.as_bytes()), + kem_len, hex::encode(kem_ct_hash), hex::encode(nonce_bytes), aead_body.len(), @@ -306,9 +446,12 @@ pub fn spawn_revealer( let enc_sk = match Encoded::>::try_from(&curr_sk_bytes[..]) { Ok(e) => e, Err(e) => { - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: DecapsulationKey::try_from(sk_bytes) failed (len={}, err={:?})", - hex::encode(id.as_bytes()), curr_sk_bytes.len(), e + hex::encode(id.as_bytes()), + curr_sk_bytes.len(), + e ); continue; } @@ -318,9 +461,11 @@ pub fn spawn_revealer( let ct = match Ciphertext::::try_from(kem_ct_bytes) { Ok(c) => c, Err(e) => { - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: Ciphertext::try_from failed: {:?}", - hex::encode(id.as_bytes()), e + hex::encode(id.as_bytes()), + e ); continue; } @@ -329,7 +474,8 @@ pub fn spawn_revealer( let ss = match sk.decapsulate(&ct) { Ok(s) => s, Err(_) => { - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: ML‑KEM decapsulate() failed", hex::encode(id.as_bytes()) ); @@ -339,9 +485,11 @@ pub fn spawn_revealer( let ss_bytes: &[u8] = ss.as_ref(); if ss_bytes.len() != 32 { - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: shared secret len={} != 32; skipping", - hex::encode(id.as_bytes()), ss_bytes.len() + hex::encode(id.as_bytes()), + ss_bytes.len() ); continue; } @@ -352,21 +500,28 @@ pub fn spawn_revealer( let aead_key = crate::mev_shield::author::derive_aead_key(&ss32); let key_hash = sp_core::hashing::blake2_256(&aead_key); - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: decapsulated shared_secret_len=32 shared_secret_hash=0x{}", - hex::encode(id.as_bytes()), hex::encode(ss_hash) + hex::encode(id.as_bytes()), + hex::encode(ss_hash) ); - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: derived AEAD key hash=0x{} (direct-from-ss)", - hex::encode(id.as_bytes()), hex::encode(key_hash) + hex::encode(id.as_bytes()), + hex::encode(key_hash) ); let mut nonce24 = [0u8; 24]; nonce24.copy_from_slice(nonce_bytes); - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: attempting AEAD decrypt nonce=0x{} ct_len={}", - hex::encode(id.as_bytes()), hex::encode(nonce24), aead_body.len() + hex::encode(id.as_bytes()), + hex::encode(nonce24), + aead_body.len() ); let plaintext = match crate::mev_shield::author::aead_decrypt( @@ -377,7 +532,8 @@ pub fn spawn_revealer( ) { Some(pt) => pt, None => { - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: AEAD decrypt FAILED with direct-from-ss key; ct_hash=0x{}", hex::encode(id.as_bytes()), hex::encode(aead_body_hash), @@ -386,67 +542,201 @@ pub fn spawn_revealer( } }; - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: AEAD decrypt OK, plaintext_len={}", - hex::encode(id.as_bytes()), plaintext.len() + hex::encode(id.as_bytes()), + plaintext.len() ); - // Decode plaintext layout… type RuntimeNonce = ::Nonce; - if plaintext.len() < 32 + 4 + 1 + 1 + 64 { - log::info!(target: "mev-shield", + // Safely parse plaintext layout without panics. + // Layout: signer (32) || nonce (4) || mortality (1) || call (..) + // || sig_kind (1) || sig (64) + let min_plain_len: usize = 32usize + .saturating_add(4) + .saturating_add(1) + .saturating_add(1) + .saturating_add(64); + if plaintext.len() < min_plain_len { + log::info!( + target: "mev-shield", " id=0x{}: plaintext too short ({}) for expected layout", - hex::encode(id.as_bytes()), plaintext.len() + hex::encode(id.as_bytes()), + plaintext.len() ); continue; } - let signer_raw = &plaintext[0..32]; - let nonce_le = &plaintext[32..36]; - let _mortality_byte = plaintext[36]; + let signer_raw = match plaintext.get(0..32) { + Some(s) => s, + None => { + log::info!( + target: "mev-shield", + " id=0x{}: missing signer bytes", + hex::encode(id.as_bytes()) + ); + continue; + } + }; + + let nonce_le = match plaintext.get(32..36) { + Some(s) => s, + None => { + log::info!( + target: "mev-shield", + " id=0x{}: missing nonce bytes", + hex::encode(id.as_bytes()) + ); + continue; + } + }; - let sig_off = plaintext.len() - 65; - let call_bytes = &plaintext[37 .. sig_off]; - let sig_kind = plaintext[sig_off]; - let sig_raw = &plaintext[sig_off + 1 ..]; + let mortality_byte = match plaintext.get(36) { + Some(b) => *b, + None => { + log::info!( + target: "mev-shield", + " id=0x{}: missing mortality byte", + hex::encode(id.as_bytes()) + ); + continue; + } + }; - let signer = sp_runtime::AccountId32::new( - <[u8; 32]>::try_from(signer_raw).expect("signer_raw is 32 bytes; qed"), - ); - let raw_nonce_u32 = u32::from_le_bytes( - <[u8; 4]>::try_from(nonce_le).expect("nonce_le is 4 bytes; qed"), - ); + let sig_off = match plaintext.len().checked_sub(65) { + Some(off) if off >= 37 => off, + _ => { + log::info!( + target: "mev-shield", + " id=0x{}: invalid plaintext length for signature split", + hex::encode(id.as_bytes()) + ); + continue; + } + }; + + let call_bytes = match plaintext.get(37..sig_off) { + Some(s) => s, + None => { + log::info!( + target: "mev-shield", + " id=0x{}: missing call bytes", + hex::encode(id.as_bytes()) + ); + continue; + } + }; + + let sig_kind = match plaintext.get(sig_off) { + Some(b) => *b, + None => { + log::info!( + target: "mev-shield", + " id=0x{}: missing signature kind byte", + hex::encode(id.as_bytes()) + ); + continue; + } + }; + + let sig_start = match sig_off.checked_add(1) { + Some(v) => v, + None => { + log::info!( + target: "mev-shield", + " id=0x{}: sig_start overflow", + hex::encode(id.as_bytes()) + ); + continue; + } + }; + + let sig_raw = match plaintext.get(sig_start..) { + Some(s) => s, + None => { + log::info!( + target: "mev-shield", + " id=0x{}: missing signature bytes", + hex::encode(id.as_bytes()) + ); + continue; + } + }; + + let signer_array: [u8; 32] = match signer_raw.try_into() { + Ok(a) => a, + Err(_) => { + log::info!( + target: "mev-shield", + " id=0x{}: signer_raw not 32 bytes", + hex::encode(id.as_bytes()) + ); + continue; + } + }; + let signer = sp_runtime::AccountId32::new(signer_array); + + let nonce_array: [u8; 4] = match nonce_le.try_into() { + Ok(a) => a, + Err(_) => { + log::info!( + target: "mev-shield", + " id=0x{}: nonce bytes not 4 bytes", + hex::encode(id.as_bytes()) + ); + continue; + } + }; + let raw_nonce_u32 = u32::from_le_bytes(nonce_array); let account_nonce: RuntimeNonce = raw_nonce_u32.saturated_into(); - let mortality = Era::Immortal; + + // Mortality currently only supports immortal; we still + // parse the byte to keep layout consistent. + let _mortality = match mortality_byte { + 0 => Era::Immortal, + _ => Era::Immortal, + }; let inner_call: node_subtensor_runtime::RuntimeCall = match Decode::decode(&mut &call_bytes[..]) { Ok(c) => c, Err(e) => { - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: failed to decode RuntimeCall (len={}): {:?}", - hex::encode(id.as_bytes()), call_bytes.len(), e + hex::encode(id.as_bytes()), + call_bytes.len(), + e ); continue; } }; - let signature: MultiSignature = if sig_kind == 0x01 && sig_raw.len() == 64 { - let mut raw = [0u8; 64]; - raw.copy_from_slice(sig_raw); - MultiSignature::from(sp_core::sr25519::Signature::from_raw(raw)) - } else { - log::info!(target: "mev-shield", - " id=0x{}: unsupported signature format kind=0x{:02x}, len={}", - hex::encode(id.as_bytes()), sig_kind, sig_raw.len() - ); - continue; - }; + let signature: MultiSignature = + if sig_kind == 0x01 && sig_raw.len() == 64 { + let mut raw = [0u8; 64]; + raw.copy_from_slice(sig_raw); + MultiSignature::from(sp_core::sr25519::Signature::from_raw(raw)) + } else { + log::info!( + target: "mev-shield", + " id=0x{}: unsupported signature format kind=0x{:02x}, len={}", + hex::encode(id.as_bytes()), + sig_kind, + sig_raw.len() + ); + continue; + }; - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: decrypted wrapper: signer={}, nonce={}, call={:?}", - hex::encode(id.as_bytes()), signer, raw_nonce_u32, inner_call + hex::encode(id.as_bytes()), + signer, + raw_nonce_u32, + inner_call ); let reveal = node_subtensor_runtime::RuntimeCall::MevShield( @@ -454,7 +744,7 @@ pub fn spawn_revealer( id, signer: signer.clone(), nonce: account_nonce, - mortality, + mortality: Era::Immortal, call: Box::new(inner_call), signature, } @@ -465,9 +755,11 @@ pub fn spawn_revealer( // Submit locally. let at = client.info().best_hash; - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", "revealer: submitting {} execute_revealed calls at best_hash={:?}", - to_submit.len(), at + to_submit.len(), + at ); for (id, call) in to_submit.into_iter() { @@ -475,9 +767,11 @@ pub fn spawn_revealer( node_subtensor_runtime::UncheckedExtrinsic::new_bare(call); let xt_bytes = uxt.encode(); - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: encoded UncheckedExtrinsic len={}", - hex::encode(id.as_bytes()), xt_bytes.len() + hex::encode(id.as_bytes()), + xt_bytes.len() ); match OpaqueExtrinsic::from_bytes(&xt_bytes) { @@ -485,28 +779,35 @@ pub fn spawn_revealer( match pool.submit_one(at, TransactionSource::Local, opaque).await { Ok(_) => { let xt_hash = sp_core::hashing::blake2_256(&xt_bytes); - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: submit_one(execute_revealed) OK, xt_hash=0x{}", - hex::encode(id.as_bytes()), hex::encode(xt_hash) + hex::encode(id.as_bytes()), + hex::encode(xt_hash) ); } Err(e) => { - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: submit_one(execute_revealed) FAILED: {:?}", - hex::encode(id.as_bytes()), e + hex::encode(id.as_bytes()), + e ); } } } Err(e) => { - log::info!(target: "mev-shield", + log::info!( + target: "mev-shield", " id=0x{}: OpaqueExtrinsic::from_bytes failed: {:?}", - hex::encode(id.as_bytes()), e + hex::encode(id.as_bytes()), + e ); } } } + // Let the decrypt window elapse. sleep(Duration::from_millis(ctx.timing.decrypt_window_ms)).await; } }, From 680880f497072a1b26dd0907b7884b5741be41ea Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:50:01 -0800 Subject: [PATCH 07/30] fix logs --- node/src/mev_shield/author.rs | 16 ++--- node/src/mev_shield/proposer.rs | 112 ++++++++++++++++---------------- 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/node/src/mev_shield/author.rs b/node/src/mev_shield/author.rs index b99d737fae..ce81dee1b3 100644 --- a/node/src/mev_shield/author.rs +++ b/node/src/mev_shield/author.rs @@ -170,7 +170,7 @@ where let (epoch_now, curr_pk_len, next_pk_len) = match ctx_clone.keys.lock() { Ok(k) => (k.epoch, k.current_pk.len(), k.next_pk.len()), Err(e) => { - log::warn!( + log::debug!( target: "mev-shield", "spawn_author_tasks: failed to lock ShieldKeys (poisoned?): {:?}", e @@ -179,7 +179,7 @@ where } }; - log::info!( + log::debug!( target: "mev-shield", "Slot start (local author): epoch={} (pk sizes: curr={}B, next={}B)", epoch_now, curr_pk_len, next_pk_len @@ -192,7 +192,7 @@ where let (next_pk, next_epoch) = match ctx_clone.keys.lock() { Ok(k) => (k.next_pk.clone(), k.epoch.saturating_add(1)), Err(e) => { - log::warn!( + log::debug!( target: "mev-shield", "spawn_author_tasks: failed to lock ShieldKeys for next_pk: {:?}", e @@ -234,13 +234,13 @@ where { local_nonce = local_nonce.saturating_add(2); } else { - log::warn!( + log::debug!( target: "mev-shield", "announce_next_key retry failed after stale nonce: {e:?}" ); } } else { - log::warn!( + log::debug!( target: "mev-shield", "announce_next_key submit error: {e:?}" ); @@ -256,14 +256,14 @@ where match ctx_clone.keys.lock() { Ok(mut k) => { k.roll_for_next_slot(); - log::info!( + log::debug!( target: "mev-shield", "Rolled ML-KEM key at slot boundary (local author): new epoch={}", k.epoch ); } Err(e) => { - log::warn!( + log::debug!( target: "mev-shield", "spawn_author_tasks: failed to lock ShieldKeys for roll_for_next_slot: {:?}", e @@ -415,7 +415,7 @@ where pool.submit_one(info.best_hash, TransactionSource::Local, xt).await?; - log::info!( + log::debug!( target: "mev-shield", "announce_next_key submitted: xt=0x{}, epoch={}, nonce={}", hex::encode(xt_hash), diff --git a/node/src/mev_shield/proposer.rs b/node/src/mev_shield/proposer.rs index 9bb3f309b7..663160dc5f 100644 --- a/node/src/mev_shield/proposer.rs +++ b/node/src/mev_shield/proposer.rs @@ -65,7 +65,7 @@ impl WrapperBuffer { } else { // key_epoch < epoch => stale / missed reveal window; drop. dropped_past = dropped_past.saturating_add(1); - log::info!( + log::debug!( target: "mev-shield", "revealer: dropping stale wrapper id=0x{} key_epoch={} < curr_epoch={}", hex::encode(id.as_bytes()), @@ -76,7 +76,7 @@ impl WrapperBuffer { } }); - log::info!( + log::debug!( target: "mev-shield", "revealer: drain_for_epoch(epoch={}): ready={}, kept_future={}, dropped_past={}", epoch, @@ -124,13 +124,13 @@ pub fn spawn_revealer( "mev-shield-buffer-wrappers", None, async move { - log::info!(target: "mev-shield", "buffer-wrappers task started"); + log::debug!(target: "mev-shield", "buffer-wrappers task started"); let mut import_stream = client.import_notification_stream(); while let Some(notif) = import_stream.next().await { let at_hash = notif.hash; - log::info!( + log::debug!( target: "mev-shield", "imported block hash={:?} origin={:?}", at_hash, notif.origin @@ -138,7 +138,7 @@ pub fn spawn_revealer( match client.block_body(at_hash) { Ok(Some(body)) => { - log::info!( + log::debug!( target: "mev-shield", " block has {} extrinsics", body.len() @@ -146,7 +146,7 @@ pub fn spawn_revealer( for (idx, opaque_xt) in body.into_iter().enumerate() { let encoded = opaque_xt.encode(); - log::info!( + log::debug!( target: "mev-shield", " [xt #{idx}] opaque len={} bytes", encoded.len() @@ -155,7 +155,7 @@ pub fn spawn_revealer( let uxt: RUnchecked = match RUnchecked::decode(&mut &encoded[..]) { Ok(u) => u, Err(e) => { - log::info!( + log::debug!( target: "mev-shield", " [xt #{idx}] failed to decode UncheckedExtrinsic: {:?}", e @@ -164,7 +164,7 @@ pub fn spawn_revealer( } }; - log::info!( + log::debug!( target: "mev-shield", " [xt #{idx}] decoded call: {:?}", &uxt.0.function @@ -184,7 +184,7 @@ pub fn spawn_revealer( }; let Some(author) = author_opt else { - log::info!( + log::debug!( target: "mev-shield", " [xt #{idx}] not a Signed(AccountId32) extrinsic; skipping" ); @@ -203,7 +203,7 @@ pub fn spawn_revealer( let payload = (author.clone(), *commitment, ciphertext).encode(); let id = H256(sp_core::hashing::blake2_256(&payload)); - log::info!( + log::debug!( target: "mev-shield", " [xt #{idx}] buffered submit_encrypted: id=0x{}, key_epoch={}, author={}, ct_len={}, commitment={:?}", hex::encode(id.as_bytes()), @@ -221,7 +221,7 @@ pub fn spawn_revealer( ciphertext.to_vec(), ); } else { - log::warn!( + log::debug!( target: "mev-shield", " [xt #{idx}] failed to lock WrapperBuffer; dropping wrapper" ); @@ -229,12 +229,12 @@ pub fn spawn_revealer( } } } - Ok(None) => log::info!( + Ok(None) => log::debug!( target: "mev-shield", " block_body returned None for hash={:?}", at_hash ), - Err(e) => log::info!( + Err(e) => log::debug!( target: "mev-shield", " block_body error for hash={:?}: {:?}", at_hash, @@ -257,11 +257,11 @@ pub fn spawn_revealer( "mev-shield-last-3s-revealer", None, async move { - log::info!(target: "mev-shield", "last-3s-revealer task started"); + log::debug!(target: "mev-shield", "last-3s-revealer task started"); loop { let tail = ctx.timing.slot_ms.saturating_sub(ctx.timing.decrypt_window_ms); - log::info!( + log::debug!( target: "mev-shield", "revealer: sleeping {} ms before decrypt window (slot_ms={}, decrypt_window_ms={})", tail, @@ -284,7 +284,7 @@ pub fn spawn_revealer( )) } Err(e) => { - log::warn!( + log::debug!( target: "mev-shield", "revealer: failed to lock ShieldKeys (poisoned?): {:?}", e @@ -304,7 +304,7 @@ pub fn spawn_revealer( } }; - log::info!( + log::debug!( target: "mev-shield", "revealer: decrypt window start. epoch={} sk_len={} sk_hash=0x{} curr_pk_len={} next_pk_len={}", curr_epoch, @@ -319,7 +319,7 @@ pub fn spawn_revealer( match buffer.lock() { Ok(mut buf) => buf.drain_for_epoch(curr_epoch), Err(e) => { - log::warn!( + log::debug!( target: "mev-shield", "revealer: failed to lock WrapperBuffer for drain_for_epoch: {:?}", e @@ -329,7 +329,7 @@ pub fn spawn_revealer( } }; - log::info!( + log::debug!( target: "mev-shield", "revealer: drained {} buffered wrappers for current epoch={}", drained.len(), @@ -339,7 +339,7 @@ pub fn spawn_revealer( let mut to_submit: Vec<(H256, node_subtensor_runtime::RuntimeCall)> = Vec::new(); for (id, key_epoch, author, blob) in drained.into_iter() { - log::info!( + log::debug!( target: "mev-shield", "revealer: candidate id=0x{} key_epoch={} (curr_epoch={}) author={} blob_len={}", hex::encode(id.as_bytes()), @@ -356,7 +356,7 @@ pub fn spawn_revealer( { Some(arr) => u16::from_le_bytes(arr) as usize, None => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: blob too short or invalid length prefix", hex::encode(id.as_bytes()) @@ -368,7 +368,7 @@ pub fn spawn_revealer( let kem_end = match 2usize.checked_add(kem_len) { Some(v) => v, None => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: kem_len overflow", hex::encode(id.as_bytes()) @@ -380,7 +380,7 @@ pub fn spawn_revealer( let nonce_end = match kem_end.checked_add(24usize) { Some(v) => v, None => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: nonce range overflow", hex::encode(id.as_bytes()) @@ -392,7 +392,7 @@ pub fn spawn_revealer( let kem_ct_bytes = match blob.get(2..kem_end) { Some(s) => s, None => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: blob too short for kem_ct (kem_len={}, total={})", hex::encode(id.as_bytes()), @@ -406,7 +406,7 @@ pub fn spawn_revealer( let nonce_bytes = match blob.get(kem_end..nonce_end) { Some(s) if s.len() == 24 => s, _ => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: blob too short for 24-byte nonce (kem_len={}, total={})", hex::encode(id.as_bytes()), @@ -420,7 +420,7 @@ pub fn spawn_revealer( let aead_body = match blob.get(nonce_end..) { Some(s) => s, None => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: blob has no AEAD body", hex::encode(id.as_bytes()) @@ -431,7 +431,7 @@ pub fn spawn_revealer( let kem_ct_hash = sp_core::hashing::blake2_256(kem_ct_bytes); let aead_body_hash = sp_core::hashing::blake2_256(aead_body); - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: kem_len={} kem_ct_hash=0x{} nonce=0x{} aead_body_len={} aead_body_hash=0x{}", hex::encode(id.as_bytes()), @@ -446,7 +446,7 @@ pub fn spawn_revealer( let enc_sk = match Encoded::>::try_from(&curr_sk_bytes[..]) { Ok(e) => e, Err(e) => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: DecapsulationKey::try_from(sk_bytes) failed (len={}, err={:?})", hex::encode(id.as_bytes()), @@ -461,7 +461,7 @@ pub fn spawn_revealer( let ct = match Ciphertext::::try_from(kem_ct_bytes) { Ok(c) => c, Err(e) => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: Ciphertext::try_from failed: {:?}", hex::encode(id.as_bytes()), @@ -474,9 +474,9 @@ pub fn spawn_revealer( let ss = match sk.decapsulate(&ct) { Ok(s) => s, Err(_) => { - log::info!( + log::debug!( target: "mev-shield", - " id=0x{}: ML‑KEM decapsulate() failed", + " id=0x{}: ML-KEM decapsulate() failed", hex::encode(id.as_bytes()) ); continue; @@ -485,7 +485,7 @@ pub fn spawn_revealer( let ss_bytes: &[u8] = ss.as_ref(); if ss_bytes.len() != 32 { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: shared secret len={} != 32; skipping", hex::encode(id.as_bytes()), @@ -500,13 +500,13 @@ pub fn spawn_revealer( let aead_key = crate::mev_shield::author::derive_aead_key(&ss32); let key_hash = sp_core::hashing::blake2_256(&aead_key); - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: decapsulated shared_secret_len=32 shared_secret_hash=0x{}", hex::encode(id.as_bytes()), hex::encode(ss_hash) ); - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: derived AEAD key hash=0x{} (direct-from-ss)", hex::encode(id.as_bytes()), @@ -516,7 +516,7 @@ pub fn spawn_revealer( let mut nonce24 = [0u8; 24]; nonce24.copy_from_slice(nonce_bytes); - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: attempting AEAD decrypt nonce=0x{} ct_len={}", hex::encode(id.as_bytes()), @@ -532,7 +532,7 @@ pub fn spawn_revealer( ) { Some(pt) => pt, None => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: AEAD decrypt FAILED with direct-from-ss key; ct_hash=0x{}", hex::encode(id.as_bytes()), @@ -542,7 +542,7 @@ pub fn spawn_revealer( } }; - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: AEAD decrypt OK, plaintext_len={}", hex::encode(id.as_bytes()), @@ -560,7 +560,7 @@ pub fn spawn_revealer( .saturating_add(1) .saturating_add(64); if plaintext.len() < min_plain_len { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: plaintext too short ({}) for expected layout", hex::encode(id.as_bytes()), @@ -572,7 +572,7 @@ pub fn spawn_revealer( let signer_raw = match plaintext.get(0..32) { Some(s) => s, None => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: missing signer bytes", hex::encode(id.as_bytes()) @@ -584,7 +584,7 @@ pub fn spawn_revealer( let nonce_le = match plaintext.get(32..36) { Some(s) => s, None => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: missing nonce bytes", hex::encode(id.as_bytes()) @@ -596,7 +596,7 @@ pub fn spawn_revealer( let mortality_byte = match plaintext.get(36) { Some(b) => *b, None => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: missing mortality byte", hex::encode(id.as_bytes()) @@ -608,7 +608,7 @@ pub fn spawn_revealer( let sig_off = match plaintext.len().checked_sub(65) { Some(off) if off >= 37 => off, _ => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: invalid plaintext length for signature split", hex::encode(id.as_bytes()) @@ -620,7 +620,7 @@ pub fn spawn_revealer( let call_bytes = match plaintext.get(37..sig_off) { Some(s) => s, None => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: missing call bytes", hex::encode(id.as_bytes()) @@ -632,7 +632,7 @@ pub fn spawn_revealer( let sig_kind = match plaintext.get(sig_off) { Some(b) => *b, None => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: missing signature kind byte", hex::encode(id.as_bytes()) @@ -644,7 +644,7 @@ pub fn spawn_revealer( let sig_start = match sig_off.checked_add(1) { Some(v) => v, None => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: sig_start overflow", hex::encode(id.as_bytes()) @@ -656,7 +656,7 @@ pub fn spawn_revealer( let sig_raw = match plaintext.get(sig_start..) { Some(s) => s, None => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: missing signature bytes", hex::encode(id.as_bytes()) @@ -668,7 +668,7 @@ pub fn spawn_revealer( let signer_array: [u8; 32] = match signer_raw.try_into() { Ok(a) => a, Err(_) => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: signer_raw not 32 bytes", hex::encode(id.as_bytes()) @@ -681,7 +681,7 @@ pub fn spawn_revealer( let nonce_array: [u8; 4] = match nonce_le.try_into() { Ok(a) => a, Err(_) => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: nonce bytes not 4 bytes", hex::encode(id.as_bytes()) @@ -703,7 +703,7 @@ pub fn spawn_revealer( match Decode::decode(&mut &call_bytes[..]) { Ok(c) => c, Err(e) => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: failed to decode RuntimeCall (len={}): {:?}", hex::encode(id.as_bytes()), @@ -720,7 +720,7 @@ pub fn spawn_revealer( raw.copy_from_slice(sig_raw); MultiSignature::from(sp_core::sr25519::Signature::from_raw(raw)) } else { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: unsupported signature format kind=0x{:02x}, len={}", hex::encode(id.as_bytes()), @@ -730,7 +730,7 @@ pub fn spawn_revealer( continue; }; - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: decrypted wrapper: signer={}, nonce={}, call={:?}", hex::encode(id.as_bytes()), @@ -755,7 +755,7 @@ pub fn spawn_revealer( // Submit locally. let at = client.info().best_hash; - log::info!( + log::debug!( target: "mev-shield", "revealer: submitting {} execute_revealed calls at best_hash={:?}", to_submit.len(), @@ -767,7 +767,7 @@ pub fn spawn_revealer( node_subtensor_runtime::UncheckedExtrinsic::new_bare(call); let xt_bytes = uxt.encode(); - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: encoded UncheckedExtrinsic len={}", hex::encode(id.as_bytes()), @@ -779,7 +779,7 @@ pub fn spawn_revealer( match pool.submit_one(at, TransactionSource::Local, opaque).await { Ok(_) => { let xt_hash = sp_core::hashing::blake2_256(&xt_bytes); - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: submit_one(execute_revealed) OK, xt_hash=0x{}", hex::encode(id.as_bytes()), @@ -787,7 +787,7 @@ pub fn spawn_revealer( ); } Err(e) => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: submit_one(execute_revealed) FAILED: {:?}", hex::encode(id.as_bytes()), @@ -797,7 +797,7 @@ pub fn spawn_revealer( } } Err(e) => { - log::info!( + log::debug!( target: "mev-shield", " id=0x{}: OpaqueExtrinsic::from_bytes failed: {:?}", hex::encode(id.as_bytes()), From c327692f944dfe5a30bf2dbe013d73563d99bef6 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:51:47 -0800 Subject: [PATCH 08/30] fmt --- node/src/lib.rs | 2 +- node/src/main.rs | 2 +- node/src/mev_shield/author.rs | 147 +++++++++++++++++--------------- node/src/mev_shield/mod.rs | 2 +- node/src/mev_shield/proposer.rs | 45 ++++------ node/src/service.rs | 26 +++--- pallets/shield/src/lib.rs | 62 +++++++------- 7 files changed, 141 insertions(+), 145 deletions(-) diff --git a/node/src/lib.rs b/node/src/lib.rs index c5e7a90c43..ab4a409e1b 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -4,6 +4,6 @@ pub mod client; pub mod conditional_evm_block_import; pub mod consensus; pub mod ethereum; +pub mod mev_shield; pub mod rpc; pub mod service; -pub mod mev_shield; diff --git a/node/src/main.rs b/node/src/main.rs index 3aac9b0d9f..7adffa0ae9 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -10,9 +10,9 @@ mod command; mod conditional_evm_block_import; mod consensus; mod ethereum; +mod mev_shield; mod rpc; mod service; -mod mev_shield; fn main() -> sc_cli::Result<()> { command::run() diff --git a/node/src/mev_shield/author.rs b/node/src/mev_shield/author.rs index ce81dee1b3..43ae5fee87 100644 --- a/node/src/mev_shield/author.rs +++ b/node/src/mev_shield/author.rs @@ -1,29 +1,32 @@ -use std::{sync::{Arc, Mutex}}; -use sp_core::blake2_256; -use sp_runtime::KeyTypeId; -use tokio::time::sleep; -use chacha20poly1305::{XChaCha20Poly1305, KeyInit, aead::{Aead, Payload}, XNonce}; +use chacha20poly1305::{ + KeyInit, XChaCha20Poly1305, XNonce, + aead::{Aead, Payload}, +}; +use ml_kem::{EncodedSizeUser, KemCore, MlKem768}; use node_subtensor_runtime as runtime; -use ml_kem::{MlKem768, KemCore, EncodedSizeUser}; use rand::rngs::OsRng; +use sp_core::blake2_256; +use sp_runtime::KeyTypeId; +use std::sync::{Arc, Mutex}; use subtensor_macros::freeze_struct; +use tokio::time::sleep; /// Parameters controlling time windows inside the slot. -#[freeze_struct("cb816cf709ea285b")] +#[freeze_struct("5c7ce101b36950de")] #[derive(Clone)] pub struct TimeParams { pub slot_ms: u64, pub announce_at_ms: u64, - pub decrypt_window_ms: u64 + pub decrypt_window_ms: u64, } /// Holds the current/next ML‑KEM keypairs and their 32‑byte fingerprints. #[freeze_struct("3a83c10877ec1f24")] #[derive(Clone)] pub struct ShieldKeys { - pub current_sk: Vec, // ML‑KEM secret key bytes (encoded form) - pub current_pk: Vec, // ML‑KEM public key bytes (encoded form) - pub current_fp: [u8; 32], // blake2_256(pk) + pub current_sk: Vec, // ML‑KEM secret key bytes (encoded form) + pub current_pk: Vec, // ML‑KEM public key bytes (encoded form) + pub current_fp: [u8; 32], // blake2_256(pk) pub next_sk: Vec, pub next_pk: Vec, pub next_fp: [u8; 32], @@ -52,7 +55,15 @@ impl ShieldKeys { let next_pk = npk_slice.to_vec(); let next_fp = blake2_256(npk_slice); - Self { current_sk, current_pk, current_fp, next_sk, next_pk, next_fp, epoch } + Self { + current_sk, + current_pk, + current_fp, + next_sk, + next_pk, + next_fp, + epoch, + } } pub fn roll_for_next_slot(&mut self) { @@ -99,7 +110,14 @@ pub fn aead_decrypt( aad: &[u8], ) -> Option> { let aead = XChaCha20Poly1305::new((&key).into()); - aead.decrypt(XNonce::from_slice(&nonce24), Payload { msg: ciphertext, aad }).ok() + aead.decrypt( + XNonce::from_slice(&nonce24), + Payload { + msg: ciphertext, + aad, + }, + ) + .ok() } const AURA_KEY_TYPE: KeyTypeId = KeyTypeId(*b"aura"); @@ -110,23 +128,19 @@ const AURA_KEY_TYPE: KeyTypeId = KeyTypeId(*b"aura"); pub fn spawn_author_tasks( task_spawner: &sc_service::SpawnTaskHandle, client: std::sync::Arc, - pool: std::sync::Arc, + pool: std::sync::Arc, keystore: sp_keystore::KeystorePtr, initial_epoch: u64, timing: TimeParams, ) -> ShieldContext where B: sp_runtime::traits::Block, - C: sc_client_api::HeaderBackend - + sc_client_api::BlockchainEvents - + Send - + Sync - + 'static, + C: sc_client_api::HeaderBackend + sc_client_api::BlockchainEvents + Send + Sync + 'static, Pool: sc_transaction_pool_api::TransactionPool + Send + Sync + 'static, B::Extrinsic: From, { let ctx = ShieldContext { - keys: std::sync::Arc::new(std::sync::Mutex::new(ShieldKeys::new(initial_epoch))), + keys: std::sync::Arc::new(std::sync::Mutex::new(ShieldKeys::new(initial_epoch))), timing: timing.clone(), }; @@ -144,9 +158,9 @@ where } }; - let ctx_clone = ctx.clone(); - let client_clone = client.clone(); - let pool_clone = pool.clone(); + let ctx_clone = ctx.clone(); + let client_clone = client.clone(); + let pool_clone = pool.clone(); let keystore_clone = keystore.clone(); // Slot tick / key-announce loop. @@ -277,11 +291,10 @@ where ctx } - /// Build & submit the signed `announce_next_key` extrinsic OFF-CHAIN pub async fn submit_announce_extrinsic( client: std::sync::Arc, - pool: std::sync::Arc, + pool: std::sync::Arc, keystore: sp_keystore::KeystorePtr, aura_pub: sp_core::sr25519::Public, next_public_key: Vec, @@ -296,15 +309,16 @@ where B::Hash: AsRef<[u8]>, { use node_subtensor_runtime as runtime; - use runtime::{RuntimeCall, UncheckedExtrinsic, SignedPayload}; + use runtime::{RuntimeCall, SignedPayload, UncheckedExtrinsic}; use sc_transaction_pool_api::TransactionSource; use sp_core::H256; + use sp_runtime::codec::Encode; use sp_runtime::{ - AccountId32, MultiSignature, generic::Era, BoundedVec, - traits::{ConstU32, TransactionExtension} + AccountId32, BoundedVec, MultiSignature, + generic::Era, + traits::{ConstU32, TransactionExtension}, }; - use sp_runtime::codec::Encode; // Helper: map a Block hash to H256 fn to_h256>(h: H) -> H256 { @@ -319,9 +333,10 @@ where let src_start = bytes.len().saturating_sub(n); let dst_start = 32usize.saturating_sub(n); - if let (Some(dst), Some(src)) = - (out.get_mut(dst_start..32), bytes.get(src_start..src_start + n)) - { + if let (Some(dst), Some(src)) = ( + out.get_mut(dst_start..32), + bytes.get(src_start..src_start + n), + ) { dst.copy_from_slice(src); H256(out) } else { @@ -331,38 +346,37 @@ where } type MaxPk = ConstU32<2048>; - let public_key: BoundedVec = - BoundedVec::try_from(next_public_key) - .map_err(|_| anyhow::anyhow!("public key too long (>2048 bytes)"))?; + let public_key: BoundedVec = BoundedVec::try_from(next_public_key) + .map_err(|_| anyhow::anyhow!("public key too long (>2048 bytes)"))?; // 1) The runtime call carrying public key bytes. - let call = RuntimeCall::MevShield( - pallet_shield::Call::announce_next_key { - public_key, - epoch, - } - ); + let call = RuntimeCall::MevShield(pallet_shield::Call::announce_next_key { public_key, epoch }); type Extra = runtime::TransactionExtensions; - let extra: Extra = ( - frame_system::CheckNonZeroSender::::new(), - frame_system::CheckSpecVersion::::new(), - frame_system::CheckTxVersion::::new(), - frame_system::CheckGenesis::::new(), - frame_system::CheckEra::::from(Era::Immortal), - node_subtensor_runtime::check_nonce::CheckNonce::::from(nonce).into(), - frame_system::CheckWeight::::new(), - node_subtensor_runtime::transaction_payment_wrapper::ChargeTransactionPaymentWrapper::::new( - pallet_transaction_payment::ChargeTransactionPayment::::from(0u64) - ), - pallet_subtensor::transaction_extension::SubtensorTransactionExtension::::new(), - pallet_drand::drand_priority::DrandPriority::::new(), - frame_metadata_hash_extension::CheckMetadataHash::::new(false), - ); + let extra: Extra = + ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckEra::::from(Era::Immortal), + node_subtensor_runtime::check_nonce::CheckNonce::::from(nonce).into(), + frame_system::CheckWeight::::new(), + node_subtensor_runtime::transaction_payment_wrapper::ChargeTransactionPaymentWrapper::< + runtime::Runtime, + >::new(pallet_transaction_payment::ChargeTransactionPayment::< + runtime::Runtime, + >::from(0u64)), + pallet_subtensor::transaction_extension::SubtensorTransactionExtension::< + runtime::Runtime, + >::new(), + pallet_drand::drand_priority::DrandPriority::::new(), + frame_metadata_hash_extension::CheckMetadataHash::::new(false), + ); type Implicit = >::Implicit; - let info = client.info(); + let info = client.info(); let genesis_h256: H256 = to_h256(info.genesis_hash); let implicit: Implicit = ( @@ -380,11 +394,8 @@ where ); // Build the exact signable payload. - let payload: SignedPayload = SignedPayload::from_raw( - call.clone(), - extra.clone(), - implicit.clone(), - ); + let payload: SignedPayload = + SignedPayload::from_raw(call.clone(), extra.clone(), implicit.clone()); let raw_payload = payload.encode(); @@ -400,20 +411,16 @@ where let who: AccountId32 = aura_pub.into(); let address = sp_runtime::MultiAddress::Id(who); - let uxt: UncheckedExtrinsic = UncheckedExtrinsic::new_signed( - call, - address, - signature, - extra, - ); + let uxt: UncheckedExtrinsic = UncheckedExtrinsic::new_signed(call, address, signature, extra); let xt_bytes = uxt.encode(); - let xt_hash = sp_core::hashing::blake2_256(&xt_bytes); + let xt_hash = sp_core::hashing::blake2_256(&xt_bytes); let opaque: sp_runtime::OpaqueExtrinsic = uxt.into(); let xt: ::Extrinsic = opaque.into(); - pool.submit_one(info.best_hash, TransactionSource::Local, xt).await?; + pool.submit_one(info.best_hash, TransactionSource::Local, xt) + .await?; log::debug!( target: "mev-shield", diff --git a/node/src/mev_shield/mod.rs b/node/src/mev_shield/mod.rs index 12db84dcd2..91817097bf 100644 --- a/node/src/mev_shield/mod.rs +++ b/node/src/mev_shield/mod.rs @@ -1,2 +1,2 @@ pub mod author; -pub mod proposer; \ No newline at end of file +pub mod proposer; diff --git a/node/src/mev_shield/proposer.rs b/node/src/mev_shield/proposer.rs index 663160dc5f..a0f61e1126 100644 --- a/node/src/mev_shield/proposer.rs +++ b/node/src/mev_shield/proposer.rs @@ -1,22 +1,17 @@ -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, - time::Duration, -}; +use super::author::ShieldContext; use futures::StreamExt; +use ml_kem::kem::{Decapsulate, DecapsulationKey}; +use ml_kem::{Ciphertext, Encoded, EncodedSizeUser, MlKem768, MlKem768Params}; use sc_service::SpawnTaskHandle; use sc_transaction_pool_api::{TransactionPool, TransactionSource}; use sp_core::H256; -use sp_runtime::{ - generic::Era, - MultiSignature, - OpaqueExtrinsic, - AccountId32, +use sp_runtime::{AccountId32, MultiSignature, OpaqueExtrinsic, generic::Era}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + time::Duration, }; use tokio::time::sleep; -use super::author::ShieldContext; -use ml_kem::{Ciphertext, Encoded, EncodedSizeUser, MlKem768, MlKem768Params}; -use ml_kem::kem::{Decapsulate, DecapsulationKey}; /// Buffer of wrappers per-slot. #[derive(Default, Clone)] @@ -24,21 +19,15 @@ struct WrapperBuffer { by_id: HashMap< H256, ( - Vec, // ciphertext blob - u64, // key_epoch - AccountId32, // wrapper author + Vec, // ciphertext blob + u64, // key_epoch + AccountId32, // wrapper author ), >, } impl WrapperBuffer { - fn upsert( - &mut self, - id: H256, - key_epoch: u64, - author: AccountId32, - ciphertext: Vec, - ) { + fn upsert(&mut self, id: H256, key_epoch: u64, author: AccountId32, ciphertext: Vec) { self.by_id.insert(id, (ciphertext, key_epoch, author)); } @@ -96,8 +85,8 @@ impl WrapperBuffer { pub fn spawn_revealer( task_spawner: &SpawnTaskHandle, client: Arc, - pool: Arc, - ctx: ShieldContext, + pool: Arc, + ctx: ShieldContext, ) where B: sp_runtime::traits::Block, C: sc_client_api::HeaderBackend @@ -111,7 +100,7 @@ pub fn spawn_revealer( use codec::{Decode, Encode}; use sp_runtime::traits::SaturatedConversion; - type Address = sp_runtime::MultiAddress; + type Address = sp_runtime::MultiAddress; type RUnchecked = node_subtensor_runtime::UncheckedExtrinsic; let buffer: Arc> = Arc::new(Mutex::new(WrapperBuffer::default())); @@ -249,9 +238,9 @@ pub fn spawn_revealer( // ── 2) last-3s revealer ───────────────────────────────────── { let client = Arc::clone(&client); - let pool = Arc::clone(&pool); + let pool = Arc::clone(&pool); let buffer = Arc::clone(&buffer); - let ctx = ctx.clone(); + let ctx = ctx.clone(); task_spawner.spawn( "mev-shield-last-3s-revealer", diff --git a/node/src/service.rs b/node/src/service.rs index d1f107928f..4149f093e3 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -28,18 +28,18 @@ use std::{cell::RefCell, path::Path}; use std::{sync::Arc, time::Duration}; use substrate_prometheus_endpoint::Registry; -use crate::mev_shield::{author, proposer}; use crate::cli::Sealing; use crate::client::{FullBackend, FullClient, HostFunctions, RuntimeExecutor}; use crate::ethereum::{ BackendType, EthConfiguration, FrontierBackend, FrontierPartialComponents, StorageOverride, StorageOverrideHandler, db_config_dir, new_frontier_partial, spawn_frontier_tasks, }; -use sp_core::twox_128; -use sc_client_api::StorageKey; +use crate::mev_shield::{author, proposer}; +use codec::Decode; use sc_client_api::HeaderBackend; +use sc_client_api::StorageKey; use sc_client_api::StorageProvider; -use codec::Decode; +use sp_core::twox_128; const LOG_TARGET: &str = "node-service"; @@ -540,13 +540,11 @@ where ) .await; - // ==== MEV-SHIELD HOOKS ==== + // ==== MEV-SHIELD HOOKS ==== let mut mev_timing: Option = None; if role.is_authority() { - let slot_duration_ms: u64 = consensus_mechanism - .slot_duration(&client)? - .as_millis() as u64; + let slot_duration_ms: u64 = consensus_mechanism.slot_duration(&client)?.as_millis() as u64; // Time windows (7s announce / last 3s decrypt). let timing = author::TimeParams { @@ -624,14 +622,16 @@ where .unwrap_or((slot_duration.as_millis() as u64, 3_000)); let guard_ms: u64 = 200; // small cushion so reveals hit the pool first - let after_decrypt_ms = slot_ms - .saturating_sub(decrypt_ms) - .saturating_add(guard_ms); + let after_decrypt_ms = slot_ms.saturating_sub(decrypt_ms).saturating_add(guard_ms); // Clamp into (0.5 .. 0.98] to give the proposer enough time let mut f = (after_decrypt_ms as f32) / (slot_ms as f32); - if f < 0.50 { f = 0.50; } - if f > 0.98 { f = 0.98; } + if f < 0.50 { + f = 0.50; + } + if f > 0.98 { + f = 0.98; + } f }; diff --git a/pallets/shield/src/lib.rs b/pallets/shield/src/lib.rs index 0d64e6f73c..36b6ad9c0f 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -6,6 +6,7 @@ pub use pallet::*; #[frame_support::pallet] pub mod pallet { use super::*; + use codec::Encode; use frame_support::{ dispatch::{GetDispatchInfo, PostDispatchInfo}, pallet_prelude::*, @@ -13,16 +14,14 @@ pub mod pallet { weights::Weight, }; use frame_system::pallet_prelude::*; + use sp_consensus_aura::sr25519::AuthorityId as AuraAuthorityId; + use sp_core::ByteArray; use sp_runtime::{ - AccountId32, MultiSignature, RuntimeDebug, - traits::{BadOrigin, Dispatchable, Hash, Verify, Zero, SaturatedConversion}, - DispatchErrorWithPostInfo, + AccountId32, DispatchErrorWithPostInfo, MultiSignature, RuntimeDebug, + traits::{BadOrigin, Dispatchable, Hash, SaturatedConversion, Verify, Zero}, }; use sp_std::{marker::PhantomData, prelude::*}; use subtensor_macros::freeze_struct; - use sp_consensus_aura::sr25519::AuthorityId as AuraAuthorityId; - use sp_core::ByteArray; - use codec::Encode; /// Origin helper: ensure the signer is an Aura authority (no session/authorship). pub struct EnsureAuraAuthority(PhantomData); @@ -93,9 +92,8 @@ pub mod pallet { type RuntimeCall: Parameter + sp_runtime::traits::Dispatchable< RuntimeOrigin = Self::RuntimeOrigin, - PostInfo = PostDispatchInfo - > - + GetDispatchInfo; + PostInfo = PostDispatchInfo, + > + GetDispatchInfo; type AuthorityOrigin: AuthorityOriginExt; @@ -140,11 +138,18 @@ pub mod pallet { #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// Encrypted wrapper accepted. - EncryptedSubmitted { id: T::Hash, who: T::AccountId, epoch: u64 }, + EncryptedSubmitted { + id: T::Hash, + who: T::AccountId, + epoch: u64, + }, /// Decrypted call executed. DecryptedExecuted { id: T::Hash, signer: T::AccountId }, /// Decrypted execution rejected. - DecryptedRejected { id: T::Hash, reason: DispatchErrorWithPostInfo }, + DecryptedRejected { + id: T::Hash, + reason: DispatchErrorWithPostInfo, + }, } #[pallet::error] @@ -191,9 +196,15 @@ pub mod pallet { T::AuthorityOrigin::ensure_validator(origin)?; const MAX_KYBER768_PK_LENGTH: usize = 1184; - ensure!(public_key.len() == MAX_KYBER768_PK_LENGTH, Error::::BadPublicKeyLen); + ensure!( + public_key.len() == MAX_KYBER768_PK_LENGTH, + Error::::BadPublicKeyLen + ); - NextKey::::put(EphemeralPubKey { public_key: public_key.clone(), epoch }); + NextKey::::put(EphemeralPubKey { + public_key: public_key.clone(), + epoch, + }); Ok(()) } @@ -277,12 +288,8 @@ pub mod pallet { return Err(Error::::MissingSubmission.into()); }; - let payload_bytes = Self::build_raw_payload_bytes( - &signer, - nonce, - &mortality, - call.as_ref(), - ); + let payload_bytes = + Self::build_raw_payload_bytes(&signer, nonce, &mortality, call.as_ref()); // 1) Commitment check against on-chain stored commitment. let recomputed: T::Hash = T::Hashing::hash(&payload_bytes); @@ -317,10 +324,7 @@ pub mod pallet { match res { Ok(post) => { let actual = post.actual_weight.unwrap_or(required); - Self::deposit_event(Event::DecryptedExecuted { - id, - signer, - }); + Self::deposit_event(Event::DecryptedExecuted { id, signer }); Ok(PostDispatchInfo { actual_weight: Some(actual), pays_fee: Pays::No, @@ -337,7 +341,6 @@ pub mod pallet { } } - impl Pallet { /// Build the raw payload bytes used for both: /// - `commitment = blake2_256(raw_payload)` @@ -380,18 +383,15 @@ pub mod pallet { _source: sp_runtime::transaction_validity::TransactionSource, call: &Self::Call, ) -> sp_runtime::transaction_validity::TransactionValidity { - use sp_runtime::transaction_validity::{ - InvalidTransaction, - ValidTransaction, - }; + use sp_runtime::transaction_validity::{InvalidTransaction, ValidTransaction}; match call { Call::execute_revealed { id, .. } => { ValidTransaction::with_tag_prefix("mev-shield-exec") .priority(u64::MAX) - .longevity(64) // High because of propagate(false) - .and_provides(id) // dedupe by wrapper id - .propagate(false) // CRITICAL: no gossip, stays on author only + .longevity(64) // High because of propagate(false) + .and_provides(id) // dedupe by wrapper id + .propagate(false) // CRITICAL: no gossip, stays on author only .build() } From 27bc7980190e941e1ee8a185b424cfb49f81d607 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:53:26 -0800 Subject: [PATCH 09/30] fix localnet script --- runtime/src/lib.rs | 2 +- scripts/localnet.sh | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 58b69ffcb0..ca2422f9b8 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -28,8 +28,8 @@ use frame_support::{ use frame_system::{EnsureRoot, EnsureRootWithSuccess, EnsureSigned}; use pallet_commitments::{CanCommit, OnMetadataCommitment}; use pallet_grandpa::{AuthorityId as GrandpaId, fg_primitives}; -pub use pallet_shield; use pallet_registry::CanRegisterIdentity; +pub use pallet_shield; use pallet_subtensor::rpc_info::{ delegate_info::DelegateInfo, dynamic_info::DynamicInfo, diff --git a/scripts/localnet.sh b/scripts/localnet.sh index d97a3e65ca..1b96baa19b 100755 --- a/scripts/localnet.sh +++ b/scripts/localnet.sh @@ -139,8 +139,6 @@ if [ $BUILD_ONLY -eq 0 ]; then trap 'pkill -P $$' EXIT SIGINT SIGTERM ( - # env MEV_SHIELD_ANNOUNCE_ACCOUNT_SEED='//Alice' RUST_LOG="${RUST_LOG:-info,mev-shield=debug}" "${alice_start[@]}" 2>&1 & - # env MEV_SHIELD_ANNOUNCE_ACCOUNT_SEED='//Bob' RUST_LOG="${RUST_LOG:-info,mev-shield=debug}" "${bob_start[@]}" 2>&1 ("${alice_start[@]}" 2>&1) & ("${bob_start[@]}" 2>&1) wait From 6bf507261fcd6d37f2ddd26b0362ea9ea343c141 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:57:03 -0800 Subject: [PATCH 10/30] remove unused params --- node/src/mev_shield/proposer.rs | 26 +++----------------------- pallets/shield/src/lib.rs | 33 +++++---------------------------- runtime/src/lib.rs | 5 ----- 3 files changed, 8 insertions(+), 56 deletions(-) diff --git a/node/src/mev_shield/proposer.rs b/node/src/mev_shield/proposer.rs index a0f61e1126..cbf4e6b03c 100644 --- a/node/src/mev_shield/proposer.rs +++ b/node/src/mev_shield/proposer.rs @@ -541,7 +541,7 @@ pub fn spawn_revealer( type RuntimeNonce = ::Nonce; // Safely parse plaintext layout without panics. - // Layout: signer (32) || nonce (4) || mortality (1) || call (..) + // Layout: signer (32) || nonce (4) || call (..) // || sig_kind (1) || sig (64) let min_plain_len: usize = 32usize .saturating_add(4) @@ -582,20 +582,8 @@ pub fn spawn_revealer( } }; - let mortality_byte = match plaintext.get(36) { - Some(b) => *b, - None => { - log::debug!( - target: "mev-shield", - " id=0x{}: missing mortality byte", - hex::encode(id.as_bytes()) - ); - continue; - } - }; - let sig_off = match plaintext.len().checked_sub(65) { - Some(off) if off >= 37 => off, + Some(off) if off >= 36 => off, _ => { log::debug!( target: "mev-shield", @@ -606,7 +594,7 @@ pub fn spawn_revealer( } }; - let call_bytes = match plaintext.get(37..sig_off) { + let call_bytes = match plaintext.get(36..sig_off) { Some(s) => s, None => { log::debug!( @@ -681,13 +669,6 @@ pub fn spawn_revealer( let raw_nonce_u32 = u32::from_le_bytes(nonce_array); let account_nonce: RuntimeNonce = raw_nonce_u32.saturated_into(); - // Mortality currently only supports immortal; we still - // parse the byte to keep layout consistent. - let _mortality = match mortality_byte { - 0 => Era::Immortal, - _ => Era::Immortal, - }; - let inner_call: node_subtensor_runtime::RuntimeCall = match Decode::decode(&mut &call_bytes[..]) { Ok(c) => c, @@ -733,7 +714,6 @@ pub fn spawn_revealer( id, signer: signer.clone(), nonce: account_nonce, - mortality: Era::Immortal, call: Box::new(inner_call), signature, } diff --git a/pallets/shield/src/lib.rs b/pallets/shield/src/lib.rs index 36b6ad9c0f..0d7145dbf5 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -10,7 +10,7 @@ pub mod pallet { use frame_support::{ dispatch::{GetDispatchInfo, PostDispatchInfo}, pallet_prelude::*, - traits::{ConstU32, Currency}, + traits::ConstU32, weights::Weight, }; use frame_system::pallet_prelude::*; @@ -60,14 +60,13 @@ pub mod pallet { // ----------------- Types ----------------- /// AEAD‑independent commitment over the revealed payload. - #[freeze_struct("6c00690caddfeb78")] + #[freeze_struct("b307ebc1f8eae75")] #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct Submission { pub author: AccountId, pub key_epoch: u64, pub commitment: Hash, pub ciphertext: BoundedVec>, - pub payload_version: u16, pub submitted_in: BlockNumber, pub submitted_at: Moment, pub max_weight: Weight, @@ -96,17 +95,6 @@ pub mod pallet { > + GetDispatchInfo; type AuthorityOrigin: AuthorityOriginExt; - - #[pallet::constant] - type SlotMs: Get; - #[pallet::constant] - type AnnounceAtMs: Get; - #[pallet::constant] - type GraceMs: Get; - #[pallet::constant] - type DecryptWindowMs: Get; - - type Currency: Currency; } #[pallet::pallet] @@ -215,7 +203,7 @@ pub mod pallet { /// /// ```text /// raw_payload = - /// signer (32B) || nonce (u32 LE) || mortality_byte || SCALE(call) + /// signer (32B) || nonce (u32 LE) || SCALE(call) /// commitment = blake2_256(raw_payload) /// ``` /// @@ -232,7 +220,6 @@ pub mod pallet { key_epoch: u64, commitment: T::Hash, ciphertext: BoundedVec>, - payload_version: u16, max_weight: Weight, ) -> DispatchResult { let who = ensure_signed(origin)?; @@ -248,7 +235,6 @@ pub mod pallet { key_epoch, commitment, ciphertext, - payload_version, submitted_in: >::block_number(), submitted_at: now, max_weight, @@ -278,7 +264,6 @@ pub mod pallet { id: T::Hash, signer: T::AccountId, nonce: T::Nonce, - mortality: sp_runtime::generic::Era, call: Box<::RuntimeCall>, signature: MultiSignature, ) -> DispatchResultWithPostInfo { @@ -289,7 +274,7 @@ pub mod pallet { }; let payload_bytes = - Self::build_raw_payload_bytes(&signer, nonce, &mortality, call.as_ref()); + Self::build_raw_payload_bytes(&signer, nonce, call.as_ref()); // 1) Commitment check against on-chain stored commitment. let recomputed: T::Hash = T::Hashing::hash(&payload_bytes); @@ -347,11 +332,10 @@ pub mod pallet { /// - signature message (after domain separation). /// /// Layout: - /// signer (32B) || nonce (u32 LE) || mortality_byte || SCALE(call) + /// signer (32B) || nonce (u32 LE) || SCALE(call) fn build_raw_payload_bytes( signer: &T::AccountId, nonce: T::Nonce, - mortality: &sp_runtime::generic::Era, call: &::RuntimeCall, ) -> Vec { let mut out = Vec::new(); @@ -361,13 +345,6 @@ pub mod pallet { let n_u32: u32 = nonce.saturated_into(); out.extend_from_slice(&n_u32.to_le_bytes()); - // Simple 1-byte mortality code to match the off-chain layout. - let m_byte: u8 = match mortality { - sp_runtime::generic::Era::Immortal => 0, - _ => 1, - }; - out.push(m_byte); - // Append SCALE-encoded call. out.extend(call.encode()); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index ca2422f9b8..39f74989e6 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -122,11 +122,6 @@ impl frame_system::offchain::SigningTypes for Runtime { impl pallet_shield::Config for Runtime { type RuntimeCall = RuntimeCall; - type SlotMs = ShieldSlotMs; - type AnnounceAtMs = ShieldAnnounceAtMs; - type GraceMs = ShieldGraceMs; - type DecryptWindowMs = ShieldDecryptWindowMs; - type Currency = Balances; type AuthorityOrigin = pallet_shield::EnsureAuraAuthority; } From 1911158728dae494ebb2259578ebb66438f26c4f Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:57:21 -0800 Subject: [PATCH 11/30] add weights --- pallets/subtensor/src/macros/dispatches.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index d534dbb0c6..e44e09bbe2 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2362,7 +2362,9 @@ mod dispatches { /// #[pallet::call_index(122)] #[pallet::weight(( - Weight::from_parts(19_420_000, 0).saturating_add(T::DbWeight::get().writes(4_u64)), + Weight::from_parts(19_420_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)), DispatchClass::Normal, Pays::Yes ))] @@ -2381,7 +2383,9 @@ mod dispatches { /// --- Sets root claim number (sudo extrinsic). Zero disables auto-claim. #[pallet::call_index(123)] #[pallet::weight(( - Weight::from_parts(4_000_000, 0).saturating_add(T::DbWeight::get().writes(1_u64)), + Weight::from_parts(4_000_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)), DispatchClass::Operational, Pays::Yes ))] @@ -2401,7 +2405,9 @@ mod dispatches { /// --- Sets root claim threshold for subnet (sudo or owner origin). #[pallet::call_index(124)] #[pallet::weight(( - Weight::from_parts(5_711_000, 0).saturating_add(T::DbWeight::get().writes(1_u64)), + Weight::from_parts(5_711_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)), DispatchClass::Operational, Pays::Yes ))] From db695a31a03380285f718e73d5ddee2e78369804 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:20:05 -0800 Subject: [PATCH 12/30] restrict execute_revealed source to local/inblock --- node/src/mev_shield/proposer.rs | 2 +- pallets/shield/src/lib.rs | 28 +++++++++++++++++++--------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/node/src/mev_shield/proposer.rs b/node/src/mev_shield/proposer.rs index cbf4e6b03c..215afeed24 100644 --- a/node/src/mev_shield/proposer.rs +++ b/node/src/mev_shield/proposer.rs @@ -5,7 +5,7 @@ use ml_kem::{Ciphertext, Encoded, EncodedSizeUser, MlKem768, MlKem768Params}; use sc_service::SpawnTaskHandle; use sc_transaction_pool_api::{TransactionPool, TransactionSource}; use sp_core::H256; -use sp_runtime::{AccountId32, MultiSignature, OpaqueExtrinsic, generic::Era}; +use sp_runtime::{AccountId32, MultiSignature, OpaqueExtrinsic}; use std::{ collections::HashMap, sync::{Arc, Mutex}, diff --git a/pallets/shield/src/lib.rs b/pallets/shield/src/lib.rs index 0d7145dbf5..292348a43d 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -22,6 +22,11 @@ pub mod pallet { }; use sp_std::{marker::PhantomData, prelude::*}; use subtensor_macros::freeze_struct; + use sp_runtime::transaction_validity::{ + InvalidTransaction, + TransactionSource, + ValidTransaction, + }; /// Origin helper: ensure the signer is an Aura authority (no session/authorship). pub struct EnsureAuraAuthority(PhantomData); @@ -357,19 +362,24 @@ pub mod pallet { type Call = Call; fn validate_unsigned( - _source: sp_runtime::transaction_validity::TransactionSource, + source: TransactionSource, call: &Self::Call, - ) -> sp_runtime::transaction_validity::TransactionValidity { - use sp_runtime::transaction_validity::{InvalidTransaction, ValidTransaction}; + ) -> TransactionValidity { match call { Call::execute_revealed { id, .. } => { - ValidTransaction::with_tag_prefix("mev-shield-exec") - .priority(u64::MAX) - .longevity(64) // High because of propagate(false) - .and_provides(id) // dedupe by wrapper id - .propagate(false) // CRITICAL: no gossip, stays on author only - .build() + match source { + // Only allow locally-submitted / already-in-block txs. + TransactionSource::Local | TransactionSource::InBlock => { + ValidTransaction::with_tag_prefix("mev-shield-exec") + .priority(u64::MAX) + .longevity(64) // long because propagate(false) + .and_provides(id) // dedupe by wrapper id + .propagate(false) // CRITICAL: no gossip, stays on author node + .build() + } + _ => InvalidTransaction::Call.into(), + } } _ => InvalidTransaction::Call.into(), From 1db33f827b38ed8c46353be1e1da0ebc573dbc89 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:52:55 -0800 Subject: [PATCH 13/30] remove more unused --- pallets/shield/src/lib.rs | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/pallets/shield/src/lib.rs b/pallets/shield/src/lib.rs index 292348a43d..241879dc83 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -65,16 +65,14 @@ pub mod pallet { // ----------------- Types ----------------- /// AEAD‑independent commitment over the revealed payload. - #[freeze_struct("b307ebc1f8eae75")] + #[freeze_struct("1eb29aa303f42c46")] #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] - pub struct Submission { + pub struct Submission { pub author: AccountId, pub key_epoch: u64, pub commitment: Hash, pub ciphertext: BoundedVec>, pub submitted_in: BlockNumber, - pub submitted_at: Moment, - pub max_weight: Weight, } /// Ephemeral key fingerprint used by off-chain code to verify the ML‑KEM pubkey. @@ -121,7 +119,7 @@ pub mod pallet { _, Blake2_128Concat, T::Hash, - Submission, T::Moment, T::Hash>, + Submission, T::Hash>, OptionQuery, >; @@ -152,7 +150,6 @@ pub mod pallet { MissingSubmission, CommitmentMismatch, SignatureInvalid, - WeightTooHigh, NonceMismatch, BadPublicKeyLen, } @@ -225,7 +222,6 @@ pub mod pallet { key_epoch: u64, commitment: T::Hash, ciphertext: BoundedVec>, - max_weight: Weight, ) -> DispatchResult { let who = ensure_signed(origin)?; ensure!( @@ -233,16 +229,13 @@ pub mod pallet { Error::::BadEpoch ); - let now = pallet_timestamp::Pallet::::get(); let id: T::Hash = T::Hashing::hash_of(&(who.clone(), commitment, &ciphertext)); - let sub = Submission::, T::Moment, T::Hash> { + let sub = Submission::, T::Hash> { author: who.clone(), key_epoch, commitment, ciphertext, submitted_in: >::block_number(), - submitted_at: now, - max_weight, }; ensure!( !Submissions::::contains_key(id), @@ -300,14 +293,10 @@ pub mod pallet { ensure!(acc == nonce, Error::::NonceMismatch); frame_system::Pallet::::inc_account_nonce(&signer); - // 4) Dispatch inner call from signer; enforce max_weight guard. + // 4) Dispatch inner call from signer. let info = call.get_dispatch_info(); let required = info.call_weight.saturating_add(info.extension_weight); - let leq = required.ref_time() <= sub.max_weight.ref_time() - && required.proof_size() <= sub.max_weight.proof_size(); - ensure!(leq, Error::::WeightTooHigh); - let origin_signed = frame_system::RawOrigin::Signed(signer.clone()).into(); let res = (*call).dispatch(origin_signed); From 754d508747cb987ffff1f348bd4d4408dfaf0904 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:50:18 -0800 Subject: [PATCH 14/30] remove key_epoch param --- node/src/mev_shield/proposer.rs | 135 ++++++++++++++++++++------------ pallets/shield/src/lib.rs | 13 +-- 2 files changed, 85 insertions(+), 63 deletions(-) diff --git a/node/src/mev_shield/proposer.rs b/node/src/mev_shield/proposer.rs index 215afeed24..97a6f2c566 100644 --- a/node/src/mev_shield/proposer.rs +++ b/node/src/mev_shield/proposer.rs @@ -109,6 +109,8 @@ pub fn spawn_revealer( { let client = Arc::clone(&client); let buffer = Arc::clone(&buffer); + let ctx_for_buffer = ctx.clone(); + task_spawner.spawn( "mev-shield-buffer-wrappers", None, @@ -161,14 +163,17 @@ pub fn spawn_revealer( let author_opt: Option = match &uxt.0.preamble { - sp_runtime::generic::Preamble::Signed(addr, _sig, _ext) => { - match addr.clone() { - Address::Id(acc) => Some(acc), - Address::Address32(bytes) => - Some(sp_runtime::AccountId32::new(bytes)), - _ => None, + sp_runtime::generic::Preamble::Signed( + addr, + _sig, + _ext, + ) => match addr.clone() { + Address::Id(acc) => Some(acc), + Address::Address32(bytes) => { + Some(sp_runtime::AccountId32::new(bytes)) } - } + _ => None, + }, _ => None, }; @@ -182,14 +187,35 @@ pub fn spawn_revealer( if let node_subtensor_runtime::RuntimeCall::MevShield( pallet_shield::Call::submit_encrypted { - key_epoch, commitment, ciphertext, - .. - } + }, ) = &uxt.0.function { - let payload = (author.clone(), *commitment, ciphertext).encode(); + // Derive the key_epoch for this wrapper from the current + // ShieldContext epoch at *buffer* time. + let key_epoch_opt = match ctx_for_buffer.keys.lock() { + Ok(k) => Some(k.epoch), + Err(e) => { + log::debug!( + target: "mev-shield", + " [xt #{idx}] failed to lock ShieldKeys in buffer task: {:?}", + e + ); + None + } + }; + + let Some(key_epoch) = key_epoch_opt else { + log::debug!( + target: "mev-shield", + " [xt #{idx}] skipping wrapper due to missing epoch snapshot" + ); + continue; + }; + + let payload = + (author.clone(), *commitment, ciphertext).encode(); let id = H256(sp_core::hashing::blake2_256(&payload)); log::debug!( @@ -205,7 +231,7 @@ pub fn spawn_revealer( if let Ok(mut buf) = buffer.lock() { buf.upsert( id, - *key_epoch, + key_epoch, author, ciphertext.to_vec(), ); @@ -260,26 +286,24 @@ pub fn spawn_revealer( sleep(Duration::from_millis(tail)).await; // Snapshot the current ML‑KEM secret and epoch - let snapshot_opt = { - match ctx.keys.lock() { - Ok(k) => { - let sk_hash = sp_core::hashing::blake2_256(&k.current_sk); - Some(( - k.current_sk.clone(), - k.epoch, - k.current_pk.len(), - k.next_pk.len(), - sk_hash, - )) - } - Err(e) => { - log::debug!( - target: "mev-shield", - "revealer: failed to lock ShieldKeys (poisoned?): {:?}", - e - ); - None - } + let snapshot_opt = match ctx.keys.lock() { + Ok(k) => { + let sk_hash = sp_core::hashing::blake2_256(&k.current_sk); + Some(( + k.current_sk.clone(), + k.epoch, + k.current_pk.len(), + k.next_pk.len(), + sk_hash, + )) + } + Err(e) => { + log::debug!( + target: "mev-shield", + "revealer: failed to lock ShieldKeys (poisoned?): {:?}", + e + ); + None } }; @@ -304,7 +328,7 @@ pub fn spawn_revealer( ); // Only process wrappers whose key_epoch == curr_epoch. - let drained: Vec<(H256, u64, sp_runtime::AccountId32, Vec)> = { + let drained: Vec<(H256, u64, sp_runtime::AccountId32, Vec)> = match buffer.lock() { Ok(mut buf) => buf.drain_for_epoch(curr_epoch), Err(e) => { @@ -315,8 +339,7 @@ pub fn spawn_revealer( ); Vec::new() } - } - }; + }; log::debug!( target: "mev-shield", @@ -432,19 +455,22 @@ pub fn spawn_revealer( ); // Rebuild DecapsulationKey and decapsulate. - let enc_sk = match Encoded::>::try_from(&curr_sk_bytes[..]) { - Ok(e) => e, - Err(e) => { - log::debug!( - target: "mev-shield", - " id=0x{}: DecapsulationKey::try_from(sk_bytes) failed (len={}, err={:?})", - hex::encode(id.as_bytes()), - curr_sk_bytes.len(), - e - ); - continue; - } - }; + let enc_sk = + match Encoded::>::try_from( + &curr_sk_bytes[..], + ) { + Ok(e) => e, + Err(e) => { + log::debug!( + target: "mev-shield", + " id=0x{}: DecapsulationKey::try_from(sk_bytes) failed (len={}, err={:?})", + hex::encode(id.as_bytes()), + curr_sk_bytes.len(), + e + ); + continue; + } + }; let sk = DecapsulationKey::::from_bytes(&enc_sk); let ct = match Ciphertext::::try_from(kem_ct_bytes) { @@ -538,7 +564,8 @@ pub fn spawn_revealer( plaintext.len() ); - type RuntimeNonce = ::Nonce; + type RuntimeNonce = + ::Nonce; // Safely parse plaintext layout without panics. // Layout: signer (32) || nonce (4) || call (..) @@ -716,7 +743,7 @@ pub fn spawn_revealer( nonce: account_nonce, call: Box::new(inner_call), signature, - } + }, ); to_submit.push((id, reveal)); @@ -745,9 +772,13 @@ pub fn spawn_revealer( match OpaqueExtrinsic::from_bytes(&xt_bytes) { Ok(opaque) => { - match pool.submit_one(at, TransactionSource::Local, opaque).await { + match pool + .submit_one(at, TransactionSource::Local, opaque) + .await + { Ok(_) => { - let xt_hash = sp_core::hashing::blake2_256(&xt_bytes); + let xt_hash = + sp_core::hashing::blake2_256(&xt_bytes); log::debug!( target: "mev-shield", " id=0x{}: submit_one(execute_revealed) OK, xt_hash=0x{}", diff --git a/pallets/shield/src/lib.rs b/pallets/shield/src/lib.rs index 241879dc83..da9ea437c7 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -65,11 +65,10 @@ pub mod pallet { // ----------------- Types ----------------- /// AEAD‑independent commitment over the revealed payload. - #[freeze_struct("1eb29aa303f42c46")] + #[freeze_struct("66e393c88124f360")] #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct Submission { pub author: AccountId, - pub key_epoch: u64, pub commitment: Hash, pub ciphertext: BoundedVec>, pub submitted_in: BlockNumber, @@ -132,7 +131,6 @@ pub mod pallet { EncryptedSubmitted { id: T::Hash, who: T::AccountId, - epoch: u64, }, /// Decrypted call executed. DecryptedExecuted { id: T::Hash, signer: T::AccountId }, @@ -199,7 +197,7 @@ pub mod pallet { Ok(()) } - /// Users submit encrypted wrapper paying the normal fee. + /// Users submit encrypted wrapper. /// /// Commitment semantics: /// @@ -219,20 +217,14 @@ pub mod pallet { })] pub fn submit_encrypted( origin: OriginFor, - key_epoch: u64, commitment: T::Hash, ciphertext: BoundedVec>, ) -> DispatchResult { let who = ensure_signed(origin)?; - ensure!( - key_epoch == Epoch::::get() || key_epoch + 1 == Epoch::::get(), - Error::::BadEpoch - ); let id: T::Hash = T::Hashing::hash_of(&(who.clone(), commitment, &ciphertext)); let sub = Submission::, T::Hash> { author: who.clone(), - key_epoch, commitment, ciphertext, submitted_in: >::block_number(), @@ -245,7 +237,6 @@ pub mod pallet { Self::deposit_event(Event::EncryptedSubmitted { id, who, - epoch: key_epoch, }); Ok(()) } From 0b64fc1c1d22c3f0e5d3c26ab0685b906a85862a Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:58:29 -0800 Subject: [PATCH 15/30] use Pays::Yes --- pallets/shield/src/lib.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pallets/shield/src/lib.rs b/pallets/shield/src/lib.rs index da9ea437c7..6d802ba05c 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -209,12 +209,12 @@ pub mod pallet { /// /// Ciphertext format: `[u16 kem_len][kem_ct][nonce24][aead_ct]` #[pallet::call_index(1)] - #[pallet::weight({ + #[pallet::weight(({ let w = Weight::from_parts(ciphertext.len() as u64, 0) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)); w - })] + }, DispatchClass::Normal, Pays::Yes))] pub fn submit_encrypted( origin: OriginFor, commitment: T::Hash, @@ -243,11 +243,9 @@ pub mod pallet { /// Executed by the block author. #[pallet::call_index(2)] - #[pallet::weight( - Weight::from_parts(10_000, 0) - .saturating_add(T::DbWeight::get().reads(3_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) - )] + #[pallet::weight(Weight::from_parts(10_000, 0) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)))] pub fn execute_revealed( origin: OriginFor, id: T::Hash, From 8036d0a913a53ea9b86188597d594a23c81625d3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 19 Nov 2025 20:59:58 +0000 Subject: [PATCH 16/30] auto-update benchmark weights --- pallets/subtensor/src/macros/dispatches.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index e44e09bbe2..912866463e 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2363,7 +2363,7 @@ mod dispatches { #[pallet::call_index(122)] #[pallet::weight(( Weight::from_parts(19_420_000, 0) - .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)), DispatchClass::Normal, Pays::Yes @@ -2384,7 +2384,7 @@ mod dispatches { #[pallet::call_index(123)] #[pallet::weight(( Weight::from_parts(4_000_000, 0) - .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().reads(0_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)), DispatchClass::Operational, Pays::Yes @@ -2406,7 +2406,7 @@ mod dispatches { #[pallet::call_index(124)] #[pallet::weight(( Weight::from_parts(5_711_000, 0) - .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().reads(0_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)), DispatchClass::Operational, Pays::Yes From b177620c31cde3daf2803b8a3300b0c263ac748e Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:09:22 -0800 Subject: [PATCH 17/30] add benchmarks for pallet-shield --- pallets/shield/src/benchmarking.rs | 213 +++++++++++++++++++++++++++++ pallets/shield/src/lib.rs | 53 +++---- runtime/Cargo.toml | 3 + runtime/src/lib.rs | 1 + scripts/benchmark_action.sh | 3 +- scripts/benchmark_all.sh | 1 + 6 files changed, 241 insertions(+), 33 deletions(-) create mode 100644 pallets/shield/src/benchmarking.rs diff --git a/pallets/shield/src/benchmarking.rs b/pallets/shield/src/benchmarking.rs new file mode 100644 index 0000000000..a6be39ab59 --- /dev/null +++ b/pallets/shield/src/benchmarking.rs @@ -0,0 +1,213 @@ +//! Benchmarking for pallet-mev-shield. +#![cfg(feature = "runtime-benchmarks")] + +use super::*; + +use codec::Encode; +use frame_benchmarking::v2::*; +use frame_system::RawOrigin; + +use frame_support::{BoundedVec, pallet_prelude::ConstU32}; +use frame_system::pallet_prelude::BlockNumberFor; + +use sp_core::crypto::KeyTypeId; +use sp_core::sr25519; +use sp_io::crypto::{sr25519_generate, sr25519_sign}; + +use sp_runtime::{ + AccountId32, MultiSignature, + traits::{Hash as HashT, SaturatedConversion, Zero}, +}; + +use sp_std::{boxed::Box, vec, vec::Vec}; + +/// Helper to build bounded bytes (public key) of a given length. +fn bounded_pk(len: usize) -> BoundedVec> { + let v = vec![7u8; len]; + BoundedVec::>::try_from(v).expect("within bound; qed") +} + +/// Helper to build bounded bytes (ciphertext) of a given length. +fn bounded_ct(len: usize) -> BoundedVec> { + let v = vec![0u8; len]; + BoundedVec::>::try_from(v).expect("within bound; qed") +} + +/// Build the raw payload bytes used by `commitment` & signature verification in the pallet. +/// Layout: signer (32B) || nonce (u32 LE) || SCALE(call) +fn build_payload_bytes( + signer: &T::AccountId, + nonce: ::Nonce, + call: &::RuntimeCall, +) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(signer.as_ref()); + + // canonicalize nonce to u32 LE + let n_u32: u32 = nonce.saturated_into(); + out.extend_from_slice(&n_u32.to_le_bytes()); + + // append SCALE-encoded call + out.extend(call.encode()); + out +} + +/// Seed Aura authorities so `EnsureAuraAuthority` passes for a given sr25519 pubkey. +/// +/// We avoid requiring `ByteArray` on `AuthorityId` by relying on: +/// `::AuthorityId: From`. +fn seed_aura_authority_from_sr25519(pubkey: &sr25519::Public) +where + T: pallet::Config + pallet_aura::Config, + ::AuthorityId: From, +{ + let auth_id: ::AuthorityId = (*pubkey).into(); + pallet_aura::Authorities::::mutate(|auths| { + let _ = auths.try_push(auth_id); + }); +} + +#[benchmarks( + where + // Needed to build a concrete inner call and convert into T::RuntimeCall. + ::RuntimeCall: From>, + // Needed so we can seed Authorities from a dev sr25519 pubkey. + ::AuthorityId: From, +)] +mod benches { + use super::*; + + /// Benchmark `announce_next_key`. + #[benchmark] + fn announce_next_key() { + // Generate a deterministic dev key in the host keystore (for benchmarks). + // Any 4-byte KeyTypeId works for generation; it does not affect AccountId derivation. + const KT: KeyTypeId = KeyTypeId(*b"benc"); + let alice_pub: sr25519::Public = sr25519_generate(KT, Some("//Alice".as_bytes().to_vec())); + let alice_acc: AccountId32 = alice_pub.into(); + + // Make this account an Aura authority for the generic runtime. + seed_aura_authority_from_sr25519::(&alice_pub); + + // Valid Kyber768 public key length per pallet check. + const KYBER768_PK_LEN: usize = 1184; + let public_key: BoundedVec> = bounded_pk::<2048>(KYBER768_PK_LEN); + let epoch: u64 = 42; + + // Measure: dispatch the extrinsic. + #[extrinsic_call] + announce_next_key( + RawOrigin::Signed(alice_acc.clone()), + public_key.clone(), + epoch, + ); + + // Assert: NextKey should be set exactly. + let stored = NextKey::::get().expect("must be set by announce_next_key"); + assert_eq!(stored.epoch, epoch); + assert_eq!(stored.public_key.as_slice(), public_key.as_slice()); + } + + /// Benchmark `submit_encrypted`. + #[benchmark] + fn submit_encrypted() { + // Any whitelisted caller is fine (no authority requirement). + let who: T::AccountId = whitelisted_caller(); + + // Dummy commitment and ciphertext (bounded to 8192). + let commitment: T::Hash = ::Hashing::hash(b"bench-commitment"); + const CT_DEFAULT_LEN: usize = 256; + let ciphertext: BoundedVec> = super::bounded_ct::<8192>(CT_DEFAULT_LEN); + + // Pre-compute expected id to assert postconditions. + let id: T::Hash = + ::Hashing::hash_of(&(who.clone(), commitment, &ciphertext)); + + // Measure: dispatch the extrinsic. + #[extrinsic_call] + submit_encrypted( + RawOrigin::Signed(who.clone()), + commitment, + ciphertext.clone(), + ); + + // Assert: stored under expected id. + let got = Submissions::::get(id).expect("submission must exist"); + assert_eq!(got.author, who); + assert_eq!( + got.commitment, + ::Hashing::hash(b"bench-commitment") + ); + assert_eq!(got.ciphertext.as_slice(), ciphertext.as_slice()); + } + + /// Benchmark `execute_revealed`. + #[benchmark] + fn execute_revealed() { + // Generate a dev sr25519 key in the host keystore and derive the account. + const KT: KeyTypeId = KeyTypeId(*b"benc"); + let signer_pub: sr25519::Public = sr25519_generate(KT, Some("//Alice".as_bytes().to_vec())); + let signer: AccountId32 = signer_pub.into(); + + // Inner call that will be executed as the signer (cheap & always available). + let inner_call: ::RuntimeCall = frame_system::Call::::remark { + remark: vec![1, 2, 3], + } + .into(); + + // Nonce must match current system nonce (fresh account => 0). + let nonce: ::Nonce = 0u32.into(); + + // Build payload and commitment exactly how the pallet expects. + let payload_bytes = super::build_payload_bytes::(&signer, nonce, &inner_call); + let commitment: ::Hash = + ::Hashing::hash(payload_bytes.as_slice()); + + // Ciphertext is stored in the submission but not used by `execute_revealed`; keep small. + const CT_DEFAULT_LEN: usize = 64; + let ciphertext: BoundedVec> = super::bounded_ct::<8192>(CT_DEFAULT_LEN); + + // The submission `id` must match pallet's hashing scheme in submit_encrypted. + let id: ::Hash = ::Hashing::hash_of( + &(signer.clone(), commitment, &ciphertext), + ); + + // Seed the Submissions map with the expected entry. + let sub = Submission::, ::Hash> { + author: signer.clone(), + commitment, + ciphertext: ciphertext.clone(), + submitted_in: frame_system::Pallet::::block_number(), + }; + Submissions::::insert(id, sub); + + // Domain-separated signing as in pallet: "mev-shield:v1" || genesis_hash || payload + let zero: BlockNumberFor = Zero::zero(); + let genesis = frame_system::Pallet::::block_hash(zero); + let mut msg = b"mev-shield:v1".to_vec(); + msg.extend_from_slice(genesis.as_ref()); + msg.extend_from_slice(&payload_bytes); + + // Sign using the host keystore and wrap into MultiSignature. + let sig = sr25519_sign(KT, &signer_pub, &msg).expect("signing should succeed in benches"); + let signature: MultiSignature = sig.into(); + + // Measure: dispatch the unsigned extrinsic (RawOrigin::None) with a valid wrapper. + #[extrinsic_call] + execute_revealed( + RawOrigin::None, + id, + signer.clone(), + nonce, + Box::new(inner_call.clone()), + signature.clone(), + ); + + // Assert: submission consumed, signer nonce bumped to 1. + assert!(Submissions::::get(id).is_none()); + let new_nonce = frame_system::Pallet::::account_nonce(&signer); + assert_eq!(new_nonce, 1u32.into()); + } + + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/pallets/shield/src/lib.rs b/pallets/shield/src/lib.rs index 6d802ba05c..8c38c78cf6 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -2,6 +2,8 @@ #![cfg_attr(not(feature = "std"), no_std)] pub use pallet::*; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; #[frame_support::pallet] pub mod pallet { @@ -16,17 +18,15 @@ pub mod pallet { use frame_system::pallet_prelude::*; use sp_consensus_aura::sr25519::AuthorityId as AuraAuthorityId; use sp_core::ByteArray; + use sp_runtime::transaction_validity::{ + InvalidTransaction, TransactionSource, ValidTransaction, + }; use sp_runtime::{ AccountId32, DispatchErrorWithPostInfo, MultiSignature, RuntimeDebug, traits::{BadOrigin, Dispatchable, Hash, SaturatedConversion, Verify, Zero}, }; use sp_std::{marker::PhantomData, prelude::*}; use subtensor_macros::freeze_struct; - use sp_runtime::transaction_validity::{ - InvalidTransaction, - TransactionSource, - ValidTransaction, - }; /// Origin helper: ensure the signer is an Aura authority (no session/authorship). pub struct EnsureAuraAuthority(PhantomData); @@ -128,10 +128,7 @@ pub mod pallet { #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// Encrypted wrapper accepted. - EncryptedSubmitted { - id: T::Hash, - who: T::AccountId, - }, + EncryptedSubmitted { id: T::Hash, who: T::AccountId }, /// Decrypted call executed. DecryptedExecuted { id: T::Hash, signer: T::AccountId }, /// Decrypted execution rejected. @@ -197,17 +194,17 @@ pub mod pallet { Ok(()) } - /// Users submit encrypted wrapper. - /// - /// Commitment semantics: + /// Users submit an encrypted wrapper. /// - /// ```text - /// raw_payload = - /// signer (32B) || nonce (u32 LE) || SCALE(call) - /// commitment = blake2_256(raw_payload) - /// ``` + /// `commitment` is `blake2_256(raw_payload)`, where: + /// raw_payload = signer || nonce || SCALE(call) /// - /// Ciphertext format: `[u16 kem_len][kem_ct][nonce24][aead_ct]` + /// `ciphertext` is constructed as: + /// [u16 kem_len] || kem_ct || nonce24 || aead_ct + /// where: + /// - `kem_ct` is the ML‑KEM ciphertext (encapsulated shared secret) + /// - `aead_ct` is XChaCha20‑Poly1305 over: + /// signer || nonce || SCALE(call) || sig_kind || signature #[pallet::call_index(1)] #[pallet::weight(({ let w = Weight::from_parts(ciphertext.len() as u64, 0) @@ -234,10 +231,7 @@ pub mod pallet { Error::::SubmissionAlreadyExists ); Submissions::::insert(id, sub); - Self::deposit_event(Event::EncryptedSubmitted { - id, - who, - }); + Self::deposit_event(Event::EncryptedSubmitted { id, who }); Ok(()) } @@ -260,8 +254,7 @@ pub mod pallet { return Err(Error::::MissingSubmission.into()); }; - let payload_bytes = - Self::build_raw_payload_bytes(&signer, nonce, call.as_ref()); + let payload_bytes = Self::build_raw_payload_bytes(&signer, nonce, call.as_ref()); // 1) Commitment check against on-chain stored commitment. let recomputed: T::Hash = T::Hashing::hash(&payload_bytes); @@ -339,11 +332,7 @@ pub mod pallet { impl ValidateUnsigned for Pallet { type Call = Call; - fn validate_unsigned( - source: TransactionSource, - call: &Self::Call, - ) -> TransactionValidity { - + fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity { match call { Call::execute_revealed { id, .. } => { match source { @@ -351,9 +340,9 @@ pub mod pallet { TransactionSource::Local | TransactionSource::InBlock => { ValidTransaction::with_tag_prefix("mev-shield-exec") .priority(u64::MAX) - .longevity(64) // long because propagate(false) - .and_provides(id) // dedupe by wrapper id - .propagate(false) // CRITICAL: no gossip, stays on author node + .longevity(64) // long because propagate(false) + .and_provides(id) // dedupe by wrapper id + .propagate(false) // CRITICAL: no gossip, stays on author node .build() } _ => InvalidTransaction::Call.into(), diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 7293841a39..b3aced2160 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -274,6 +274,7 @@ std = [ "pallet-contracts/std", "subtensor-chain-extensions/std", "ethereum/std", + "pallet-shield/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", @@ -318,6 +319,7 @@ runtime-benchmarks = [ # Smart Tx fees pallet "subtensor-transaction-fee/runtime-benchmarks", + "pallet-shield/runtime-benchmarks", ] try-runtime = [ "frame-try-runtime/try-runtime", @@ -365,5 +367,6 @@ try-runtime = [ "pallet-drand/try-runtime", "runtime-common/try-runtime", "pallet-aura/try-runtime", + "pallet-shield/try-runtime", ] metadata-hash = ["substrate-wasm-builder/metadata-hash"] diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 39f74989e6..6ce1c32ff6 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1668,6 +1668,7 @@ mod benches { [pallet_drand, Drand] [pallet_crowdloan, Crowdloan] [pallet_subtensor_swap, Swap] + [pallet_shield, MevShield] ); } diff --git a/scripts/benchmark_action.sh b/scripts/benchmark_action.sh index a475211163..53d7580a87 100755 --- a/scripts/benchmark_action.sh +++ b/scripts/benchmark_action.sh @@ -1,13 +1,14 @@ #!/usr/bin/env bash set -euo pipefail -PALLET_LIST=(subtensor admin_utils commitments drand) +PALLET_LIST=(subtensor admin_utils commitments drand, shield) declare -A DISPATCH_PATHS=( [subtensor]="../pallets/subtensor/src/macros/dispatches.rs" [admin_utils]="../pallets/admin-utils/src/lib.rs" [commitments]="../pallets/commitments/src/lib.rs" [drand]="../pallets/drand/src/lib.rs" + [shield]="../pallet/shield/src/lib.rs" [swap]="../pallets/swap/src/pallet/mod.rs" ) diff --git a/scripts/benchmark_all.sh b/scripts/benchmark_all.sh index 22d23483f3..99bdb60338 100755 --- a/scripts/benchmark_all.sh +++ b/scripts/benchmark_all.sh @@ -6,6 +6,7 @@ pallets=( "pallet_commitments" "pallet_drand" "pallet_admin_utils" + "pallet_shield" ) RUNTIME_WASM=./target/production/wbuild/node-subtensor-runtime/node_subtensor_runtime.compact.compressed.wasm From b8b20c71fe1ef9072cf758a784ed90981f822b77 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:27:58 -0800 Subject: [PATCH 18/30] fix typo --- scripts/benchmark_action.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/benchmark_action.sh b/scripts/benchmark_action.sh index 53d7580a87..f84f60bf76 100755 --- a/scripts/benchmark_action.sh +++ b/scripts/benchmark_action.sh @@ -8,7 +8,7 @@ declare -A DISPATCH_PATHS=( [admin_utils]="../pallets/admin-utils/src/lib.rs" [commitments]="../pallets/commitments/src/lib.rs" [drand]="../pallets/drand/src/lib.rs" - [shield]="../pallet/shield/src/lib.rs" + [shield]="../pallets/shield/src/lib.rs" [swap]="../pallets/swap/src/pallet/mod.rs" ) From fb9d9413bda62ab91f99111b5823250d86a41c02 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Sun, 23 Nov 2025 09:17:40 -0800 Subject: [PATCH 19/30] fix typo again --- scripts/benchmark_action.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/benchmark_action.sh b/scripts/benchmark_action.sh index f84f60bf76..29bd7744d3 100755 --- a/scripts/benchmark_action.sh +++ b/scripts/benchmark_action.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -PALLET_LIST=(subtensor admin_utils commitments drand, shield) +PALLET_LIST=(subtensor admin_utils commitments drand shield) declare -A DISPATCH_PATHS=( [subtensor]="../pallets/subtensor/src/macros/dispatches.rs" From 3c096d604ee3298691124b61c4dbb10a49917c2c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 16:34:35 +0000 Subject: [PATCH 20/30] auto-update benchmark weights --- pallets/shield/src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pallets/shield/src/lib.rs b/pallets/shield/src/lib.rs index 8c38c78cf6..41ff12ec00 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -168,8 +168,8 @@ pub mod pallet { impl Pallet { #[pallet::call_index(0)] #[pallet::weight( - Weight::from_parts(5_000, 0) - .saturating_add(T::DbWeight::get().reads(0_u64)) + Weight::from_parts(9_979_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) )] pub fn announce_next_key( @@ -207,7 +207,7 @@ pub mod pallet { /// signer || nonce || SCALE(call) || sig_kind || signature #[pallet::call_index(1)] #[pallet::weight(({ - let w = Weight::from_parts(ciphertext.len() as u64, 0) + let w = Weight::from_parts(13_980_000.len() as u64, 0) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)); w @@ -237,8 +237,8 @@ pub mod pallet { /// Executed by the block author. #[pallet::call_index(2)] - #[pallet::weight(Weight::from_parts(10_000, 0) - .saturating_add(T::DbWeight::get().reads(3_u64)) + #[pallet::weight(Weight::from_parts(77_280_000, 0) + .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)))] pub fn execute_revealed( origin: OriginFor, From 0a4845cd6ddf1c499423d82ed44e22d6f7a2d6df Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Mon, 24 Nov 2025 08:36:56 -0800 Subject: [PATCH 21/30] finalize weights --- pallets/shield/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pallets/shield/src/lib.rs b/pallets/shield/src/lib.rs index 41ff12ec00..a1d0246fe2 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -207,7 +207,7 @@ pub mod pallet { /// signer || nonce || SCALE(call) || sig_kind || signature #[pallet::call_index(1)] #[pallet::weight(({ - let w = Weight::from_parts(13_980_000.len() as u64, 0) + let w = Weight::from_parts(13_980_000, 0) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)); w From f1ce2fefc024421bd1e250c6b414042482de03f5 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Mon, 24 Nov 2025 08:49:56 -0800 Subject: [PATCH 22/30] zepter --- node/Cargo.toml | 6 ++++++ pallets/shield/Cargo.toml | 8 +++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/node/Cargo.toml b/node/Cargo.toml index fbeda4f536..1d2351c265 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -154,6 +154,7 @@ default = ["rocksdb", "sql", "txpool"] fast-runtime = [ "node-subtensor-runtime/fast-runtime", "subtensor-runtime-common/fast-runtime", + "pallet-subtensor/fast-runtime", ] sql = ["fc-db/sql", "fc-mapping-sync/sql"] txpool = ["fc-rpc/txpool", "fc-rpc-core/txpool"] @@ -170,7 +171,10 @@ runtime-benchmarks = [ "pallet-drand/runtime-benchmarks", "pallet-transaction-payment/runtime-benchmarks", "polkadot-sdk/runtime-benchmarks", + "pallet-subtensor/runtime-benchmarks", + "pallet-shield/runtime-benchmarks", ] + pow-faucet = [] # Enable features that allow the runtime to be tried and debugged. Name might be subject to change @@ -183,6 +187,8 @@ try-runtime = [ "pallet-commitments/try-runtime", "pallet-drand/try-runtime", "polkadot-sdk/try-runtime", + "pallet-shield/try-runtime", + "pallet-subtensor/try-runtime", ] metadata-hash = ["node-subtensor-runtime/metadata-hash"] diff --git a/pallets/shield/Cargo.toml b/pallets/shield/Cargo.toml index fcbb827871..c0038f2b92 100644 --- a/pallets/shield/Cargo.toml +++ b/pallets/shield/Cargo.toml @@ -43,18 +43,17 @@ default = [] std = [ "codec/std", "scale-info/std", - "frame-support/std", "frame-system/std", "frame-benchmarking?/std", - "sp-core/std", "sp-runtime/std", "sp-io/std", "sp-std/std", "sp-weights/std", - "pallet-timestamp/std", + "pallet-aura/std", + "sp-consensus-aura/std", ] runtime-benchmarks = [ @@ -62,10 +61,13 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", ] try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", "sp-runtime/try-runtime", + "pallet-aura/try-runtime", + "pallet-timestamp/try-runtime", ] From 7a572dc797e8d2757a82998491faf69035135094 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Mon, 24 Nov 2025 08:59:55 -0800 Subject: [PATCH 23/30] bump spec --- runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index ef506e5acd..adff037ac8 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -237,7 +237,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 348, + spec_version: 349, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From db447561222ab3c3e0745c644e5d5c0efe18ad53 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:05:15 -0800 Subject: [PATCH 24/30] add unit tests --- pallets/shield/src/benchmarking.rs | 2 - pallets/shield/src/lib.rs | 6 + pallets/shield/src/mock.rs | 153 +++++++++++++ pallets/shield/src/tests.rs | 349 +++++++++++++++++++++++++++++ 4 files changed, 508 insertions(+), 2 deletions(-) create mode 100644 pallets/shield/src/mock.rs create mode 100644 pallets/shield/src/tests.rs diff --git a/pallets/shield/src/benchmarking.rs b/pallets/shield/src/benchmarking.rs index a6be39ab59..be7d522c50 100644 --- a/pallets/shield/src/benchmarking.rs +++ b/pallets/shield/src/benchmarking.rs @@ -208,6 +208,4 @@ mod benches { let new_nonce = frame_system::Pallet::::account_nonce(&signer); assert_eq!(new_nonce, 1u32.into()); } - - impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); } diff --git a/pallets/shield/src/lib.rs b/pallets/shield/src/lib.rs index a1d0246fe2..d24e30e141 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -5,6 +5,12 @@ pub use pallet::*; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; +#[cfg(test)] +pub mod mock; + +#[cfg(test)] +mod tests; + #[frame_support::pallet] pub mod pallet { use super::*; diff --git a/pallets/shield/src/mock.rs b/pallets/shield/src/mock.rs new file mode 100644 index 0000000000..7285dee844 --- /dev/null +++ b/pallets/shield/src/mock.rs @@ -0,0 +1,153 @@ +#![cfg(test)] + +use crate as pallet_mev_shield; + +use frame_support::{ + construct_runtime, + derive_impl, + parameter_types, + traits::Everything, +}; +use frame_system as system; + +use sp_consensus_aura::sr25519::AuthorityId as AuraId; +use sp_core::{ConstU32, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + AccountId32, BuildStorage, +}; +use sp_runtime::traits::BadOrigin; + +// ----------------------------------------------------------------------------- +// Mock runtime +// ----------------------------------------------------------------------------- + +pub type UncheckedExtrinsic = system::mocking::MockUncheckedExtrinsic; +pub type Block = system::mocking::MockBlock; + +construct_runtime!( + pub enum Test { + System: frame_system = 0, + Timestamp: pallet_timestamp = 1, + Aura: pallet_aura = 2, + MevShield: pallet_mev_shield = 3, + } +); + +// A concrete nonce type used in tests. +pub type TestNonce = u64; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl system::Config for Test { + // Basic system config + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + + type Nonce = TestNonce; + type Hash = H256; + type Hashing = BlakeTwo256; + + type AccountId = AccountId32; + type Lookup = IdentityLookup; + type Block = Block; + + type BlockHashCount = (); + type Version = (); + type PalletInfo = PalletInfo; + + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + + // Max number of consumer refs per account. + type MaxConsumers = ConstU32<16>; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 1; +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +// Aura mock configuration +parameter_types! { + pub const MaxAuthorities: u32 = 32; + pub const AllowMultipleBlocksPerSlot: bool = false; + pub const SlotDuration: u64 = 6000; +} + +impl pallet_aura::Config for Test { + type AuthorityId = AuraId; + // For tests we don't need dynamic disabling; just use unit type. + type DisabledValidators = (); + type MaxAuthorities = MaxAuthorities; + type AllowMultipleBlocksPerSlot = AllowMultipleBlocksPerSlot; + type SlotDuration = SlotDuration; +} + +// ----------------------------------------------------------------------------- +// Authority origin for tests – root-only +// ----------------------------------------------------------------------------- + +/// For tests, treat Root as the “validator set” and return a dummy AccountId. +pub struct TestAuthorityOrigin; + +impl pallet_mev_shield::AuthorityOriginExt for TestAuthorityOrigin { + type AccountId = AccountId32; + + fn ensure_validator(origin: RuntimeOrigin) -> Result { + // Must be a signed origin. + let who: AccountId32 = frame_system::ensure_signed(origin).map_err(|_| BadOrigin)?; + + // Interpret the AccountId bytes as an AuraId, just like the real pallet. + let aura_id = + ::from_slice(who.as_ref()).map_err(|_| BadOrigin)?; + + // Check membership in the Aura validator set. + let is_validator = pallet_aura::Authorities::::get() + .into_iter() + .any(|id| id == aura_id); + + if is_validator { + Ok(who) + } else { + Err(BadOrigin) + } + } +} + + +// ----------------------------------------------------------------------------- +// MevShield Config +// ----------------------------------------------------------------------------- + +impl pallet_mev_shield::Config for Test { + type RuntimeCall = RuntimeCall; + type AuthorityOrigin = TestAuthorityOrigin; +} + +// ----------------------------------------------------------------------------- +// new_test_ext +// ----------------------------------------------------------------------------- + +pub fn new_test_ext() -> sp_io::TestExternalities { + // Use the construct_runtime!-generated genesis config. + RuntimeGenesisConfig::default() + .build_storage() + .expect("RuntimeGenesisConfig builds valid default genesis storage") + .into() +} diff --git a/pallets/shield/src/tests.rs b/pallets/shield/src/tests.rs new file mode 100644 index 0000000000..e632e89a42 --- /dev/null +++ b/pallets/shield/src/tests.rs @@ -0,0 +1,349 @@ +#![cfg(test)] + +use crate as pallet_mev_shield; +use crate::mock::*; + +use codec::Encode; +use frame_support::{assert_noop, assert_ok, BoundedVec}; +use frame_support::traits::ConstU32 as FrameConstU32; +use sp_core::sr25519; +use sp_runtime::{ + traits::{SaturatedConversion, Zero}, + transaction_validity::TransactionSource, + AccountId32, MultiSignature, +}; +use frame_support::pallet_prelude::ValidateUnsigned; +use sp_runtime::traits::Hash; +use sp_core::Pair; +use pallet_mev_shield::{ + Call as MevShieldCall, + CurrentKey, + Epoch, + Event as MevShieldEvent, + NextKey, + Submissions, +}; +use frame_support::traits::Hooks; + +// ----------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------- + +/// Deterministic sr25519 pair for tests (acts as "Alice"). +fn test_sr25519_pair() -> sr25519::Pair { + sr25519::Pair::from_seed(&[1u8; 32]) +} + +/// Reproduce the pallet's raw payload layout: +/// signer (32B) || nonce (u32 LE) || SCALE(call) +fn build_raw_payload_bytes_for_test( + signer: &AccountId32, + nonce: TestNonce, + call: &RuntimeCall, +) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(signer.as_ref()); + + let n_u32: u32 = nonce.saturated_into(); + out.extend_from_slice(&n_u32.to_le_bytes()); + + out.extend(call.encode()); + out +} + +#[test] +fn authority_can_announce_next_key_and_on_initialize_rolls_it() { + new_test_ext().execute_with(|| { + let epoch: u64 = 42; + const KYBER_PK_LEN: usize = 1184; + let pk_bytes = vec![7u8; KYBER_PK_LEN]; + let bounded_pk: BoundedVec> = + BoundedVec::truncate_from(pk_bytes.clone()); + + // Seed Aura authorities with a single validator and derive the matching account. + let validator_pair = test_sr25519_pair(); + let validator_account: AccountId32 = validator_pair.public().into(); + let validator_aura_id: ::AuthorityId = + validator_pair.public().into(); + + // Authorities storage expects a BoundedVec. + let authorities: BoundedVec< + ::AuthorityId, + ::MaxAuthorities, + > = BoundedVec::truncate_from(vec![validator_aura_id.clone()]); + pallet_aura::Authorities::::put(authorities); + + assert_eq!(Epoch::::get(), 0); + assert!(CurrentKey::::get().is_none()); + assert!(NextKey::::get().is_none()); + + // Signed by an Aura validator -> passes TestAuthorityOrigin::ensure_validator. + assert_ok!(MevShield::announce_next_key( + RuntimeOrigin::signed(validator_account.clone()), + bounded_pk.clone(), + epoch + )); + + // NextKey storage updated + let next = NextKey::::get().expect("NextKey should be set"); + assert_eq!(next.epoch, epoch); + assert_eq!(next.public_key.to_vec(), pk_bytes); + + // Roll on new block + MevShield::on_initialize(2); + + let curr = CurrentKey::::get().expect("CurrentKey should be set"); + assert_eq!(curr.epoch, epoch); + assert_eq!(curr.public_key.to_vec(), pk_bytes); + + assert_eq!(Epoch::::get(), epoch); + assert!(NextKey::::get().is_none()); + }); +} + + +#[test] +fn announce_next_key_rejects_non_validator_origins() { + new_test_ext().execute_with(|| { + const KYBER_PK_LEN: usize = 1184; + let epoch: u64 = 7; + + // Validator account: bytes match the Aura authority we put into storage. + let validator_pair = test_sr25519_pair(); + let validator_account: AccountId32 = validator_pair.public().into(); + let validator_aura_id: ::AuthorityId = + validator_pair.public().into(); + + // Non‑validator is some other key (not in Aura::Authorities). + let non_validator_pair = sr25519::Pair::from_seed(&[2u8; 32]); + let non_validator: AccountId32 = non_validator_pair.public().into(); + + // Only the validator is in the Aura validator set. + let authorities: BoundedVec< + ::AuthorityId, + ::MaxAuthorities, + > = BoundedVec::truncate_from(vec![validator_aura_id.clone()]); + pallet_aura::Authorities::::put(authorities); + + let pk_bytes = vec![9u8; KYBER_PK_LEN]; + let bounded_pk: BoundedVec> = + BoundedVec::truncate_from(pk_bytes.clone()); + + // 1) Signed non‑validator origin must fail with BadOrigin. + assert_noop!( + MevShield::announce_next_key( + RuntimeOrigin::signed(non_validator.clone()), + bounded_pk.clone(), + epoch, + ), + sp_runtime::DispatchError::BadOrigin + ); + + // 2) Unsigned origin must also fail with BadOrigin. + assert_noop!( + MevShield::announce_next_key( + RuntimeOrigin::none(), + bounded_pk.clone(), + epoch, + ), + sp_runtime::DispatchError::BadOrigin + ); + + // 3) Signed validator origin succeeds (sanity check). + assert_ok!(MevShield::announce_next_key( + RuntimeOrigin::signed(validator_account.clone()), + bounded_pk.clone(), + epoch + )); + + let next = NextKey::::get().expect("NextKey must be set by validator"); + assert_eq!(next.epoch, epoch); + assert_eq!(next.public_key.to_vec(), pk_bytes); + }); +} + +#[test] +fn submit_encrypted_stores_submission_and_emits_event() { + new_test_ext().execute_with(|| { + let pair = test_sr25519_pair(); + let who: AccountId32 = pair.public().into(); + + System::set_block_number(10); + + let commitment = + ::Hashing::hash(b"test-mevshield-commitment"); + let ciphertext_bytes = vec![1u8, 2, 3, 4]; + let ciphertext: BoundedVec> = + BoundedVec::truncate_from(ciphertext_bytes.clone()); + + assert_ok!(MevShield::submit_encrypted( + RuntimeOrigin::signed(who.clone()), + commitment, + ciphertext.clone(), + )); + + let id = ::Hashing::hash_of(&( + who.clone(), + commitment, + &ciphertext, + )); + + let stored = Submissions::::get(id).expect("submission stored"); + assert_eq!(stored.author, who); + assert_eq!(stored.commitment, commitment); + assert_eq!(stored.ciphertext.to_vec(), ciphertext_bytes); + assert_eq!(stored.submitted_in, 10); + + let events = System::events(); + let last = events.last().expect("at least one event").event.clone(); + + assert!( + matches!( + last, + RuntimeEvent::MevShield( + MevShieldEvent::::EncryptedSubmitted { id: ev_id, who: ev_who } + ) + if ev_id == id && ev_who == who + ), + "expected EncryptedSubmitted event with correct id & who", + ); + }); +} + +#[test] +fn execute_revealed_happy_path_verifies_and_executes_inner_call() { + new_test_ext().execute_with(|| { + let pair = test_sr25519_pair(); + let signer: AccountId32 = pair.public().into(); + + // Inner call – System.remark; must dispatch successfully. + let inner_call = RuntimeCall::System(frame_system::Call::::remark { + remark: b"hello-mevshield".to_vec(), + }); + + let nonce: TestNonce = Zero::zero(); + assert_eq!(System::account_nonce(&signer), nonce); + + let payload_bytes = build_raw_payload_bytes_for_test(&signer, nonce, &inner_call); + + let commitment = + ::Hashing::hash(payload_bytes.as_ref()); + + let ciphertext_bytes = vec![9u8, 9, 9, 9]; + let ciphertext: BoundedVec> = + BoundedVec::truncate_from(ciphertext_bytes.clone()); + + System::set_block_number(1); + + // Wrapper author == signer for simplest path + assert_ok!(MevShield::submit_encrypted( + RuntimeOrigin::signed(signer.clone()), + commitment, + ciphertext.clone(), + )); + + let id = ::Hashing::hash_of(&( + signer.clone(), + commitment, + &ciphertext, + )); + + // Build message "mev-shield:v1" || genesis_hash || payload + let genesis = System::block_hash(0); + let mut msg = b"mev-shield:v1".to_vec(); + msg.extend_from_slice(genesis.as_ref()); + msg.extend_from_slice(&payload_bytes); + + let sig_sr25519 = pair.sign(&msg); + let signature: MultiSignature = sig_sr25519.into(); + + let result = MevShield::execute_revealed( + RuntimeOrigin::none(), + id, + signer.clone(), + nonce, + Box::new(inner_call.clone()), + signature, + ); + + assert_ok!(result); + + // Submission consumed + assert!(Submissions::::get(id).is_none()); + + // Nonce bumped once + let expected_nonce: TestNonce = (1u32).saturated_into(); + assert_eq!(System::account_nonce(&signer), expected_nonce); + + // Last event is DecryptedExecuted + let events = System::events(); + let last = events.last().expect("an event should be emitted").event.clone(); + + assert!( + matches!( + last, + RuntimeEvent::MevShield( + MevShieldEvent::::DecryptedExecuted { id: ev_id, signer: ev_signer } + ) + if ev_id == id && ev_signer == signer + ), + "expected DecryptedExecuted event" + ); + }); +} + +#[test] +fn validate_unsigned_accepts_local_source_for_execute_revealed() { + new_test_ext().execute_with(|| { + let pair = test_sr25519_pair(); + let signer: AccountId32 = pair.public().into(); + let nonce: TestNonce = Zero::zero(); + + let inner_call = RuntimeCall::System(frame_system::Call::::remark { + remark: b"noop-local".to_vec(), + }); + + let id = ::Hashing::hash(b"mevshield-id-local"); + let signature: MultiSignature = + sr25519::Signature::from_raw([0u8; 64]).into(); + + let call = MevShieldCall::::execute_revealed { + id, + signer, + nonce, + call: Box::new(inner_call), + signature, + }; + + let validity = MevShield::validate_unsigned(TransactionSource::Local, &call); + assert_ok!(validity); + }); +} + +#[test] +fn validate_unsigned_accepts_inblock_source_for_execute_revealed() { + new_test_ext().execute_with(|| { + let pair = test_sr25519_pair(); + let signer: AccountId32 = pair.public().into(); + let nonce: TestNonce = Zero::zero(); + + let inner_call = RuntimeCall::System(frame_system::Call::::remark { + remark: b"noop-inblock".to_vec(), + }); + + let id = ::Hashing::hash(b"mevshield-id-inblock"); + let signature: MultiSignature = + sr25519::Signature::from_raw([1u8; 64]).into(); + + let call = MevShieldCall::::execute_revealed { + id, + signer, + nonce, + call: Box::new(inner_call), + signature, + }; + + let validity = MevShield::validate_unsigned(TransactionSource::InBlock, &call); + assert_ok!(validity); + }); +} From d8fe74bea5ed2c79d0782ad08f966c1c22c4c682 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:54:13 -0800 Subject: [PATCH 25/30] remove epoch --- node/src/mev_shield/author.rs | 38 ++++------- node/src/mev_shield/proposer.rs | 111 ++++++++++++++------------------ node/src/service.rs | 15 ----- pallets/shield/src/lib.rs | 23 +------ pallets/shield/src/tests.rs | 18 +----- 5 files changed, 68 insertions(+), 137 deletions(-) diff --git a/node/src/mev_shield/author.rs b/node/src/mev_shield/author.rs index 43ae5fee87..52943cf77a 100644 --- a/node/src/mev_shield/author.rs +++ b/node/src/mev_shield/author.rs @@ -21,7 +21,7 @@ pub struct TimeParams { } /// Holds the current/next ML‑KEM keypairs and their 32‑byte fingerprints. -#[freeze_struct("3a83c10877ec1f24")] +#[freeze_struct("5e3c8209248282c3")] #[derive(Clone)] pub struct ShieldKeys { pub current_sk: Vec, // ML‑KEM secret key bytes (encoded form) @@ -30,11 +30,10 @@ pub struct ShieldKeys { pub next_sk: Vec, pub next_pk: Vec, pub next_fp: [u8; 32], - pub epoch: u64, } impl ShieldKeys { - pub fn new(epoch: u64) -> Self { + pub fn new() -> Self { let (sk, pk) = MlKem768::generate(&mut OsRng); let sk_bytes = sk.as_bytes(); @@ -62,7 +61,6 @@ impl ShieldKeys { next_sk, next_pk, next_fp, - epoch, } } @@ -81,8 +79,6 @@ impl ShieldKeys { self.next_sk = nsk_slice.to_vec(); self.next_pk = npk_slice.to_vec(); self.next_fp = blake2_256(npk_slice); - - self.epoch = self.epoch.saturating_add(1); } } @@ -130,7 +126,6 @@ pub fn spawn_author_tasks( client: std::sync::Arc, pool: std::sync::Arc, keystore: sp_keystore::KeystorePtr, - initial_epoch: u64, timing: TimeParams, ) -> ShieldContext where @@ -140,7 +135,7 @@ where B::Extrinsic: From, { let ctx = ShieldContext { - keys: std::sync::Arc::new(std::sync::Mutex::new(ShieldKeys::new(initial_epoch))), + keys: std::sync::Arc::new(std::sync::Mutex::new(ShieldKeys::new())), timing: timing.clone(), }; @@ -181,8 +176,8 @@ where } // This block is the start of a slot for which we are the author. - let (epoch_now, curr_pk_len, next_pk_len) = match ctx_clone.keys.lock() { - Ok(k) => (k.epoch, k.current_pk.len(), k.next_pk.len()), + let (curr_pk_len, next_pk_len) = match ctx_clone.keys.lock() { + Ok(k) => (k.current_pk.len(), k.next_pk.len()), Err(e) => { log::debug!( target: "mev-shield", @@ -195,16 +190,16 @@ where log::debug!( target: "mev-shield", - "Slot start (local author): epoch={} (pk sizes: curr={}B, next={}B)", - epoch_now, curr_pk_len, next_pk_len + "Slot start (local author): (pk sizes: curr={}B, next={}B)", + curr_pk_len, next_pk_len ); // Wait until the announce window in this slot. sleep(std::time::Duration::from_millis(timing.announce_at_ms)).await; - // Read the next key we intend to use for the following epoch. - let (next_pk, next_epoch) = match ctx_clone.keys.lock() { - Ok(k) => (k.next_pk.clone(), k.epoch.saturating_add(1)), + // Read the next key we intend to use for the following block. + let next_pk = match ctx_clone.keys.lock() { + Ok(k) => k.next_pk.clone(), Err(e) => { log::debug!( target: "mev-shield", @@ -222,7 +217,6 @@ where keystore_clone.clone(), local_aura_pub.clone(), next_pk.clone(), - next_epoch, local_nonce, ) .await @@ -240,7 +234,6 @@ where keystore_clone.clone(), local_aura_pub.clone(), next_pk, - next_epoch, local_nonce.saturating_add(1), ) .await @@ -266,14 +259,13 @@ where let tail = timing.slot_ms.saturating_sub(timing.announce_at_ms); sleep(std::time::Duration::from_millis(tail)).await; - // Roll keys for the next epoch. + // Roll keys for the next block. match ctx_clone.keys.lock() { Ok(mut k) => { k.roll_for_next_slot(); log::debug!( target: "mev-shield", - "Rolled ML-KEM key at slot boundary (local author): new epoch={}", - k.epoch + "Rolled ML-KEM key at slot boundary", ); } Err(e) => { @@ -298,7 +290,6 @@ pub async fn submit_announce_extrinsic( keystore: sp_keystore::KeystorePtr, aura_pub: sp_core::sr25519::Public, next_public_key: Vec, - epoch: u64, nonce: u32, ) -> anyhow::Result<()> where @@ -350,7 +341,7 @@ where .map_err(|_| anyhow::anyhow!("public key too long (>2048 bytes)"))?; // 1) The runtime call carrying public key bytes. - let call = RuntimeCall::MevShield(pallet_shield::Call::announce_next_key { public_key, epoch }); + let call = RuntimeCall::MevShield(pallet_shield::Call::announce_next_key { public_key }); type Extra = runtime::TransactionExtensions; let extra: Extra = @@ -424,9 +415,8 @@ where log::debug!( target: "mev-shield", - "announce_next_key submitted: xt=0x{}, epoch={}, nonce={}", + "announce_next_key submitted: xt=0x{}, nonce={}", hex::encode(xt_hash), - epoch, nonce ); diff --git a/node/src/mev_shield/proposer.rs b/node/src/mev_shield/proposer.rs index 97a6f2c566..6fa60133ff 100644 --- a/node/src/mev_shield/proposer.rs +++ b/node/src/mev_shield/proposer.rs @@ -6,6 +6,7 @@ use sc_service::SpawnTaskHandle; use sc_transaction_pool_api::{TransactionPool, TransactionSource}; use sp_core::H256; use sp_runtime::{AccountId32, MultiSignature, OpaqueExtrinsic}; +use sp_runtime::traits::Header; use std::{ collections::HashMap, sync::{Arc, Mutex}, @@ -13,53 +14,53 @@ use std::{ }; use tokio::time::sleep; -/// Buffer of wrappers per-slot. +/// Buffer of wrappers keyed by the block number in which they were included. #[derive(Default, Clone)] struct WrapperBuffer { by_id: HashMap< H256, ( Vec, // ciphertext blob - u64, // key_epoch + u64, // originating block number AccountId32, // wrapper author ), >, } impl WrapperBuffer { - fn upsert(&mut self, id: H256, key_epoch: u64, author: AccountId32, ciphertext: Vec) { - self.by_id.insert(id, (ciphertext, key_epoch, author)); + fn upsert(&mut self, id: H256, block_number: u64, author: AccountId32, ciphertext: Vec) { + self.by_id.insert(id, (ciphertext, block_number, author)); } - /// Drain only wrappers whose `key_epoch` matches the given `epoch`. - /// - Wrappers with `key_epoch > epoch` are kept for future decrypt windows. - /// - Wrappers with `key_epoch < epoch` are considered stale and dropped. - fn drain_for_epoch( + /// Drain only wrappers whose `block_number` matches the given `block`. + /// - Wrappers with `block_number > block` are kept for future decrypt windows. + /// - Wrappers with `block_number < block` are considered stale and dropped. + fn drain_for_block( &mut self, - epoch: u64, + block: u64, ) -> Vec<(H256, u64, sp_runtime::AccountId32, Vec)> { let mut ready = Vec::new(); let mut kept_future: usize = 0; let mut dropped_past: usize = 0; - self.by_id.retain(|id, (ct, key_epoch, who)| { - if *key_epoch == epoch { + self.by_id.retain(|id, (ct, block_number, who)| { + if *block_number == block { // Ready to process now; remove from buffer. - ready.push((*id, *key_epoch, who.clone(), ct.clone())); + ready.push((*id, *block_number, who.clone(), ct.clone())); false - } else if *key_epoch > epoch { - // Not yet reveal time; keep for future epochs. + } else if *block_number > block { + // Not yet reveal time; keep for future blocks. kept_future = kept_future.saturating_add(1); true } else { - // key_epoch < epoch => stale / missed reveal window; drop. + // block_number < block => stale / missed reveal window; drop. dropped_past = dropped_past.saturating_add(1); log::debug!( target: "mev-shield", - "revealer: dropping stale wrapper id=0x{} key_epoch={} < curr_epoch={}", + "revealer: dropping stale wrapper id=0x{} block_number={} < curr_block={}", hex::encode(id.as_bytes()), - *key_epoch, - epoch + *block_number, + block ); false } @@ -67,8 +68,8 @@ impl WrapperBuffer { log::debug!( target: "mev-shield", - "revealer: drain_for_epoch(epoch={}): ready={}, kept_future={}, dropped_past={}", - epoch, + "revealer: drain_for_block(block={}): ready={}, kept_future={}, dropped_past={}", + block, ready.len(), kept_future, dropped_past @@ -80,7 +81,7 @@ impl WrapperBuffer { /// Start a background worker that: /// • watches imported blocks and captures `MevShield::submit_encrypted` -/// • buffers those wrappers, +/// • buffers those wrappers per originating block, /// • ~last `decrypt_window_ms` of the slot: decrypt & submit unsigned `execute_revealed` pub fn spawn_revealer( task_spawner: &SpawnTaskHandle, @@ -109,7 +110,6 @@ pub fn spawn_revealer( { let client = Arc::clone(&client); let buffer = Arc::clone(&buffer); - let ctx_for_buffer = ctx.clone(); task_spawner.spawn( "mev-shield-buffer-wrappers", @@ -120,11 +120,16 @@ pub fn spawn_revealer( while let Some(notif) = import_stream.next().await { let at_hash = notif.hash; + // FIX: dereference the number before saturated_into() + let block_number_u64: u64 = + (*notif.header.number()).saturated_into(); log::debug!( target: "mev-shield", - "imported block hash={:?} origin={:?}", - at_hash, notif.origin + "imported block hash={:?} number={} origin={:?}", + at_hash, + block_number_u64, + notif.origin ); match client.block_body(at_hash) { @@ -192,37 +197,15 @@ pub fn spawn_revealer( }, ) = &uxt.0.function { - // Derive the key_epoch for this wrapper from the current - // ShieldContext epoch at *buffer* time. - let key_epoch_opt = match ctx_for_buffer.keys.lock() { - Ok(k) => Some(k.epoch), - Err(e) => { - log::debug!( - target: "mev-shield", - " [xt #{idx}] failed to lock ShieldKeys in buffer task: {:?}", - e - ); - None - } - }; - - let Some(key_epoch) = key_epoch_opt else { - log::debug!( - target: "mev-shield", - " [xt #{idx}] skipping wrapper due to missing epoch snapshot" - ); - continue; - }; - let payload = (author.clone(), *commitment, ciphertext).encode(); let id = H256(sp_core::hashing::blake2_256(&payload)); log::debug!( target: "mev-shield", - " [xt #{idx}] buffered submit_encrypted: id=0x{}, key_epoch={}, author={}, ct_len={}, commitment={:?}", + " [xt #{idx}] buffered submit_encrypted: id=0x{}, block_number={}, author={}, ct_len={}, commitment={:?}", hex::encode(id.as_bytes()), - key_epoch, + block_number_u64, author, ciphertext.len(), commitment @@ -231,7 +214,7 @@ pub fn spawn_revealer( if let Ok(mut buf) = buffer.lock() { buf.upsert( id, - key_epoch, + block_number_u64, author, ciphertext.to_vec(), ); @@ -285,13 +268,12 @@ pub fn spawn_revealer( ); sleep(Duration::from_millis(tail)).await; - // Snapshot the current ML‑KEM secret and epoch + // Snapshot the current ML‑KEM secret (but *not* any epoch). let snapshot_opt = match ctx.keys.lock() { Ok(k) => { let sk_hash = sp_core::hashing::blake2_256(&k.current_sk); Some(( k.current_sk.clone(), - k.epoch, k.current_pk.len(), k.next_pk.len(), sk_hash, @@ -307,7 +289,7 @@ pub fn spawn_revealer( } }; - let (curr_sk_bytes, curr_epoch, curr_pk_len, next_pk_len, sk_hash) = + let (curr_sk_bytes, curr_pk_len, next_pk_len, sk_hash) = match snapshot_opt { Some(v) => v, None => { @@ -317,24 +299,27 @@ pub fn spawn_revealer( } }; + // Use best block number as the “epoch” for which we reveal. + let curr_block: u64 = client.info().best_number.saturated_into(); + log::debug!( target: "mev-shield", - "revealer: decrypt window start. epoch={} sk_len={} sk_hash=0x{} curr_pk_len={} next_pk_len={}", - curr_epoch, + "revealer: decrypt window start. block={} sk_len={} sk_hash=0x{} curr_pk_len={} next_pk_len={}", + curr_block, curr_sk_bytes.len(), hex::encode(sk_hash), curr_pk_len, next_pk_len ); - // Only process wrappers whose key_epoch == curr_epoch. + // Only process wrappers whose originating block matches the current block. let drained: Vec<(H256, u64, sp_runtime::AccountId32, Vec)> = match buffer.lock() { - Ok(mut buf) => buf.drain_for_epoch(curr_epoch), + Ok(mut buf) => buf.drain_for_block(curr_block), Err(e) => { log::debug!( target: "mev-shield", - "revealer: failed to lock WrapperBuffer for drain_for_epoch: {:?}", + "revealer: failed to lock WrapperBuffer for drain_for_block: {:?}", e ); Vec::new() @@ -343,20 +328,20 @@ pub fn spawn_revealer( log::debug!( target: "mev-shield", - "revealer: drained {} buffered wrappers for current epoch={}", + "revealer: drained {} buffered wrappers for current block={}", drained.len(), - curr_epoch + curr_block ); let mut to_submit: Vec<(H256, node_subtensor_runtime::RuntimeCall)> = Vec::new(); - for (id, key_epoch, author, blob) in drained.into_iter() { + for (id, block_number, author, blob) in drained.into_iter() { log::debug!( target: "mev-shield", - "revealer: candidate id=0x{} key_epoch={} (curr_epoch={}) author={} blob_len={}", + "revealer: candidate id=0x{} block_number={} (curr_block={}) author={} blob_len={}", hex::encode(id.as_bytes()), - key_epoch, - curr_epoch, + block_number, + curr_block, author, blob.len() ); diff --git a/node/src/service.rs b/node/src/service.rs index 4149f093e3..3e5f7e612a 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -554,27 +554,12 @@ where }; mev_timing = Some(timing.clone()); - // Initialize author‑side epoch from chain storage - let initial_epoch: u64 = { - let best = client.info().best_hash; - let mut key_bytes = Vec::with_capacity(32); - key_bytes.extend_from_slice(&twox_128(b"MevShield")); - key_bytes.extend_from_slice(&twox_128(b"Epoch")); - let key = StorageKey(key_bytes); - - match client.storage(best, &key) { - Ok(Some(raw_bytes)) => u64::decode(&mut &raw_bytes.0[..]).unwrap_or(0), - _ => 0, - } - }; - // Start author-side tasks with the epoch. let mev_ctx = author::spawn_author_tasks::( &task_manager.spawn_handle(), client.clone(), transaction_pool.clone(), keystore_container.keystore(), - initial_epoch, timing.clone(), ); diff --git a/pallets/shield/src/lib.rs b/pallets/shield/src/lib.rs index d24e30e141..5a72a62e94 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -80,14 +80,6 @@ pub mod pallet { pub submitted_in: BlockNumber, } - /// Ephemeral key fingerprint used by off-chain code to verify the ML‑KEM pubkey. - #[freeze_struct("4e13d24516013712")] - #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] - pub struct EphemeralPubKey { - pub public_key: BoundedVec>, - pub epoch: u64, - } - // ----------------- Config ----------------- #[pallet::config] @@ -111,13 +103,10 @@ pub mod pallet { // ----------------- Storage ----------------- #[pallet::storage] - pub type CurrentKey = StorageValue<_, EphemeralPubKey, OptionQuery>; - - #[pallet::storage] - pub type NextKey = StorageValue<_, EphemeralPubKey, OptionQuery>; + pub type CurrentKey = StorageValue<_, BoundedVec>, OptionQuery>; #[pallet::storage] - pub type Epoch = StorageValue<_, u64, ValueQuery>; + pub type NextKey = StorageValue<_, BoundedVec>, OptionQuery>; #[pallet::storage] pub type Submissions = StorageMap< @@ -146,7 +135,6 @@ pub mod pallet { #[pallet::error] pub enum Error { - BadEpoch, SubmissionAlreadyExists, MissingSubmission, CommitmentMismatch, @@ -162,7 +150,6 @@ pub mod pallet { fn on_initialize(_n: BlockNumberFor) -> Weight { if let Some(next) = >::take() { >::put(&next); - >::mutate(|e| *e = next.epoch); } T::DbWeight::get().reads_writes(1, 2) } @@ -181,7 +168,6 @@ pub mod pallet { pub fn announce_next_key( origin: OriginFor, public_key: BoundedVec>, - epoch: u64, ) -> DispatchResult { // Only a current Aura validator may call this (signed account ∈ Aura authorities) T::AuthorityOrigin::ensure_validator(origin)?; @@ -192,10 +178,7 @@ pub mod pallet { Error::::BadPublicKeyLen ); - NextKey::::put(EphemeralPubKey { - public_key: public_key.clone(), - epoch, - }); + NextKey::::put(public_key.clone()); Ok(()) } diff --git a/pallets/shield/src/tests.rs b/pallets/shield/src/tests.rs index e632e89a42..583bb90184 100644 --- a/pallets/shield/src/tests.rs +++ b/pallets/shield/src/tests.rs @@ -18,7 +18,6 @@ use sp_core::Pair; use pallet_mev_shield::{ Call as MevShieldCall, CurrentKey, - Epoch, Event as MevShieldEvent, NextKey, Submissions, @@ -54,7 +53,6 @@ fn build_raw_payload_bytes_for_test( #[test] fn authority_can_announce_next_key_and_on_initialize_rolls_it() { new_test_ext().execute_with(|| { - let epoch: u64 = 42; const KYBER_PK_LEN: usize = 1184; let pk_bytes = vec![7u8; KYBER_PK_LEN]; let bounded_pk: BoundedVec> = @@ -73,7 +71,6 @@ fn authority_can_announce_next_key_and_on_initialize_rolls_it() { > = BoundedVec::truncate_from(vec![validator_aura_id.clone()]); pallet_aura::Authorities::::put(authorities); - assert_eq!(Epoch::::get(), 0); assert!(CurrentKey::::get().is_none()); assert!(NextKey::::get().is_none()); @@ -81,22 +78,18 @@ fn authority_can_announce_next_key_and_on_initialize_rolls_it() { assert_ok!(MevShield::announce_next_key( RuntimeOrigin::signed(validator_account.clone()), bounded_pk.clone(), - epoch )); // NextKey storage updated let next = NextKey::::get().expect("NextKey should be set"); - assert_eq!(next.epoch, epoch); - assert_eq!(next.public_key.to_vec(), pk_bytes); + assert_eq!(next, pk_bytes); // Roll on new block MevShield::on_initialize(2); let curr = CurrentKey::::get().expect("CurrentKey should be set"); - assert_eq!(curr.epoch, epoch); - assert_eq!(curr.public_key.to_vec(), pk_bytes); + assert_eq!(curr, pk_bytes); - assert_eq!(Epoch::::get(), epoch); assert!(NextKey::::get().is_none()); }); } @@ -106,7 +99,6 @@ fn authority_can_announce_next_key_and_on_initialize_rolls_it() { fn announce_next_key_rejects_non_validator_origins() { new_test_ext().execute_with(|| { const KYBER_PK_LEN: usize = 1184; - let epoch: u64 = 7; // Validator account: bytes match the Aura authority we put into storage. let validator_pair = test_sr25519_pair(); @@ -134,7 +126,6 @@ fn announce_next_key_rejects_non_validator_origins() { MevShield::announce_next_key( RuntimeOrigin::signed(non_validator.clone()), bounded_pk.clone(), - epoch, ), sp_runtime::DispatchError::BadOrigin ); @@ -144,7 +135,6 @@ fn announce_next_key_rejects_non_validator_origins() { MevShield::announce_next_key( RuntimeOrigin::none(), bounded_pk.clone(), - epoch, ), sp_runtime::DispatchError::BadOrigin ); @@ -153,12 +143,10 @@ fn announce_next_key_rejects_non_validator_origins() { assert_ok!(MevShield::announce_next_key( RuntimeOrigin::signed(validator_account.clone()), bounded_pk.clone(), - epoch )); let next = NextKey::::get().expect("NextKey must be set by validator"); - assert_eq!(next.epoch, epoch); - assert_eq!(next.public_key.to_vec(), pk_bytes); + assert_eq!(next, pk_bytes); }); } From 1999fbf10e17c28f6373a792da9ca57baf51dec6 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:10:01 -0800 Subject: [PATCH 26/30] remove epoch in benchmarks --- pallets/shield/src/benchmarking.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pallets/shield/src/benchmarking.rs b/pallets/shield/src/benchmarking.rs index be7d522c50..18bbd51cba 100644 --- a/pallets/shield/src/benchmarking.rs +++ b/pallets/shield/src/benchmarking.rs @@ -92,20 +92,17 @@ mod benches { // Valid Kyber768 public key length per pallet check. const KYBER768_PK_LEN: usize = 1184; let public_key: BoundedVec> = bounded_pk::<2048>(KYBER768_PK_LEN); - let epoch: u64 = 42; // Measure: dispatch the extrinsic. #[extrinsic_call] announce_next_key( RawOrigin::Signed(alice_acc.clone()), public_key.clone(), - epoch, ); // Assert: NextKey should be set exactly. let stored = NextKey::::get().expect("must be set by announce_next_key"); - assert_eq!(stored.epoch, epoch); - assert_eq!(stored.public_key.as_slice(), public_key.as_slice()); + assert_eq!(stored, public_key.as_slice()); } /// Benchmark `submit_encrypted`. From 5f68f4594bcbf97aa5b6a0f2a140abadcd0aca10 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:20:11 -0800 Subject: [PATCH 27/30] add fast-runtime compatibility --- node/src/mev_shield/author.rs | 34 +++++++++++++++++--- node/src/mev_shield/proposer.rs | 57 +++++++++++++++++++++++++-------- node/src/service.rs | 5 --- 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/node/src/mev_shield/author.rs b/node/src/mev_shield/author.rs index 52943cf77a..0efe099f60 100644 --- a/node/src/mev_shield/author.rs +++ b/node/src/mev_shield/author.rs @@ -166,6 +166,29 @@ where use futures::StreamExt; use sp_consensus::BlockOrigin; + let slot_ms = timing.slot_ms; + + // Clamp announce_at_ms so it never exceeds slot_ms. + let mut announce_at_ms = timing.announce_at_ms; + if announce_at_ms > slot_ms { + log::warn!( + target: "mev-shield", + "spawn_author_tasks: announce_at_ms ({}) > slot_ms ({}); clamping to slot_ms", + announce_at_ms, + slot_ms, + ); + announce_at_ms = slot_ms; + } + let tail_ms = slot_ms.saturating_sub(announce_at_ms); + + log::debug!( + target: "mev-shield", + "author timing: slot_ms={} announce_at_ms={} (effective) tail_ms={}", + slot_ms, + announce_at_ms, + tail_ms + ); + let mut import_stream = client_clone.import_notification_stream(); let mut local_nonce: u32 = 0; @@ -195,7 +218,9 @@ where ); // Wait until the announce window in this slot. - sleep(std::time::Duration::from_millis(timing.announce_at_ms)).await; + if announce_at_ms > 0 { + sleep(std::time::Duration::from_millis(announce_at_ms)).await; + } // Read the next key we intend to use for the following block. let next_pk = match ctx_clone.keys.lock() { @@ -255,9 +280,10 @@ where } } - // Sleep the remainder of the slot. - let tail = timing.slot_ms.saturating_sub(timing.announce_at_ms); - sleep(std::time::Duration::from_millis(tail)).await; + // Sleep the remainder of the slot (if any). + if tail_ms > 0 { + sleep(std::time::Duration::from_millis(tail_ms)).await; + } // Roll keys for the next block. match ctx_clone.keys.lock() { diff --git a/node/src/mev_shield/proposer.rs b/node/src/mev_shield/proposer.rs index 6fa60133ff..95b837beb8 100644 --- a/node/src/mev_shield/proposer.rs +++ b/node/src/mev_shield/proposer.rs @@ -82,7 +82,7 @@ impl WrapperBuffer { /// Start a background worker that: /// • watches imported blocks and captures `MevShield::submit_encrypted` /// • buffers those wrappers per originating block, -/// • ~last `decrypt_window_ms` of the slot: decrypt & submit unsigned `execute_revealed` +/// • during the last `decrypt_window_ms` of the slot: decrypt & submit unsigned `execute_revealed` pub fn spawn_revealer( task_spawner: &SpawnTaskHandle, client: Arc, @@ -120,9 +120,7 @@ pub fn spawn_revealer( while let Some(notif) = import_stream.next().await { let at_hash = notif.hash; - // FIX: dereference the number before saturated_into() - let block_number_u64: u64 = - (*notif.header.number()).saturated_into(); + let block_number_u64: u64 = (*notif.header.number()).saturated_into(); log::debug!( target: "mev-shield", @@ -244,7 +242,7 @@ pub fn spawn_revealer( ); } - // ── 2) last-3s revealer ───────────────────────────────────── + // ── 2) decrypt window revealer ────────────────────────────── { let client = Arc::clone(&client); let pool = Arc::clone(&pool); @@ -252,21 +250,48 @@ pub fn spawn_revealer( let ctx = ctx.clone(); task_spawner.spawn( - "mev-shield-last-3s-revealer", + "mev-shield-last-window-revealer", None, async move { - log::debug!(target: "mev-shield", "last-3s-revealer task started"); + log::debug!(target: "mev-shield", "last-window-revealer task started"); + + // Respect the configured slot_ms, but clamp the decrypt window so it never + // exceeds the slot length (important for fast runtimes). + let slot_ms = ctx.timing.slot_ms; + let mut decrypt_window_ms = ctx.timing.decrypt_window_ms; + + if decrypt_window_ms > slot_ms { + log::warn!( + target: "mev-shield", + "spawn_revealer: decrypt_window_ms ({}) > slot_ms ({}); clamping to slot_ms", + decrypt_window_ms, + slot_ms + ); + decrypt_window_ms = slot_ms; + } + + let tail_ms = slot_ms.saturating_sub(decrypt_window_ms); + + log::debug!( + target: "mev-shield", + "revealer timing: slot_ms={} decrypt_window_ms={} (effective) tail_ms={}", + slot_ms, + decrypt_window_ms, + tail_ms + ); loop { - let tail = ctx.timing.slot_ms.saturating_sub(ctx.timing.decrypt_window_ms); log::debug!( target: "mev-shield", "revealer: sleeping {} ms before decrypt window (slot_ms={}, decrypt_window_ms={})", - tail, - ctx.timing.slot_ms, - ctx.timing.decrypt_window_ms + tail_ms, + slot_ms, + decrypt_window_ms ); - sleep(Duration::from_millis(tail)).await; + + if tail_ms > 0 { + sleep(Duration::from_millis(tail_ms)).await; + } // Snapshot the current ML‑KEM secret (but *not* any epoch). let snapshot_opt = match ctx.keys.lock() { @@ -294,7 +319,9 @@ pub fn spawn_revealer( Some(v) => v, None => { // Skip this decrypt window entirely, without holding any guard. - sleep(Duration::from_millis(ctx.timing.decrypt_window_ms)).await; + if decrypt_window_ms > 0 { + sleep(Duration::from_millis(decrypt_window_ms)).await; + } continue; } }; @@ -793,7 +820,9 @@ pub fn spawn_revealer( } // Let the decrypt window elapse. - sleep(Duration::from_millis(ctx.timing.decrypt_window_ms)).await; + if decrypt_window_ms > 0 { + sleep(Duration::from_millis(decrypt_window_ms)).await; + } } }, ); diff --git a/node/src/service.rs b/node/src/service.rs index 3e5f7e612a..7c32e3a393 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -35,11 +35,6 @@ use crate::ethereum::{ StorageOverrideHandler, db_config_dir, new_frontier_partial, spawn_frontier_tasks, }; use crate::mev_shield::{author, proposer}; -use codec::Decode; -use sc_client_api::HeaderBackend; -use sc_client_api::StorageKey; -use sc_client_api::StorageProvider; -use sp_core::twox_128; const LOG_TARGET: &str = "node-service"; From 6ea7c548782c3ab419bac10c56a443c50263d1f3 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:25:41 -0800 Subject: [PATCH 28/30] fmt --- node/src/mev_shield/proposer.rs | 2 +- pallets/shield/src/benchmarking.rs | 5 +--- pallets/shield/src/mock.rs | 12 +++------ pallets/shield/src/tests.rs | 43 +++++++++++++----------------- 4 files changed, 23 insertions(+), 39 deletions(-) diff --git a/node/src/mev_shield/proposer.rs b/node/src/mev_shield/proposer.rs index 95b837beb8..78724087ef 100644 --- a/node/src/mev_shield/proposer.rs +++ b/node/src/mev_shield/proposer.rs @@ -5,8 +5,8 @@ use ml_kem::{Ciphertext, Encoded, EncodedSizeUser, MlKem768, MlKem768Params}; use sc_service::SpawnTaskHandle; use sc_transaction_pool_api::{TransactionPool, TransactionSource}; use sp_core::H256; -use sp_runtime::{AccountId32, MultiSignature, OpaqueExtrinsic}; use sp_runtime::traits::Header; +use sp_runtime::{AccountId32, MultiSignature, OpaqueExtrinsic}; use std::{ collections::HashMap, sync::{Arc, Mutex}, diff --git a/pallets/shield/src/benchmarking.rs b/pallets/shield/src/benchmarking.rs index 18bbd51cba..bf8a85a0b2 100644 --- a/pallets/shield/src/benchmarking.rs +++ b/pallets/shield/src/benchmarking.rs @@ -95,10 +95,7 @@ mod benches { // Measure: dispatch the extrinsic. #[extrinsic_call] - announce_next_key( - RawOrigin::Signed(alice_acc.clone()), - public_key.clone(), - ); + announce_next_key(RawOrigin::Signed(alice_acc.clone()), public_key.clone()); // Assert: NextKey should be set exactly. let stored = NextKey::::get().expect("must be set by announce_next_key"); diff --git a/pallets/shield/src/mock.rs b/pallets/shield/src/mock.rs index 7285dee844..826d6c8c6f 100644 --- a/pallets/shield/src/mock.rs +++ b/pallets/shield/src/mock.rs @@ -2,21 +2,16 @@ use crate as pallet_mev_shield; -use frame_support::{ - construct_runtime, - derive_impl, - parameter_types, - traits::Everything, -}; +use frame_support::{construct_runtime, derive_impl, parameter_types, traits::Everything}; use frame_system as system; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{ConstU32, H256}; +use sp_runtime::traits::BadOrigin; use sp_runtime::{ - traits::{BlakeTwo256, IdentityLookup}, AccountId32, BuildStorage, + traits::{BlakeTwo256, IdentityLookup}, }; -use sp_runtime::traits::BadOrigin; // ----------------------------------------------------------------------------- // Mock runtime @@ -130,7 +125,6 @@ impl pallet_mev_shield::AuthorityOriginExt for TestAuthorityOrigi } } - // ----------------------------------------------------------------------------- // MevShield Config // ----------------------------------------------------------------------------- diff --git a/pallets/shield/src/tests.rs b/pallets/shield/src/tests.rs index 583bb90184..716903e906 100644 --- a/pallets/shield/src/tests.rs +++ b/pallets/shield/src/tests.rs @@ -4,25 +4,21 @@ use crate as pallet_mev_shield; use crate::mock::*; use codec::Encode; -use frame_support::{assert_noop, assert_ok, BoundedVec}; +use frame_support::pallet_prelude::ValidateUnsigned; use frame_support::traits::ConstU32 as FrameConstU32; +use frame_support::traits::Hooks; +use frame_support::{BoundedVec, assert_noop, assert_ok}; +use pallet_mev_shield::{ + Call as MevShieldCall, CurrentKey, Event as MevShieldEvent, NextKey, Submissions, +}; +use sp_core::Pair; use sp_core::sr25519; +use sp_runtime::traits::Hash; use sp_runtime::{ + AccountId32, MultiSignature, traits::{SaturatedConversion, Zero}, transaction_validity::TransactionSource, - AccountId32, MultiSignature, }; -use frame_support::pallet_prelude::ValidateUnsigned; -use sp_runtime::traits::Hash; -use sp_core::Pair; -use pallet_mev_shield::{ - Call as MevShieldCall, - CurrentKey, - Event as MevShieldEvent, - NextKey, - Submissions, -}; -use frame_support::traits::Hooks; // ----------------------------------------------------------------------------- // Helpers @@ -94,7 +90,6 @@ fn authority_can_announce_next_key_and_on_initialize_rolls_it() { }); } - #[test] fn announce_next_key_rejects_non_validator_origins() { new_test_ext().execute_with(|| { @@ -132,10 +127,7 @@ fn announce_next_key_rejects_non_validator_origins() { // 2) Unsigned origin must also fail with BadOrigin. assert_noop!( - MevShield::announce_next_key( - RuntimeOrigin::none(), - bounded_pk.clone(), - ), + MevShield::announce_next_key(RuntimeOrigin::none(), bounded_pk.clone(),), sp_runtime::DispatchError::BadOrigin ); @@ -214,8 +206,7 @@ fn execute_revealed_happy_path_verifies_and_executes_inner_call() { let payload_bytes = build_raw_payload_bytes_for_test(&signer, nonce, &inner_call); - let commitment = - ::Hashing::hash(payload_bytes.as_ref()); + let commitment = ::Hashing::hash(payload_bytes.as_ref()); let ciphertext_bytes = vec![9u8, 9, 9, 9]; let ciphertext: BoundedVec> = @@ -265,7 +256,11 @@ fn execute_revealed_happy_path_verifies_and_executes_inner_call() { // Last event is DecryptedExecuted let events = System::events(); - let last = events.last().expect("an event should be emitted").event.clone(); + let last = events + .last() + .expect("an event should be emitted") + .event + .clone(); assert!( matches!( @@ -292,8 +287,7 @@ fn validate_unsigned_accepts_local_source_for_execute_revealed() { }); let id = ::Hashing::hash(b"mevshield-id-local"); - let signature: MultiSignature = - sr25519::Signature::from_raw([0u8; 64]).into(); + let signature: MultiSignature = sr25519::Signature::from_raw([0u8; 64]).into(); let call = MevShieldCall::::execute_revealed { id, @@ -320,8 +314,7 @@ fn validate_unsigned_accepts_inblock_source_for_execute_revealed() { }); let id = ::Hashing::hash(b"mevshield-id-inblock"); - let signature: MultiSignature = - sr25519::Signature::from_raw([1u8; 64]).into(); + let signature: MultiSignature = sr25519::Signature::from_raw([1u8; 64]).into(); let call = MevShieldCall::::execute_revealed { id, From e99df02522df12f4dfc2d126e8b5c70823f20996 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:36:35 -0800 Subject: [PATCH 29/30] more timing --- node/src/service.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/node/src/service.rs b/node/src/service.rs index 7c32e3a393..96620e763b 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -539,17 +539,32 @@ where let mut mev_timing: Option = None; if role.is_authority() { - let slot_duration_ms: u64 = consensus_mechanism.slot_duration(&client)?.as_millis() as u64; + let slot_duration = consensus_mechanism.slot_duration(&client)?; + let slot_duration_ms: u64 = + u64::try_from(slot_duration.as_millis()).unwrap_or(u64::MAX); + + // For 12s blocks: announce ≈ 7s, decrypt window ≈ 3s. + // For 250ms blocks: announce ≈ 145ms, decrypt window ≈ 62ms, etc. + let announce_at_ms_raw = slot_duration_ms + .saturating_mul(7) + .saturating_div(12); + + let decrypt_window_ms = slot_duration_ms + .saturating_mul(3) + .saturating_div(12); + + // Ensure announce_at_ms + decrypt_window_ms never exceeds slot_ms. + let max_announce = slot_duration_ms.saturating_sub(decrypt_window_ms); + let announce_at_ms = announce_at_ms_raw.min(max_announce); - // Time windows (7s announce / last 3s decrypt). let timing = author::TimeParams { slot_ms: slot_duration_ms, - announce_at_ms: 7_000, - decrypt_window_ms: 3_000, + announce_at_ms, + decrypt_window_ms, }; mev_timing = Some(timing.clone()); - // Start author-side tasks with the epoch. + // Start author-side tasks with dynamic timing. let mev_ctx = author::spawn_author_tasks::( &task_manager.spawn_handle(), client.clone(), @@ -558,7 +573,7 @@ where timing.clone(), ); - // Start last-3s revealer (decrypt -> execute_revealed). + // Start last-portion-of-slot revealer (decrypt -> execute_revealed). proposer::spawn_revealer::( &task_manager.spawn_handle(), client.clone(), From c153a37b90a74283264aead3329d701fb9a2076e Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:39:11 -0800 Subject: [PATCH 30/30] clippy --- node/src/mev_shield/author.rs | 65 ++++++++++++++---------------- node/src/mev_shield/proposer.rs | 30 ++++---------- node/src/service.rs | 30 ++++++-------- pallets/shield/src/benchmarking.rs | 5 +-- pallets/shield/src/lib.rs | 12 +++--- pallets/shield/src/mock.rs | 2 - pallets/shield/src/tests.rs | 2 - 7 files changed, 59 insertions(+), 87 deletions(-) diff --git a/node/src/mev_shield/author.rs b/node/src/mev_shield/author.rs index 0efe099f60..20ac53702a 100644 --- a/node/src/mev_shield/author.rs +++ b/node/src/mev_shield/author.rs @@ -82,6 +82,12 @@ impl ShieldKeys { } } +impl Default for ShieldKeys { + fn default() -> Self { + Self::new() + } +} + /// Shared context state. #[freeze_struct("62af7d26cf7c1271")] #[derive(Clone)] @@ -94,7 +100,10 @@ pub struct ShieldContext { pub fn derive_aead_key(ss: &[u8]) -> [u8; 32] { let mut key = [0u8; 32]; let n = ss.len().min(32); - key[..n].copy_from_slice(&ss[..n]); + + if let (Some(dst), Some(src)) = (key.get_mut(..n), ss.get(..n)) { + dst.copy_from_slice(src); + } key } @@ -123,8 +132,8 @@ const AURA_KEY_TYPE: KeyTypeId = KeyTypeId(*b"aura"); /// - at ~announce_at_ms announce the next key bytes on chain, pub fn spawn_author_tasks( task_spawner: &sc_service::SpawnTaskHandle, - client: std::sync::Arc, - pool: std::sync::Arc, + client: Arc, + pool: Arc, keystore: sp_keystore::KeystorePtr, timing: TimeParams, ) -> ShieldContext @@ -135,13 +144,13 @@ where B::Extrinsic: From, { let ctx = ShieldContext { - keys: std::sync::Arc::new(std::sync::Mutex::new(ShieldKeys::new())), + keys: Arc::new(Mutex::new(ShieldKeys::new())), timing: timing.clone(), }; let aura_keys: Vec = keystore.sr25519_public_keys(AURA_KEY_TYPE); - let local_aura_pub = match aura_keys.get(0).cloned() { + let local_aura_pub = match aura_keys.first().copied() { Some(k) => k, None => { log::warn!( @@ -173,9 +182,7 @@ where if announce_at_ms > slot_ms { log::warn!( target: "mev-shield", - "spawn_author_tasks: announce_at_ms ({}) > slot_ms ({}); clamping to slot_ms", - announce_at_ms, - slot_ms, + "spawn_author_tasks: announce_at_ms ({announce_at_ms}) > slot_ms ({slot_ms}); clamping to slot_ms", ); announce_at_ms = slot_ms; } @@ -183,10 +190,7 @@ where log::debug!( target: "mev-shield", - "author timing: slot_ms={} announce_at_ms={} (effective) tail_ms={}", - slot_ms, - announce_at_ms, - tail_ms + "author timing: slot_ms={slot_ms} announce_at_ms={announce_at_ms} (effective) tail_ms={tail_ms}", ); let mut import_stream = client_clone.import_notification_stream(); @@ -204,8 +208,7 @@ where Err(e) => { log::debug!( target: "mev-shield", - "spawn_author_tasks: failed to lock ShieldKeys (poisoned?): {:?}", - e + "spawn_author_tasks: failed to lock ShieldKeys (poisoned?): {e:?}", ); continue; } @@ -213,8 +216,7 @@ where log::debug!( target: "mev-shield", - "Slot start (local author): (pk sizes: curr={}B, next={}B)", - curr_pk_len, next_pk_len + "Slot start (local author): (pk sizes: curr={curr_pk_len}B, next={next_pk_len}B)", ); // Wait until the announce window in this slot. @@ -228,8 +230,7 @@ where Err(e) => { log::debug!( target: "mev-shield", - "spawn_author_tasks: failed to lock ShieldKeys for next_pk: {:?}", - e + "spawn_author_tasks: failed to lock ShieldKeys for next_pk: {e:?}", ); continue; } @@ -240,7 +241,7 @@ where client_clone.clone(), pool_clone.clone(), keystore_clone.clone(), - local_aura_pub.clone(), + local_aura_pub, next_pk.clone(), local_nonce, ) @@ -257,7 +258,7 @@ where client_clone.clone(), pool_clone.clone(), keystore_clone.clone(), - local_aura_pub.clone(), + local_aura_pub, next_pk, local_nonce.saturating_add(1), ) @@ -297,13 +298,12 @@ where Err(e) => { log::debug!( target: "mev-shield", - "spawn_author_tasks: failed to lock ShieldKeys for roll_for_next_slot: {:?}", - e + "spawn_author_tasks: failed to lock ShieldKeys for roll_for_next_slot: {e:?}", ); } } } - } + }, ); ctx @@ -311,8 +311,8 @@ where /// Build & submit the signed `announce_next_key` extrinsic OFF-CHAIN pub async fn submit_announce_extrinsic( - client: std::sync::Arc, - pool: std::sync::Arc, + client: Arc, + pool: Arc, keystore: sp_keystore::KeystorePtr, aura_pub: sp_core::sr25519::Public, next_public_key: Vec, @@ -350,10 +350,9 @@ where let src_start = bytes.len().saturating_sub(n); let dst_start = 32usize.saturating_sub(n); - if let (Some(dst), Some(src)) = ( - out.get_mut(dst_start..32), - bytes.get(src_start..src_start + n), - ) { + let src_slice = bytes.get(src_start..).and_then(|s| s.get(..n)); + + if let (Some(dst), Some(src)) = (out.get_mut(dst_start..32), src_slice) { dst.copy_from_slice(src); H256(out) } else { @@ -411,8 +410,7 @@ where ); // Build the exact signable payload. - let payload: SignedPayload = - SignedPayload::from_raw(call.clone(), extra.clone(), implicit.clone()); + let payload: SignedPayload = SignedPayload::from_raw(call.clone(), extra.clone(), implicit); let raw_payload = payload.encode(); @@ -432,6 +430,7 @@ where let xt_bytes = uxt.encode(); let xt_hash = sp_core::hashing::blake2_256(&xt_bytes); + let xt_hash_hex = hex::encode(xt_hash); let opaque: sp_runtime::OpaqueExtrinsic = uxt.into(); let xt: ::Extrinsic = opaque.into(); @@ -441,9 +440,7 @@ where log::debug!( target: "mev-shield", - "announce_next_key submitted: xt=0x{}, nonce={}", - hex::encode(xt_hash), - nonce + "announce_next_key submitted: xt=0x{xt_hash_hex}, nonce={nonce}", ); Ok(()) diff --git a/node/src/mev_shield/proposer.rs b/node/src/mev_shield/proposer.rs index 78724087ef..7bacbf90e3 100644 --- a/node/src/mev_shield/proposer.rs +++ b/node/src/mev_shield/proposer.rs @@ -151,8 +151,7 @@ pub fn spawn_revealer( Err(e) => { log::debug!( target: "mev-shield", - " [xt #{idx}] failed to decode UncheckedExtrinsic: {:?}", - e + " [xt #{idx}] failed to decode UncheckedExtrinsic: {e:?}", ); continue; } @@ -227,14 +226,11 @@ pub fn spawn_revealer( } Ok(None) => log::debug!( target: "mev-shield", - " block_body returned None for hash={:?}", - at_hash + " block_body returned None for hash={at_hash:?}", ), Err(e) => log::debug!( target: "mev-shield", - " block_body error for hash={:?}: {:?}", - at_hash, - e + " block_body error for hash={at_hash:?}: {e:?}", ), } } @@ -263,9 +259,7 @@ pub fn spawn_revealer( if decrypt_window_ms > slot_ms { log::warn!( target: "mev-shield", - "spawn_revealer: decrypt_window_ms ({}) > slot_ms ({}); clamping to slot_ms", - decrypt_window_ms, - slot_ms + "spawn_revealer: decrypt_window_ms ({decrypt_window_ms}) > slot_ms ({slot_ms}); clamping to slot_ms", ); decrypt_window_ms = slot_ms; } @@ -274,19 +268,13 @@ pub fn spawn_revealer( log::debug!( target: "mev-shield", - "revealer timing: slot_ms={} decrypt_window_ms={} (effective) tail_ms={}", - slot_ms, - decrypt_window_ms, - tail_ms + "revealer timing: slot_ms={slot_ms} decrypt_window_ms={decrypt_window_ms} (effective) tail_ms={tail_ms}", ); loop { log::debug!( target: "mev-shield", - "revealer: sleeping {} ms before decrypt window (slot_ms={}, decrypt_window_ms={})", - tail_ms, - slot_ms, - decrypt_window_ms + "revealer: sleeping {tail_ms} ms before decrypt window (slot_ms={slot_ms}, decrypt_window_ms={decrypt_window_ms})", ); if tail_ms > 0 { @@ -307,8 +295,7 @@ pub fn spawn_revealer( Err(e) => { log::debug!( target: "mev-shield", - "revealer: failed to lock ShieldKeys (poisoned?): {:?}", - e + "revealer: failed to lock ShieldKeys (poisoned?): {e:?}", ); None } @@ -346,8 +333,7 @@ pub fn spawn_revealer( Err(e) => { log::debug!( target: "mev-shield", - "revealer: failed to lock WrapperBuffer for drain_for_block: {:?}", - e + "revealer: failed to lock WrapperBuffer for drain_for_block: {e:?}", ); Vec::new() } diff --git a/node/src/service.rs b/node/src/service.rs index 96620e763b..d32aceea9c 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -540,18 +540,13 @@ where if role.is_authority() { let slot_duration = consensus_mechanism.slot_duration(&client)?; - let slot_duration_ms: u64 = - u64::try_from(slot_duration.as_millis()).unwrap_or(u64::MAX); + let slot_duration_ms: u64 = u64::try_from(slot_duration.as_millis()).unwrap_or(u64::MAX); // For 12s blocks: announce ≈ 7s, decrypt window ≈ 3s. // For 250ms blocks: announce ≈ 145ms, decrypt window ≈ 62ms, etc. - let announce_at_ms_raw = slot_duration_ms - .saturating_mul(7) - .saturating_div(12); + let announce_at_ms_raw = slot_duration_ms.saturating_mul(7).saturating_div(12); - let decrypt_window_ms = slot_duration_ms - .saturating_mul(3) - .saturating_div(12); + let decrypt_window_ms = slot_duration_ms.saturating_mul(3).saturating_div(12); // Ensure announce_at_ms + decrypt_window_ms never exceeds slot_ms. let max_announce = slot_duration_ms.saturating_sub(decrypt_window_ms); @@ -614,20 +609,19 @@ where let (slot_ms, decrypt_ms) = mev_timing .as_ref() .map(|t| (t.slot_ms, t.decrypt_window_ms)) - .unwrap_or((slot_duration.as_millis() as u64, 3_000)); + .unwrap_or((slot_duration.as_millis(), 3_000)); let guard_ms: u64 = 200; // small cushion so reveals hit the pool first let after_decrypt_ms = slot_ms.saturating_sub(decrypt_ms).saturating_add(guard_ms); - // Clamp into (0.5 .. 0.98] to give the proposer enough time - let mut f = (after_decrypt_ms as f32) / (slot_ms as f32); - if f < 0.50 { - f = 0.50; - } - if f > 0.98 { - f = 0.98; - } - f + let f_raw = if slot_ms > 0 { + (after_decrypt_ms as f32) / (slot_ms as f32) + } else { + // Extremely defensive fallback; should never happen in practice. + 0.75 + }; + + f_raw.clamp(0.50, 0.98) }; let create_inherent_data_providers = diff --git a/pallets/shield/src/benchmarking.rs b/pallets/shield/src/benchmarking.rs index bf8a85a0b2..8e1d370e7f 100644 --- a/pallets/shield/src/benchmarking.rs +++ b/pallets/shield/src/benchmarking.rs @@ -1,6 +1,3 @@ -//! Benchmarking for pallet-mev-shield. -#![cfg(feature = "runtime-benchmarks")] - use super::*; use codec::Encode; @@ -99,7 +96,7 @@ mod benches { // Assert: NextKey should be set exactly. let stored = NextKey::::get().expect("must be set by announce_next_key"); - assert_eq!(stored, public_key.as_slice()); + assert_eq!(stored, public_key); } /// Benchmark `submit_encrypted`. diff --git a/pallets/shield/src/lib.rs b/pallets/shield/src/lib.rs index 5a72a62e94..1e1605b633 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -195,12 +195,13 @@ pub mod pallet { /// - `aead_ct` is XChaCha20‑Poly1305 over: /// signer || nonce || SCALE(call) || sig_kind || signature #[pallet::call_index(1)] - #[pallet::weight(({ - let w = Weight::from_parts(13_980_000, 0) + #[pallet::weight(( + Weight::from_parts(13_980_000, 0) .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)); - w - }, DispatchClass::Normal, Pays::Yes))] + .saturating_add(T::DbWeight::get().writes(1_u64)), + DispatchClass::Normal, + Pays::Yes, + ))] pub fn submit_encrypted( origin: OriginFor, commitment: T::Hash, @@ -229,6 +230,7 @@ pub mod pallet { #[pallet::weight(Weight::from_parts(77_280_000, 0) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)))] + #[allow(clippy::useless_conversion)] pub fn execute_revealed( origin: OriginFor, id: T::Hash, diff --git a/pallets/shield/src/mock.rs b/pallets/shield/src/mock.rs index 826d6c8c6f..0732670406 100644 --- a/pallets/shield/src/mock.rs +++ b/pallets/shield/src/mock.rs @@ -1,5 +1,3 @@ -#![cfg(test)] - use crate as pallet_mev_shield; use frame_support::{construct_runtime, derive_impl, parameter_types, traits::Everything}; diff --git a/pallets/shield/src/tests.rs b/pallets/shield/src/tests.rs index 716903e906..e3bc630014 100644 --- a/pallets/shield/src/tests.rs +++ b/pallets/shield/src/tests.rs @@ -1,5 +1,3 @@ -#![cfg(test)] - use crate as pallet_mev_shield; use crate::mock::*;