From 260edeb7acbe221f0c8981ae5319e9bcbb64b9a9 Mon Sep 17 00:00:00 2001 From: jaypan Date: Wed, 17 Jun 2026 17:41:15 +0200 Subject: [PATCH 01/14] fix(runtime): require council 3/5 to authorize Treasury::spend The stable2503 treasury rework gates Treasury::spend behind SpendOrigin. es-dev-sync set SpendOrigin to council >1/2 (reusing RootOrTreasuryCouncilOrigin, which is the RejectOrigin threshold). Restore the pre-stable2503 ApproveOrigin threshold of a council 3/5 supermajority by introducing a dedicated RootOrTreasuryCouncilSpendOrigin (Root or EnsureProportionAtLeast<3,5>) and pointing SpendOrigin at it. RejectOrigin keeps the looser >1/2. Applied consistently across peaq, krest and peaq-dev. Rationale: 3/5 matches historical on-chain governance for spend approval; chosen over es-dev-sync's >1/2 per maintainer decision. --- runtime/krest/src/lib.rs | 9 ++++++++- runtime/peaq-dev/src/lib.rs | 9 ++++++++- runtime/peaq/src/lib.rs | 9 ++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/runtime/krest/src/lib.rs b/runtime/krest/src/lib.rs index 4c6fbf85..fab4eff1 100644 --- a/runtime/krest/src/lib.rs +++ b/runtime/krest/src/lib.rs @@ -609,6 +609,13 @@ type RootOrTreasuryCouncilOrigin = EitherOfDiverse< pallet_collective::EnsureProportionMoreThan, >; +// Treasury spends require a council 3/5 supermajority (or root), matching the +// pre-stable2503 ApproveOrigin threshold. RejectOrigin keeps the looser >1/2. +type RootOrTreasuryCouncilSpendOrigin = EitherOfDiverse< + EnsureRoot, + pallet_collective::EnsureProportionAtLeast, +>; + impl pallet_treasury::Config for Runtime { type PalletId = TreasuryPalletId; type Currency = Balances; @@ -619,7 +626,7 @@ impl pallet_treasury::Config for Runtime { type SpendFunds = (); type WeightInfo = pallet_treasury::weights::SubstrateWeight; type MaxApprovals = MaxApprovals; - type SpendOrigin = EnsureWithSuccess; + type SpendOrigin = EnsureWithSuccess; type RuntimeEvent = RuntimeEvent; type AssetKind = (); diff --git a/runtime/peaq-dev/src/lib.rs b/runtime/peaq-dev/src/lib.rs index 2cdd92a3..ca84e14a 100644 --- a/runtime/peaq-dev/src/lib.rs +++ b/runtime/peaq-dev/src/lib.rs @@ -614,6 +614,13 @@ type RootOrTreasuryCouncilOrigin = EitherOfDiverse< pallet_collective::EnsureProportionMoreThan, >; +// Treasury spends require a council 3/5 supermajority (or root), matching the +// pre-stable2503 ApproveOrigin threshold. RejectOrigin keeps the looser >1/2. +type RootOrTreasuryCouncilSpendOrigin = EitherOfDiverse< + EnsureRoot, + pallet_collective::EnsureProportionAtLeast, +>; + impl pallet_treasury::Config for Runtime { type PalletId = TreasuryPalletId; type Currency = Balances; @@ -624,7 +631,7 @@ impl pallet_treasury::Config for Runtime { type SpendFunds = (); type WeightInfo = pallet_treasury::weights::SubstrateWeight; type MaxApprovals = MaxApprovals; - type SpendOrigin = EnsureWithSuccess; + type SpendOrigin = EnsureWithSuccess; type RuntimeEvent = RuntimeEvent; type AssetKind = (); diff --git a/runtime/peaq/src/lib.rs b/runtime/peaq/src/lib.rs index 1bb14a0a..c811ae8b 100644 --- a/runtime/peaq/src/lib.rs +++ b/runtime/peaq/src/lib.rs @@ -635,6 +635,13 @@ type RootOrTreasuryCouncilOrigin = EitherOfDiverse< pallet_collective::EnsureProportionMoreThan, >; +// Treasury spends require a council 3/5 supermajority (or root), matching the +// pre-stable2503 ApproveOrigin threshold. RejectOrigin keeps the looser >1/2. +type RootOrTreasuryCouncilSpendOrigin = EitherOfDiverse< + EnsureRoot, + pallet_collective::EnsureProportionAtLeast, +>; + impl pallet_treasury::Config for Runtime { type PalletId = TreasuryPalletId; type Currency = Balances; @@ -645,7 +652,7 @@ impl pallet_treasury::Config for Runtime { type SpendFunds = (); type WeightInfo = pallet_treasury::weights::SubstrateWeight; type MaxApprovals = MaxApprovals; - type SpendOrigin = EnsureWithSuccess; + type SpendOrigin = EnsureWithSuccess; type RuntimeEvent = RuntimeEvent; type AssetKind = (); From 0803694185222e3e371ba8b92005cd21f3ee612f Mon Sep 17 00:00:00 2001 From: jaypan Date: Wed, 17 Jun 2026 17:41:25 +0200 Subject: [PATCH 02/14] feat(node): lookahead collator + shared eth pubsub sink for transitional binary Two changes required to run the stable2503 binary against the live v1.7.2 runtime wasm (decoupled, binary-first upgrade): 1. Switch the parachain collator from slot_based to lookahead. slot_based (elastic scaling) needs the CoreSelector/claim-queue runtime API that the old 1.7.2 wasm does not expose; lookahead (async backing) is compatible. Drops the SlotBasedBlockImport handle wiring throughout new_partial and start_node, and spawns lookahead::run as an essential task. 2. Share one pubsub_notification_sinks Arc between MappingSyncWorker and the EthPubSub RPC. They were each constructing their own sink, so eth_subscribe("newHeads"/"logs") returned a sub ID but never delivered notifications. Thread the worker's sink through FullDeps. Rationale: ported as targeted hunks onto es-dev-sync (not a wholesale file copy from the old lookahead branch) to preserve es-dev-sync's service-layer improvements and avoid pulling in its command_sink/xcm_senders FullDeps fields. --- node/src/parachain/service.rs | 47 +++++++++++++---------------------- node/src/rpc.rs | 14 +++++++---- 2 files changed, 26 insertions(+), 35 deletions(-) diff --git a/node/src/parachain/service.rs b/node/src/parachain/service.rs index 88049293..f731b2a3 100644 --- a/node/src/parachain/service.rs +++ b/node/src/parachain/service.rs @@ -1,9 +1,6 @@ //! Parachain Service and ServiceFactory implementation. use cumulus_client_cli::CollatorOptions; -use cumulus_client_consensus_aura::collators::slot_based::{ - self as slot_based, Params as SlotBasedParams, SlotBasedBlockImport as TSlotBasedBlockImport, - SlotBasedBlockImportHandle, -}; +use cumulus_client_consensus_aura::collators::lookahead::{self as lookahead, Params as LookaheadParams}; use cumulus_client_consensus_common::ParachainBlockImport as TParachainBlockImport; use cumulus_client_consensus_relay_chain::Verifier as RelayChainVerifier; use cumulus_client_service::{ @@ -82,10 +79,7 @@ type ParachainClient = TFullClient; type FrontierBlockImport = - TFrontierBlockImport, ParachainClient>; - -type SlotBasedBlockImport = - TSlotBasedBlockImport>, ParachainClient>; + TFrontierBlockImport>, ParachainClient>; type ParachainBlockImport = TParachainBlockImport, ParachainBackend>; @@ -98,8 +92,6 @@ type Service = PartialComponents< sc_transaction_pool::TransactionPoolHandle>, ( ParachainBlockImport, - SlotBasedBlockImportHandle, - // SlotBasedBlockImport, ParachainClient>, Option, Option, Option, @@ -198,11 +190,10 @@ where let executor = sc_service::new_wasm_executor(&config.executor); let (client, backend, keystore_container, task_manager) = - sc_service::new_full_parts_record_import::( + sc_service::new_full_parts::( config, telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()), executor, - true, )?; let client = Arc::new(client); @@ -227,12 +218,9 @@ where .with_prometheus(config.prometheus_registry()) .build(); - let (slot_based_block_import, slot_based_handle) = - SlotBasedBlockImport::new(client.clone(), client.clone()); - let frontier_block_import = - FrontierBlockImport::new(slot_based_block_import.clone(), client.clone()); + let frontier_block_import = FrontierBlockImport::new(client.clone(), client.clone()); let parachain_block_import = - ParachainBlockImport::new(frontier_block_import.clone(), backend.clone()); + ParachainBlockImport::new(frontier_block_import, backend.clone()); let import_queue = fn_build_import_queue( client.clone(), @@ -253,7 +241,6 @@ where select_chain: (), other: ( parachain_block_import, - slot_based_handle, filter_pool, telemetry, telemetry_worker_handle, @@ -352,7 +339,6 @@ where KeystorePtr, ParaId, CollatorPair, - SlotBasedBlockImportHandle, ) -> Result<(), sc_service::Error>, { let mut parachain_config = prepare_node_config(parachain_config); @@ -363,7 +349,6 @@ where )?; let ( parachain_block_import, - slot_based_handle, filter_pool, mut telemetry, telemetry_worker_handle, @@ -506,6 +491,7 @@ where let overrides = overrides.clone(); let fee_history_cache = fee_history_cache.clone(); let block_data_cache = block_data_cache.clone(); + let pubsub_notification_sinks = pubsub_notification_sinks.clone(); move |subscription_task_executor| { let deps = crate::rpc::FullDeps { @@ -527,6 +513,7 @@ where overrides: overrides.clone(), block_data_cache: block_data_cache.clone(), forced_parent_hashes: None, + pubsub_notification_sinks: pubsub_notification_sinks.clone(), }; if ethapi_cmd.contains(&EthApiCmd::Debug) || ethapi_cmd.contains(&EthApiCmd::Trace) { @@ -602,7 +589,6 @@ where params.keystore_container.keystore(), id, collator_key.expect("Command line arguments do not allow this. qed"), - slot_based_handle, )?; } @@ -751,8 +737,7 @@ where sync_oracle, keystore, para_id, - collator_key, - block_import_handle| { + collator_key| { let spawn_handle = task_manager.spawn_handle(); let proposer_factory = sc_basic_authorship::ProposerFactory::with_proof_recording( @@ -763,6 +748,10 @@ where telemetry.clone(), ); + let overseer_handle = relay_chain_interface + .overseer_handle() + .map_err(|e| sc_service::Error::Application(Box::new(e)))?; + let announce_block = { let sync_service = sync_oracle.clone(); Arc::new(move |hash, data| sync_service.announce_block(hash, data)) @@ -775,7 +764,7 @@ where client.clone(), ); - let params = SlotBasedParams { + let params = LookaheadParams { create_inherent_data_providers: move |_, ()| async move { Ok(()) }, block_import: block_import.clone(), para_client: client.clone(), @@ -787,6 +776,7 @@ where keystore, collator_key, para_id, + overseer_handle, // [TODO] max_pov_percentage: Some(85), relay_chain_slot_duration: Duration::from_secs(6), @@ -796,13 +786,9 @@ where authoring_duration: Duration::from_millis(2000), // collation_request_receiver: None, reinitialize: false, - slot_offset: Duration::from_secs(1), - spawner: task_manager.spawn_handle(), - export_pov: None, - block_import_handle, }; - slot_based::run::< + let fut = lookahead::run::< Block, sp_consensus_aura::sr25519::AuthorityPair, _, @@ -813,9 +799,10 @@ where _, _, _, - _, >(params); + task_manager.spawn_essential_handle().spawn("aura", None, fut); + Ok(()) }, ) diff --git a/node/src/rpc.rs b/node/src/rpc.rs index b241c111..b431c13a 100644 --- a/node/src/rpc.rs +++ b/node/src/rpc.rs @@ -92,6 +92,14 @@ pub struct FullDeps { pub block_data_cache: Arc>, /// Mandated parent hashes for a given block hash. pub forced_parent_hashes: Option>, + /// Shared sink list — must be the same Arc passed to `MappingSyncWorker`, + /// otherwise `eth_subscribe("newHeads"/"logs")` returns a sub ID but never + /// delivers notifications. + pub pubsub_notification_sinks: Arc< + fc_mapping_sync::EthereumBlockNotificationSinks< + fc_mapping_sync::EthereumBlockNotification, + >, + >, } pub struct TracingConfig { @@ -161,6 +169,7 @@ where overrides, block_data_cache, forced_parent_hashes, + pubsub_notification_sinks, } = deps; io.merge(System::new(Arc::clone(&client), Arc::clone(&pool)).into_rpc())?; @@ -251,11 +260,6 @@ where .into_rpc(), )?; - let pubsub_notification_sinks: fc_mapping_sync::EthereumBlockNotificationSinks< - fc_mapping_sync::EthereumBlockNotification, - > = Default::default(); - let pubsub_notification_sinks = Arc::new(pubsub_notification_sinks); - io.merge(PeaqStorage::new(Arc::clone(&client)).into_rpc())?; io.merge(PeaqDID::new(Arc::clone(&client)).into_rpc())?; io.merge(PeaqRBAC::new(Arc::clone(&client)).into_rpc())?; From 4ce561bf3fe944fee29703c7caf69e34b10a05c2 Mon Sep 17 00:00:00 2001 From: jaypan Date: Wed, 17 Jun 2026 17:57:03 +0200 Subject: [PATCH 03/14] chore(peaq): bump spec_version 111 -> 112 for stable2503 forkless upgrade Live peaq mainnet is on spec 110 (verified via RPC); tag peaq-v0.0.111 (spec 111) exists but is not yet deployed. Bump to 112 so the stable2503 forkless upgrade is strictly greater than both the current on-chain value and the pending v0.0.111 release, avoiding a version collision. krest and peaq-dev not bumped here (krest live spec unverified; peaq-dev is non-mainnet). --- runtime/peaq/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/peaq/src/lib.rs b/runtime/peaq/src/lib.rs index c811ae8b..08bd31a5 100644 --- a/runtime/peaq/src/lib.rs +++ b/runtime/peaq/src/lib.rs @@ -210,7 +210,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: 111, + spec_version: 112, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From 4b83d07f8ad0e25479fc093d21ceb4f4a401bebd Mon Sep 17 00:00:00 2001 From: jaypan Date: Fri, 19 Jun 2026 10:48:20 +0200 Subject: [PATCH 04/14] chore: clean up review-flagged comments in transitional binary changes Quality-pipeline back-half (code review / simplify / security / cross-AI via Codex) on the consolidation commits: PASS, no CRITICAL/HIGH. Only the LOW cosmetic findings are actioned here: - remove dead `// [TODO]` and `// collation_request_receiver: None,` left over from the slot_based->lookahead migration in node/src/parachain/service.rs - reword the treasury SpendOrigin comment in peaq/krest/peaq-dev to state the threshold decision accurately: the stable2503 base gated spends at council >1/2; deliberately tightened to council 3/5 here. (Replaces the loose "pre-stable2503 ApproveOrigin" wording.) Comment-only change; behaviour unchanged. Verified: cargo check -p peaq-node -p peaq-runtime -p peaq-krest-runtime -p peaq-dev-runtime exit 0. --- node/src/parachain/service.rs | 2 -- runtime/krest/src/lib.rs | 5 +++-- runtime/peaq-dev/src/lib.rs | 5 +++-- runtime/peaq/src/lib.rs | 5 +++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/node/src/parachain/service.rs b/node/src/parachain/service.rs index f731b2a3..9d4503e3 100644 --- a/node/src/parachain/service.rs +++ b/node/src/parachain/service.rs @@ -777,14 +777,12 @@ where collator_key, para_id, overseer_handle, - // [TODO] max_pov_percentage: Some(85), relay_chain_slot_duration: Duration::from_secs(6), proposer: cumulus_client_consensus_proposer::Proposer::new(proposer_factory), collator_service, // We got around 1500ms for proposing authoring_duration: Duration::from_millis(2000), - // collation_request_receiver: None, reinitialize: false, }; diff --git a/runtime/krest/src/lib.rs b/runtime/krest/src/lib.rs index fab4eff1..e3be76ef 100644 --- a/runtime/krest/src/lib.rs +++ b/runtime/krest/src/lib.rs @@ -609,8 +609,9 @@ type RootOrTreasuryCouncilOrigin = EitherOfDiverse< pallet_collective::EnsureProportionMoreThan, >; -// Treasury spends require a council 3/5 supermajority (or root), matching the -// pre-stable2503 ApproveOrigin threshold. RejectOrigin keeps the looser >1/2. +// Treasury spends require root or a council 3/5 supermajority. The stable2503 +// base gated this at council >1/2; deliberately tightened to 3/5 here. +// RejectOrigin is intentionally left at the looser >1/2. type RootOrTreasuryCouncilSpendOrigin = EitherOfDiverse< EnsureRoot, pallet_collective::EnsureProportionAtLeast, diff --git a/runtime/peaq-dev/src/lib.rs b/runtime/peaq-dev/src/lib.rs index ca84e14a..7a259553 100644 --- a/runtime/peaq-dev/src/lib.rs +++ b/runtime/peaq-dev/src/lib.rs @@ -614,8 +614,9 @@ type RootOrTreasuryCouncilOrigin = EitherOfDiverse< pallet_collective::EnsureProportionMoreThan, >; -// Treasury spends require a council 3/5 supermajority (or root), matching the -// pre-stable2503 ApproveOrigin threshold. RejectOrigin keeps the looser >1/2. +// Treasury spends require root or a council 3/5 supermajority. The stable2503 +// base gated this at council >1/2; deliberately tightened to 3/5 here. +// RejectOrigin is intentionally left at the looser >1/2. type RootOrTreasuryCouncilSpendOrigin = EitherOfDiverse< EnsureRoot, pallet_collective::EnsureProportionAtLeast, diff --git a/runtime/peaq/src/lib.rs b/runtime/peaq/src/lib.rs index 08bd31a5..61597c14 100644 --- a/runtime/peaq/src/lib.rs +++ b/runtime/peaq/src/lib.rs @@ -635,8 +635,9 @@ type RootOrTreasuryCouncilOrigin = EitherOfDiverse< pallet_collective::EnsureProportionMoreThan, >; -// Treasury spends require a council 3/5 supermajority (or root), matching the -// pre-stable2503 ApproveOrigin threshold. RejectOrigin keeps the looser >1/2. +// Treasury spends require root or a council 3/5 supermajority. The stable2503 +// base gated this at council >1/2; deliberately tightened to 3/5 here. +// RejectOrigin is intentionally left at the looser >1/2. type RootOrTreasuryCouncilSpendOrigin = EitherOfDiverse< EnsureRoot, pallet_collective::EnsureProportionAtLeast, From d981ad190ec2e1911a046308ad26eac5702c5c1b Mon Sep 17 00:00:00 2001 From: jaypan Date: Fri, 19 Jun 2026 11:46:51 +0200 Subject: [PATCH 05/14] chore(runtime): bump transaction_version (peaq, peaq-dev) for stable2503 call reordering stable2503 reorders dispatchable call indices within several pallets - most critically pallet_treasury, where the same index maps to a different call (live spec 110: idx0=propose_spend; new: idx0=spend_local, idx2 approve_proposal -> spend, etc.). A client using cached pre-upgrade metadata would silently mis-encode/mis-dispatch these calls. Bumping transaction_version forces tooling to refresh metadata so reordered calls encode correctly (fail-loud instead of silent mis-dispatch). - peaq: transaction_version 1 -> 2 (spec_version already 112) - peaq-dev: transaction_version 1 -> 2, spec_version 107 -> 108 (a transaction_version bump must be paired with a spec_version bump) krest not changed (deferred this round). Evidence: metadata call-index diff of live spec-110 vs rig spec-112 (Treasury/ParachainSystem reordered; Assets/Balances/Council/XCM/Scheduler/Utility appended new calls). --- runtime/peaq-dev/src/lib.rs | 4 ++-- runtime/peaq/src/lib.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/runtime/peaq-dev/src/lib.rs b/runtime/peaq-dev/src/lib.rs index 7a259553..fa5061ff 100644 --- a/runtime/peaq-dev/src/lib.rs +++ b/runtime/peaq-dev/src/lib.rs @@ -184,10 +184,10 @@ 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: 107, + spec_version: 108, impl_version: 1, apis: RUNTIME_API_VERSIONS, - transaction_version: 1, + transaction_version: 2, system_version: 1, }; diff --git a/runtime/peaq/src/lib.rs b/runtime/peaq/src/lib.rs index 61597c14..186e92e9 100644 --- a/runtime/peaq/src/lib.rs +++ b/runtime/peaq/src/lib.rs @@ -213,7 +213,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_version: 112, impl_version: 1, apis: RUNTIME_API_VERSIONS, - transaction_version: 1, + transaction_version: 2, system_version: 1, }; From 290638204fff91c708923dc257823397fa7d1e93 Mon Sep 17 00:00:00 2001 From: jaypan Date: Sun, 21 Jun 2026 08:40:22 +0200 Subject: [PATCH 06/14] docs(peaq): fix stale EVM gas-limit comment (75% -> 90%, 15M -> 18M) NORMAL_DISPATCH_RATIO was deliberately raised 75% -> 90% (commit 7f265868 "90pc dispatch ratio and 10MB PoV size"), which raises the derived BlockGasLimit from ~60M to ~72M and the GAS_PER_SECOND-based estimate from ~15M to ~18M. The doc comment above GAS_PER_SECOND still cited the old 0.75 / 15M figures. Comment-only; no behaviour change. --- runtime/peaq/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/peaq/src/lib.rs b/runtime/peaq/src/lib.rs index 186e92e9..a8d7bbea 100644 --- a/runtime/peaq/src/lib.rs +++ b/runtime/peaq/src/lib.rs @@ -697,8 +697,8 @@ impl> FindAuthor for FindAuthorTruncated { /// Current approximation of the gas/s consumption considering /// EVM execution over compiled WASM (on 4.4Ghz CPU). -/// Given the 500ms Weight, from which 75% only are used for transactions, -/// the total EVM execution gas limit is: GAS_PER_SECOND * 0.500 * 0.75 ~= 15_000_000. +/// Given the 500ms Weight, from which 90% only are used for transactions, +/// the total EVM execution gas limit is: GAS_PER_SECOND * 0.500 * 0.90 ~= 18_000_000. pub const GAS_PER_SECOND: u64 = 40_000_000; /// Approximate ratio of the amount of Weight per Gas. From e9f7ac2c2759e9fcc89d44c21aced00c5cc5c323 Mon Sep 17 00:00:00 2001 From: jaypan Date: Wed, 1 Jul 2026 10:05:09 +0200 Subject: [PATCH 07/14] fix: non-native withdraw uses Expendable to restore v1.7.2 reap behavior PeaqMultiCurrenciesWrapper::withdraw (non-native branch) passed Preservation::Protect to fungibles::burn_from, which caps the reducible balance at the asset min_balance floor. A non-native withdraw that drains an account below min_balance (e.g. Zenlink local_withdraw during a swap) then fails with FundsUnavailable, whereas v1.7.2 succeeded and reaped the account (v1.7.2 burn_from hardcoded Expendable internally). Switch to Preservation::Expendable, matching the slash path in the same file and restoring v1.7.2 semantics. Reviewed (rust + security): no CRITICAL/HIGH. Reaping a non-native asset account on a legitimate withdraw is safe; native ED is 0, the relevant floor is the per-asset min_balance. --- runtime/common/src/wrapper.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runtime/common/src/wrapper.rs b/runtime/common/src/wrapper.rs index 0ca4817e..c8f00c38 100644 --- a/runtime/common/src/wrapper.rs +++ b/runtime/common/src/wrapper.rs @@ -146,7 +146,10 @@ where asset_id, who, amount, - Preservation::Protect, // TODO What does this do? + // 1.7.2 對齊:舊 fungibles::burn_from 內部寫死 Expendable(可扣到 0、可 reap)。 + // Protect 會卡 ED 底線 → 舊版會成功的非原生 withdraw 現在 FundsUnavailable。 + // 與同檔 slash 的 Expendable 保持一致。 + Preservation::Expendable, Precision::Exact, Fortitude::Polite, ); From e1fa3d12d0da3de63fd3fdafdf9891adfd91b2ae Mon Sep 17 00:00:00 2001 From: jaypan Date: Wed, 1 Jul 2026 10:05:09 +0200 Subject: [PATCH 08/14] fix: can_withdraw_fee must not execute a DEX swap in the validate phase PeaqMultiCurrenciesOnChargeTransaction::can_withdraw_fee is invoked by pallet-transaction-payment in the validate phase (mempool), which must be side-effect-free. It called ensure_can_withdraw, which executes a real Zenlink swap (inner_swap_assets_for_exact_assets). withdraw_fee (in the prepare phase) also calls ensure_can_withdraw, so a foreign-currency-paid extrinsic swapped twice (double-swap) and mutated state during mempool validation. Use the read-only check_currencies_n_priorities (balance check + AMM quote via get_amount_in_by_path, no swap) in can_withdraw_fee. The real swap happens once, in withdraw_fee during prepare, matching the standard substrate can_withdraw -> withdraw contract. Also drop the stale TODO. Reviewed (rust + security): both SAFE/CORRECT, no fee-evasion path; the validate->prepare TOCTOU is inherent to the substrate fee model and handled (prepare failure drops the tx). Known pre-existing MEDIUM (not introduced here): can_withdraw_fee checks `fee` while withdraw_fee charges `fee + eot_fee`, so an account exactly at the boundary can pass validate but fail prepare; follow-up to pass the EoT-adjusted fee. --- runtime/common/src/payment.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/runtime/common/src/payment.rs b/runtime/common/src/payment.rs index 43773b06..768ab9b0 100644 --- a/runtime/common/src/payment.rs +++ b/runtime/common/src/payment.rs @@ -132,7 +132,6 @@ where Ok(()) } - // [TODO] Need to check... fn can_withdraw_fee( who: &::AccountId, _call: &::RuntimeCall, @@ -143,8 +142,11 @@ where if fee.is_zero() { return Ok(()); } - // Check if user can withdraw in any valid currency. - let currency_id = PCPC::ensure_can_withdraw(who, fee)?; + // Read-only check that the fee is payable in SOME currency, WITHOUT executing the swap. + // can_withdraw_fee runs in `validate` (mempool), which must be side-effect-free; the real + // swap happens later in withdraw_fee (`prepare`). Calling the swap-executing + // ensure_can_withdraw here would swap in validate AND again in withdraw_fee -> double-swap. + let (currency_id, _) = PCPC::check_currencies_n_priorities(who, fee)?; let native_currency_id = PeaqAssetId::default().try_into().ok().unwrap(); if currency_id != native_currency_id { log!( From c654f5ea3bbeb477322a8e9340e8f2119c5bd81e Mon Sep 17 00:00:00 2001 From: jaypan Date: Fri, 3 Jul 2026 10:35:51 +0200 Subject: [PATCH 09/14] docs: translate withdraw preservation comment to English --- runtime/common/src/wrapper.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/runtime/common/src/wrapper.rs b/runtime/common/src/wrapper.rs index c8f00c38..f5a1ffe9 100644 --- a/runtime/common/src/wrapper.rs +++ b/runtime/common/src/wrapper.rs @@ -146,9 +146,10 @@ where asset_id, who, amount, - // 1.7.2 對齊:舊 fungibles::burn_from 內部寫死 Expendable(可扣到 0、可 reap)。 - // Protect 會卡 ED 底線 → 舊版會成功的非原生 withdraw 現在 FundsUnavailable。 - // 與同檔 slash 的 Expendable 保持一致。 + // 1.7.2 parity: the old fungibles::burn_from hardcoded Expendable + // (balance may drop to 0 and the account may be reaped). Protect would + // enforce the ED floor, making non-native withdraws that used to succeed + // fail with FundsUnavailable. Also consistent with slash() below. Preservation::Expendable, Precision::Exact, Fortitude::Polite, From 198ab98a4576c176a0daab19af8317d6db80be66 Mon Sep 17 00:00:00 2001 From: jaypan Date: Fri, 3 Jul 2026 11:15:36 +0200 Subject: [PATCH 10/14] docs(runtime): correct EVM gas-limit comment to 2s weight / 72M MAXIMUM_BLOCK_WEIGHT is WEIGHT_REF_TIME_PER_SECOND*2 (2s), not 500ms, and NORMAL_DISPATCH_RATIO is 90% in all three runtimes, so the block EVM gas limit is GAS_PER_SECOND(40M)*2.0*0.90 = 72,000,000 (matches on-chain gasLimit). peaq-dev/krest comments additionally still said 75% -> 15M, inconsistent with the 90% code; corrected to 72M as well. --- runtime/krest/src/lib.rs | 4 ++-- runtime/peaq-dev/src/lib.rs | 4 ++-- runtime/peaq/src/lib.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/runtime/krest/src/lib.rs b/runtime/krest/src/lib.rs index e3be76ef..fdd50f71 100644 --- a/runtime/krest/src/lib.rs +++ b/runtime/krest/src/lib.rs @@ -671,8 +671,8 @@ impl> FindAuthor for FindAuthorTruncated { /// Current approximation of the gas/s consumption considering /// EVM execution over compiled WASM (on 4.4Ghz CPU). -/// Given the 500ms Weight, from which 75% only are used for transactions, -/// the total EVM execution gas limit is: GAS_PER_SECOND * 0.500 * 0.75 ~= 15_000_000. +/// Given the 2s Weight, from which 90% only are used for transactions, +/// the total EVM execution gas limit is: GAS_PER_SECOND * 2.0 * 0.90 ~= 72_000_000. pub const GAS_PER_SECOND: u64 = 40_000_000; /// Approximate ratio of the amount of Weight per Gas. diff --git a/runtime/peaq-dev/src/lib.rs b/runtime/peaq-dev/src/lib.rs index fa5061ff..18450734 100644 --- a/runtime/peaq-dev/src/lib.rs +++ b/runtime/peaq-dev/src/lib.rs @@ -676,8 +676,8 @@ impl> FindAuthor for FindAuthorTruncated { /// Current approximation of the gas/s consumption considering /// EVM execution over compiled WASM (on 4.4Ghz CPU). -/// Given the 500ms Weight, from which 75% only are used for transactions, -/// the total EVM execution gas limit is: GAS_PER_SECOND * 0.500 * 0.75 ~= 15_000_000. +/// Given the 2s Weight, from which 90% only are used for transactions, +/// the total EVM execution gas limit is: GAS_PER_SECOND * 2.0 * 0.90 ~= 72_000_000. pub const GAS_PER_SECOND: u64 = 40_000_000; /// Approximate ratio of the amount of Weight per Gas. diff --git a/runtime/peaq/src/lib.rs b/runtime/peaq/src/lib.rs index a8d7bbea..0229aa77 100644 --- a/runtime/peaq/src/lib.rs +++ b/runtime/peaq/src/lib.rs @@ -697,8 +697,8 @@ impl> FindAuthor for FindAuthorTruncated { /// Current approximation of the gas/s consumption considering /// EVM execution over compiled WASM (on 4.4Ghz CPU). -/// Given the 500ms Weight, from which 90% only are used for transactions, -/// the total EVM execution gas limit is: GAS_PER_SECOND * 0.500 * 0.90 ~= 18_000_000. +/// Given the 2s Weight, from which 90% only are used for transactions, +/// the total EVM execution gas limit is: GAS_PER_SECOND * 2.0 * 0.90 ~= 72_000_000. pub const GAS_PER_SECOND: u64 = 40_000_000; /// Approximate ratio of the amount of Weight per Gas. From 247136df46e89bb2f409a9ba5eb4085d9c1cccdc Mon Sep 17 00:00:00 2001 From: jaypan Date: Fri, 3 Jul 2026 11:45:50 +0200 Subject: [PATCH 11/14] refactor(payment): rename ensure_can_withdraw -> resolve_and_swap_fee_currency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PCPC helper is named like a read-only check (per the Substrate/ORML ensure_can_withdraw convention) but actually EXECUTES a DEX swap — that misleading name is what let the double-swap bug (calling it from the side-effect-free validate phase) slip in originally. Rename it to state the mutation plainly; the read-only counterpart stays check_currencies_n_priorities. Behavior-neutral (default trait method + its single caller in withdraw_fee); ORML MultiCurrency::ensure_can_withdraw is unrelated and untouched. --- runtime/common/src/payment.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/runtime/common/src/payment.rs b/runtime/common/src/payment.rs index 768ab9b0..91e44044 100644 --- a/runtime/common/src/payment.rs +++ b/runtime/common/src/payment.rs @@ -77,7 +77,7 @@ where let tx_fee = total_fee.saturating_add(eot_fee); // Check if user can withdraw in any valid currency. - let currency_id = PCPC::ensure_can_withdraw(who, tx_fee)?; + let currency_id = PCPC::resolve_and_swap_fee_currency(who, tx_fee)?; let native_currency_id = PeaqAssetId::default().try_into().ok().unwrap(); if currency_id != native_currency_id { log!( @@ -145,7 +145,7 @@ where // Read-only check that the fee is payable in SOME currency, WITHOUT executing the swap. // can_withdraw_fee runs in `validate` (mempool), which must be side-effect-free; the real // swap happens later in withdraw_fee (`prepare`). Calling the swap-executing - // ensure_can_withdraw here would swap in validate AND again in withdraw_fee -> double-swap. + // resolve_and_swap_fee_currency here would swap in validate AND again in withdraw_fee. let (currency_id, _) = PCPC::check_currencies_n_priorities(who, fee)?; let native_currency_id = PeaqAssetId::default().try_into().ok().unwrap(); if currency_id != native_currency_id { @@ -208,9 +208,11 @@ pub trait PeaqMultiCurrenciesPaymentConvert { type AssetIdToZenlinkId: Convert>; - /// This method checks if the fee can be withdrawn in any currency and returns the asset_id - /// of the choosen currency in dependency of the priority-list and availability of tokens. - fn ensure_can_withdraw( + /// Resolves which currency pays the fee (per the priority list) and, if it is a non-native + /// currency, EXECUTES the DEX swap to native so the caller can then withdraw it. This MUTATES + /// chain state, so it must NOT be called from the `validate` phase — use the read-only + /// `check_currencies_n_priorities` there. Returns the asset_id of the chosen source currency. + fn resolve_and_swap_fee_currency( who: &Self::AccountId, tx_fee: BalanceOfA, ) -> Result { From c15aec6ec872529e9672580139cf4d44728231ca Mon Sep 17 00:00:00 2001 From: jaypan Date: Tue, 7 Jul 2026 18:44:28 +0200 Subject: [PATCH 12/14] fix(node): record import proof so importer matches author execution The parachain importer used sc_service::new_full_parts (proof recording off) while the proposer uses ProposerFactory::with_proof_recording. That asymmetry made cumulus-pallet-weight-reclaim write a different frame_system::BlockWeight on author vs importer, which changed the intermediate state root that frontier embeds into the fron digest; every non-author node then failed execute_block final_checks (Digest item must match that calculated) on any block containing a user extrinsic. Restore new_full_parts_record_import(.., true), dropped in 08036941 while porting the lookahead collator (originally added in 6800a326). Client-only fix, no runtime change. --- node/src/parachain/service.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/node/src/parachain/service.rs b/node/src/parachain/service.rs index 9d4503e3..1ac1e03b 100644 --- a/node/src/parachain/service.rs +++ b/node/src/parachain/service.rs @@ -190,10 +190,15 @@ where let executor = sc_service::new_wasm_executor(&config.executor); let (client, backend, keystore_container, task_manager) = - sc_service::new_full_parts::( + sc_service::new_full_parts_record_import::( config, telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()), executor, + // Author uses ProposerFactory::with_proof_recording; the importer MUST also record + // proof so cumulus-pallet-weight-reclaim writes an identical frame_system::BlockWeight + // on both paths. Otherwise the intermediate state root (embedded by frontier into the + // `fron` digest) diverges and execute_block fails final_checks ("Digest item must match"). + true, )?; let client = Arc::new(client); From 3294f8578960b169f8f0b58e6f784e9a3a7c04f3 Mon Sep 17 00:00:00 2001 From: jaypan Date: Wed, 8 Jul 2026 08:03:10 +0200 Subject: [PATCH 13/14] Update the krest runtime due to the consistency Update the krest network's comment + version even we won't use this anymore, but we can remove this runtime in our next release --- runtime/krest/src/lib.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/runtime/krest/src/lib.rs b/runtime/krest/src/lib.rs index fdd50f71..b008eac3 100644 --- a/runtime/krest/src/lib.rs +++ b/runtime/krest/src/lib.rs @@ -1,4 +1,7 @@ #![cfg_attr(not(feature = "std"), no_std)] +// Currently, we sunset the krest network. Therefore, after this release, we can remove the +// krest runtime module to speed up the build time. + // `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. #![recursion_limit = "256"] @@ -183,10 +186,10 @@ 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: 107, + spec_version: 108, impl_version: 1, apis: RUNTIME_API_VERSIONS, - transaction_version: 1, + transaction_version: 2, system_version: 1, }; From 5351b8848373769321b170d90ea0b7f51d7110d5 Mon Sep 17 00:00:00 2001 From: jaypan Date: Wed, 8 Jul 2026 08:16:26 +0200 Subject: [PATCH 14/14] Cargo fmt fix --- node/src/parachain/service.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/node/src/parachain/service.rs b/node/src/parachain/service.rs index 1ac1e03b..7798a0a6 100644 --- a/node/src/parachain/service.rs +++ b/node/src/parachain/service.rs @@ -1,6 +1,8 @@ //! Parachain Service and ServiceFactory implementation. use cumulus_client_cli::CollatorOptions; -use cumulus_client_consensus_aura::collators::lookahead::{self as lookahead, Params as LookaheadParams}; +use cumulus_client_consensus_aura::collators::lookahead::{ + self as lookahead, Params as LookaheadParams, +}; use cumulus_client_consensus_common::ParachainBlockImport as TParachainBlockImport; use cumulus_client_consensus_relay_chain::Verifier as RelayChainVerifier; use cumulus_client_service::{ @@ -197,7 +199,8 @@ where // Author uses ProposerFactory::with_proof_recording; the importer MUST also record // proof so cumulus-pallet-weight-reclaim writes an identical frame_system::BlockWeight // on both paths. Otherwise the intermediate state root (embedded by frontier into the - // `fron` digest) diverges and execute_block fails final_checks ("Digest item must match"). + // `fron` digest) diverges and execute_block fails final_checks ("Digest item must + // match"). true, )?; let client = Arc::new(client); @@ -224,8 +227,7 @@ where .build(); let frontier_block_import = FrontierBlockImport::new(client.clone(), client.clone()); - let parachain_block_import = - ParachainBlockImport::new(frontier_block_import, backend.clone()); + let parachain_block_import = ParachainBlockImport::new(frontier_block_import, backend.clone()); let import_queue = fn_build_import_queue( client.clone(),