diff --git a/node/src/mev_shield/author.rs b/node/src/mev_shield/author.rs index 2f6c86f5df..bc6cbc2f2d 100644 --- a/node/src/mev_shield/author.rs +++ b/node/src/mev_shield/author.rs @@ -2,13 +2,11 @@ use chacha20poly1305::{ KeyInit, XChaCha20Poly1305, XNonce, aead::{Aead, Payload}, }; -use frame_system_rpc_runtime_api::AccountNonceApi; use ml_kem::{EncodedSizeUser, KemCore, MlKem768}; use node_subtensor_runtime as runtime; use rand::rngs::OsRng; -use sp_api::ProvideRuntimeApi; use sp_core::blake2_256; -use sp_runtime::{AccountId32, KeyTypeId}; +use sp_runtime::KeyTypeId; use std::sync::{Arc, Mutex}; use subtensor_macros::freeze_struct; use tokio::time::sleep; @@ -131,7 +129,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, +/// - at ~announce_at_ms announce the next key bytes on chain (as an UNSIGNED tx), pub fn spawn_author_tasks( task_spawner: &sc_service::SpawnTaskHandle, client: Arc, @@ -141,13 +139,7 @@ pub fn spawn_author_tasks( ) -> ShieldContext where B: sp_runtime::traits::Block, - C: sc_client_api::HeaderBackend - + sc_client_api::BlockchainEvents - + ProvideRuntimeApi - + Send - + Sync - + 'static, - C::Api: AccountNonceApi, + C: sc_client_api::HeaderBackend + sc_client_api::BlockchainEvents + Send + Sync + 'static, Pool: sc_transaction_pool_api::TransactionPool + Send + Sync + 'static, B::Extrinsic: From, { @@ -156,25 +148,20 @@ where timing: timing.clone(), }; + // Only run these tasks on nodes that actually have an Aura key in their keystore. let aura_keys: Vec = keystore.sr25519_public_keys(AURA_KEY_TYPE); + if aura_keys.is_empty() { + 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 = match aura_keys.first().copied() { - 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 aura_account: AccountId32 = local_aura_pub.into(); 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( @@ -243,37 +230,17 @@ where } }; - // 🔑 Fetch the current on-chain nonce for the Aura account using the best block hash. - let best_hash = client_clone.info().best_hash; - - let nonce: u32 = match client_clone - .runtime_api() - .account_nonce(best_hash, aura_account.clone()) - { - Ok(n) => n, - Err(e) => { - log::debug!( - target: "mev-shield", - "spawn_author_tasks: failed to fetch account nonce for MEV-Shield author: {e:?}", - ); - continue; - } - }; - - // Submit announce_next_key signed with the Aura key using the correct nonce. + // Submit announce_next_key as an UNSIGNED extrinsic (Origin::None). if let Err(e) = submit_announce_extrinsic::( client_clone.clone(), pool_clone.clone(), - keystore_clone.clone(), - local_aura_pub, next_pk.clone(), - nonce, ) .await { log::debug!( target: "mev-shield", - "announce_next_key submit error (nonce={nonce:?}): {e:?}" + "announce_next_key unsigned submit error: {e:?}" ); } @@ -305,140 +272,47 @@ where ctx } -/// Build & submit the signed `announce_next_key` extrinsic OFF-CHAIN +/// Build & submit the **unsigned** `announce_next_key` extrinsic OFF-CHAIN pub async fn submit_announce_extrinsic( client: Arc, pool: Arc, - keystore: sp_keystore::KeystorePtr, - aura_pub: sp_core::sr25519::Public, next_public_key: Vec, - nonce: u32, ) -> anyhow::Result<()> where B: sp_runtime::traits::Block, C: sc_client_api::HeaderBackend + Send + Sync + 'static, Pool: sc_transaction_pool_api::TransactionPool + Send + Sync + 'static, B::Extrinsic: From, - B::Hash: AsRef<[u8]>, { - use node_subtensor_runtime as runtime; - use runtime::{RuntimeCall, SignedPayload, UncheckedExtrinsic}; - + use runtime::{RuntimeCall, UncheckedExtrinsic}; use sc_transaction_pool_api::TransactionSource; - use sp_core::H256; use sp_runtime::codec::Encode; - use sp_runtime::{ - BoundedVec, MultiSignature, - generic::Era, - traits::{ConstU32, TransactionExtension}, - }; - - 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); - let src_start = bytes.len().saturating_sub(n); - let dst_start = 32usize.saturating_sub(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 { - // Extremely defensive fallback. - H256([0u8; 32]) - } - } + use sp_runtime::{BoundedVec, traits::ConstU32}; 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) Runtime call carrying the public key bytes. + // Runtime call carrying the public key bytes. let call = RuntimeCall::MevShield(pallet_shield::Call::announce_next_key { public_key }); - // 2) Build the transaction extensions exactly like the runtime. - 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::< - 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), - ); - - // 3) Manually construct the `Implicit` tuple that the runtime will also derive. - 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::Implicit = u32 - runtime::VERSION.transaction_version, // CheckTxVersion::Implicit = u32 - genesis_h256, // CheckGenesis::Implicit = Hash - genesis_h256, // CheckEra::Implicit (Immortal => genesis hash) - (), // CheckNonce::Implicit = () - (), // CheckWeight::Implicit = () - (), // ChargeTransactionPaymentWrapper::Implicit = () - (), // SubtensorTransactionExtension::Implicit = () - (), // DrandPriority::Implicit = () - None, // CheckMetadataHash::Implicit = Option<[u8; 32]> - ); - - // 4) Build the exact signable payload from call + extra + implicit. - let payload: SignedPayload = SignedPayload::from_raw(call.clone(), extra.clone(), implicit); - - // 5) Sign with the local Aura key using the same SCALE bytes the runtime expects. - let sig_opt = payload - .using_encoded(|bytes| keystore.sr25519_sign(AURA_KEY_TYPE, &aura_pub, bytes)) - .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(); - - // 6) Sender address = AccountId32 derived from the Aura sr25519 public key. - let who: AccountId32 = aura_pub.into(); - let address = sp_runtime::MultiAddress::Id(who); - - // 7) Assemble the signed extrinsic and submit it to the pool. - let uxt: UncheckedExtrinsic = UncheckedExtrinsic::new_signed(call, address, signature, extra); + // Build UNSIGNED extrinsic (origin = None) using Frontier's `new_bare`. + let uxt: UncheckedExtrinsic = UncheckedExtrinsic::new_bare(call); let xt_bytes = uxt.encode(); - let xt_hash = sp_core::hashing::blake2_256(&xt_bytes); + let xt_hash = blake2_256(&xt_bytes); let xt_hash_hex = hex::encode(xt_hash); let opaque: sp_runtime::OpaqueExtrinsic = uxt.into(); let xt: ::Extrinsic = opaque.into(); - pool.submit_one(info.best_hash, TransactionSource::Local, xt) + let best_hash = client.info().best_hash; + pool.submit_one(best_hash, TransactionSource::Local, xt) .await?; log::debug!( target: "mev-shield", - "announce_next_key submitted: xt=0x{xt_hash_hex}, nonce={nonce:?}", + "announce_next_key (unsigned) submitted: xt=0x{xt_hash_hex}", ); Ok(()) diff --git a/pallets/shield/src/benchmarking.rs b/pallets/shield/src/benchmarking.rs index a88061287e..9240e6651c 100644 --- a/pallets/shield/src/benchmarking.rs +++ b/pallets/shield/src/benchmarking.rs @@ -48,22 +48,13 @@ mod benches { /// 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); - // Measure: dispatch the extrinsic. + // Dispatch as UNSIGNED: the pallet expects `ensure_none(origin)?;`. #[extrinsic_call] - announce_next_key(RawOrigin::Signed(alice_acc.clone()), public_key.clone()); + announce_next_key(RawOrigin::None, 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/lib.rs b/pallets/shield/src/lib.rs index 831ea7e0a2..dbb2bf3ea2 100644 --- a/pallets/shield/src/lib.rs +++ b/pallets/shield/src/lib.rs @@ -249,15 +249,14 @@ pub mod pallet { .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)), DispatchClass::Operational, - Pays::Yes + Pays::No ))] #[allow(clippy::useless_conversion)] pub fn announce_next_key( origin: OriginFor, public_key: BoundedVec>, - ) -> DispatchResultWithPostInfo { - // Only a current Aura validator may call this (signed account ∈ Aura authorities) - T::AuthorityOrigin::ensure_validator(origin)?; + ) -> DispatchResult { + ensure_none(origin)?; const MAX_KYBER768_PK_LENGTH: usize = 1184; ensure!( @@ -267,11 +266,7 @@ pub mod pallet { NextKey::::put(public_key); - // Refund the fee on success by setting pays_fee = Pays::No - Ok(PostDispatchInfo { - actual_weight: None, - pays_fee: Pays::No, - }) + Ok(()) } /// Users submit an encrypted wrapper. @@ -348,11 +343,12 @@ pub mod pallet { DispatchClass::Normal, Pays::No ))] + #[allow(clippy::useless_conversion)] pub fn mark_decryption_failed( origin: OriginFor, id: T::Hash, reason: BoundedVec>, - ) -> DispatchResult { + ) -> DispatchResultWithPostInfo { // Unsigned: only the author node may inject this via ValidateUnsigned. ensure_none(origin)?; @@ -364,7 +360,7 @@ pub mod pallet { // Emit event to notify clients Self::deposit_event(Event::DecryptionFailed { id, reason }); - Ok(()) + Ok(().into()) } } @@ -387,6 +383,19 @@ pub mod pallet { _ => InvalidTransaction::Call.into(), } } + Call::announce_next_key { public_key, .. } => { + match source { + TransactionSource::Local | TransactionSource::InBlock => { + ValidTransaction::with_tag_prefix("mev-shield-failed") + .priority(10_000u64) + .longevity(4) + .and_provides(public_key) // dedupe by public_key + .propagate(true) + .build() + } + _ => InvalidTransaction::Call.into(), + } + } _ => InvalidTransaction::Call.into(), } } diff --git a/pallets/shield/src/tests.rs b/pallets/shield/src/tests.rs index 18eda7eacc..7af08c269b 100644 --- a/pallets/shield/src/tests.rs +++ b/pallets/shield/src/tests.rs @@ -7,8 +7,7 @@ use frame_support::{ }; use frame_system::pallet_prelude::BlockNumberFor; use pallet_mev_shield::{ - Call as MevShieldCall, CurrentKey, Event as MevShieldEvent, KeyHashByBlock, NextKey, - Submissions, + CurrentKey, Event as MevShieldEvent, KeyHashByBlock, NextKey, Submissions, }; use sp_core::{Pair, sr25519}; use sp_runtime::{ @@ -41,25 +40,13 @@ fn authority_can_announce_next_key_and_on_initialize_rolls_it_and_records_epoch_ 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); - + // Initially there is no current or next key. assert!(CurrentKey::::get().is_none()); assert!(NextKey::::get().is_none()); - // Signed by an Aura validator -> passes TestAuthorityOrigin::ensure_validator. + // The call is now UNSIGNED-ONLY: origin must be `RuntimeOrigin::none()`. assert_ok!(MevShield::announce_next_key( - RuntimeOrigin::signed(validator_account.clone()), + RuntimeOrigin::none(), bounded_pk.clone(), )); @@ -91,23 +78,13 @@ fn announce_next_key_rejects_non_validator_origins() { new_test_ext().execute_with(|| { const KYBER_PK_LEN: usize = 1184; - // Validator account: bytes match the Aura authority we put into storage. + // Two arbitrary accounts (one we used to think of as "validator", one as "non‑validator"). 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()); @@ -121,19 +98,22 @@ fn announce_next_key_rejects_non_validator_origins() { sp_runtime::DispatchError::BadOrigin ); - // 2) Unsigned origin must also fail with BadOrigin. + // 2) Signed validator origin ALSO fails with BadOrigin: this call is unsigned-only now. assert_noop!( - MevShield::announce_next_key(RuntimeOrigin::none(), bounded_pk.clone(),), + MevShield::announce_next_key( + RuntimeOrigin::signed(validator_account.clone()), + bounded_pk.clone(), + ), sp_runtime::DispatchError::BadOrigin ); - // 3) Signed validator origin succeeds (sanity check). + // 3) Unsigned origin (RuntimeOrigin::none()) succeeds. assert_ok!(MevShield::announce_next_key( - RuntimeOrigin::signed(validator_account.clone()), + RuntimeOrigin::none(), bounded_pk.clone(), )); - let next = NextKey::::get().expect("NextKey must be set by validator"); + let next = NextKey::::get().expect("NextKey must be set by unsigned origin"); assert_eq!(next, pk_bytes); }); } @@ -372,72 +352,3 @@ fn mark_decryption_failed_removes_submission_and_emits_event() { assert_noop!(res, pallet_mev_shield::Error::::MissingSubmission); }); } - -#[test] -fn announce_next_key_charges_then_refunds_fee() { - new_test_ext().execute_with(|| { - const KYBER_PK_LEN: usize = 1184; - - // --------------------------------------------------------------------- - // 1. Seed Aura authorities with a single validator and derive account. - // --------------------------------------------------------------------- - let validator_pair = test_sr25519_pair(); - let validator_account: AccountId32 = validator_pair.public().into(); - let validator_aura_id: ::AuthorityId = - validator_pair.public().into(); - - let authorities: BoundedVec< - ::AuthorityId, - ::MaxAuthorities, - > = BoundedVec::truncate_from(vec![validator_aura_id]); - pallet_aura::Authorities::::put(authorities); - - // --------------------------------------------------------------------- - // 2. Build a valid Kyber public key and the corresponding RuntimeCall. - // --------------------------------------------------------------------- - let pk_bytes = vec![42u8; KYBER_PK_LEN]; - let bounded_pk: BoundedVec> = - BoundedVec::truncate_from(pk_bytes.clone()); - - let runtime_call = RuntimeCall::MevShield(MevShieldCall::::announce_next_key { - public_key: bounded_pk.clone(), - }); - - // --------------------------------------------------------------------- - // 3. Pre-dispatch: DispatchInfo must say Pays::Yes. - // --------------------------------------------------------------------- - let pre_info = ::get_dispatch_info( - &runtime_call, - ); - - assert_eq!( - pre_info.pays_fee, - frame_support::dispatch::Pays::Yes, - "announce_next_key must be declared as fee-paying at pre-dispatch" - ); - - // --------------------------------------------------------------------- - // 4. Dispatch via the pallet function. - // --------------------------------------------------------------------- - let post = MevShield::announce_next_key( - RuntimeOrigin::signed(validator_account.clone()), - bounded_pk.clone(), - ) - .expect("announce_next_key should succeed for an Aura validator"); - - // Post-dispatch info should switch pays_fee from Yes -> No (refund). - assert_eq!( - post.pays_fee, - frame_support::dispatch::Pays::No, - "announce_next_key must refund the previously chargeable fee" - ); - - // And we don't override the actual weight (None => use pre-dispatch weight). - assert!( - post.actual_weight.is_none(), - "announce_next_key should not override actual_weight in PostDispatchInfo" - ); - let next = NextKey::::get().expect("NextKey should be set by announce_next_key"); - assert_eq!(next, pk_bytes); - }); -} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 8036969927..340361cf05 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: 358, + spec_version: 359, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,