From c6acbf564e1eeba44158244d94d02619846fd52e Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Fri, 30 Jan 2026 13:37:58 +0100 Subject: [PATCH 1/7] polkadot v2512 upgrade --- Cargo.toml | 16 +++---- examples/aura/node/src/service.rs | 1 + examples/aura/runtime/src/lib.rs | 4 +- examples/babe/node/Cargo.toml | 1 + examples/babe/node/src/service.rs | 61 +++++++++++++++++++------- examples/babe/runtime/src/lib.rs | 10 ++++- examples/parachain/node/src/service.rs | 11 ++--- examples/parachain/runtime/src/lib.rs | 8 ++-- simnode/src/client/aura.rs | 1 + simnode/src/client/babe.rs | 1 + simnode/src/client/parachain.rs | 1 + 11 files changed, 79 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7a347ee..a33087e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,16 +25,16 @@ scale-info = { version = "2.1.1", default-features = false, features = [ "derive", ] } jsonrpsee = "0.24" -polkadot-sdk = { version = "2506.0.0", default-features = false } +polkadot-sdk = { version = "2512.1.0", default-features = false } # crates which cannot be used from polkadot-sdk -sp-core = { version = "37.0.0", default-features = false } -sp-runtime-interface = { version = "30.0.0", default-features = false } -cumulus-pallet-parachain-system = { version = "0.21.0", default-features = false } -substrate-wasm-builder = "27.0.0" -sc-service = "0.52.0" -sc-network-sync = "0.50.0" -sc-tracing = "40.0.0" +sp-core = { version = "39.0.0", default-features = false } +sp-runtime-interface = { version = "33.0.0", default-features = false } +cumulus-pallet-parachain-system = { version = "0.25.0", default-features = false } +substrate-wasm-builder = "31.1.0" +sc-service = "0.56.0" +sc-network-sync = "0.54.0" +sc-tracing = "44.0.0" # local crates simnode-runtime-api = { path = "./runtime-api", version = "2506.0.0", default-features = false } diff --git a/examples/aura/node/src/service.rs b/examples/aura/node/src/service.rs index 51cfb9e..9832d04 100644 --- a/examples/aura/node/src/service.rs +++ b/examples/aura/node/src/service.rs @@ -241,6 +241,7 @@ pub fn new_full(config: Configuration) -> Result { sync_service: sync_service.clone(), config, telemetry: telemetry.as_mut(), + tracing_execute_block: None, })?; if role.is_authority() { diff --git a/examples/aura/runtime/src/lib.rs b/examples/aura/runtime/src/lib.rs index de3fbe7..9ce9110 100644 --- a/examples/aura/runtime/src/lib.rs +++ b/examples/aura/runtime/src/lib.rs @@ -335,7 +335,7 @@ impl_runtime_apis! { VERSION } - fn execute_block(block: Block) { + fn execute_block(block: ::LazyBlock) { Executive::execute_block(block); } @@ -372,7 +372,7 @@ impl_runtime_apis! { } fn check_inherents( - block: Block, + block: ::LazyBlock, data: sp_inherents::InherentData, ) -> sp_inherents::CheckInherentsResult { data.check_extrinsics(&block) diff --git a/examples/babe/node/Cargo.toml b/examples/babe/node/Cargo.toml index 064459b..57318a3 100644 --- a/examples/babe/node/Cargo.toml +++ b/examples/babe/node/Cargo.toml @@ -36,6 +36,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] # third-party dependencies array-bytes = "4.1" +async-trait = "0.1" clap = { version = "4.0.9", features = ["derive"], optional = true } codec = { workspace = true } serde = { version = "1.0.136", features = ["derive"] } diff --git a/examples/babe/node/src/service.rs b/examples/babe/node/src/service.rs index a215b08..360e772 100644 --- a/examples/babe/node/src/service.rs +++ b/examples/babe/node/src/service.rs @@ -51,6 +51,42 @@ type FullSelectChain = sc_consensus::LongestChain; type FullGrandpaBlockImport> = sc_consensus_grandpa::GrandpaBlockImport, FullSelectChain>; +/// Inherent data provider for BABE consensus. +#[derive(Clone)] +pub struct BabeInherentDataProvider { + slot_duration: sp_consensus_babe::SlotDuration, +} + +impl BabeInherentDataProvider { + pub fn new(slot_duration: sp_consensus_babe::SlotDuration) -> Self { + Self { slot_duration } + } +} + +#[async_trait::async_trait] +impl sp_inherents::CreateInherentDataProviders for BabeInherentDataProvider { + type InherentDataProviders = ( + sp_consensus_babe::inherents::InherentDataProvider, + sp_timestamp::InherentDataProvider, + ); + + async fn create_inherent_data_providers( + &self, + _parent: ::Hash, + _extra_args: (), + ) -> Result> { + let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); + let slot = sp_consensus_babe::inherents::InherentDataProvider::from_timestamp_and_slot_duration( + *timestamp, + self.slot_duration, + ); + Ok((slot, timestamp)) + } +} + +type FullBabeBlockImport> = + sc_consensus_babe::BabeBlockImport, FullGrandpaBlockImport, BabeInherentDataProvider, FullSelectChain>; + /// Our native executor instance. pub struct ExecutorDispatch; @@ -166,7 +202,7 @@ pub fn new_partial( sc_rpc::SubscriptionTaskExecutor, ) -> Result, sc_service::Error>, ( - sc_consensus_babe::BabeBlockImport, FullGrandpaBlockImport>, + FullBabeBlockImport, sc_consensus_grandpa::LinkHalf, FullSelectChain>, sc_consensus_babe::BabeLink, ), @@ -226,35 +262,27 @@ where let justification_import = grandpa_block_import.clone(); + let slot_duration = sc_consensus_babe::configuration(&*client)?.slot_duration(); + let inherent_data_provider = BabeInherentDataProvider::new(slot_duration); let (block_import, babe_link) = sc_consensus_babe::block_import( sc_consensus_babe::configuration(&*client)?, grandpa_block_import, client.clone(), + inherent_data_provider, + select_chain.clone(), + OffchainTransactionPoolFactory::new(transaction_pool.clone()), )?; - let slot_duration = babe_link.config().slot_duration(); let (import_queue, babe_worker_handle) = sc_consensus_babe::import_queue(sc_consensus_babe::ImportQueueParams { link: babe_link.clone(), block_import: block_import.clone(), justification_import: Some(Box::new(justification_import)), client: client.clone(), - select_chain: select_chain.clone(), - create_inherent_data_providers: move |_, ()| async move { - let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); - - let slot = - sp_consensus_babe::inherents::InherentDataProvider::from_timestamp_and_slot_duration( - *timestamp, - slot_duration, - ); - - Ok((slot, timestamp)) - }, + slot_duration, spawner: &task_manager.spawn_essential_handle(), registry: config.prometheus_registry(), telemetry: telemetry.as_ref().map(|x| x.handle()), - offchain_tx_pool_factory: OffchainTransactionPoolFactory::new(transaction_pool.clone()), })?; let import_setup = (block_import, grandpa_link, babe_link); @@ -337,7 +365,7 @@ pub fn new_full_base( config: Configuration, disable_hardware_benchmarks: bool, with_startup_data: impl FnOnce( - &sc_consensus_babe::BabeBlockImport, + &FullBabeBlockImport, &sc_consensus_babe::BabeLink, ), ) -> Result { @@ -448,6 +476,7 @@ pub fn new_full_base( tx_handler_controller, sync_service: sync_service.clone(), telemetry: telemetry.as_mut(), + tracing_execute_block: None, })?; if let Some(hwbench) = hwbench { diff --git a/examples/babe/runtime/src/lib.rs b/examples/babe/runtime/src/lib.rs index bbbc84b..7d36915 100644 --- a/examples/babe/runtime/src/lib.rs +++ b/examples/babe/runtime/src/lib.rs @@ -440,6 +440,10 @@ impl_opaque_keys! { } } +parameter_types! { + pub const SessionKeyDeposit: Balance = ExistentialDeposit::get() * 10; +} + impl pallet_session::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ValidatorId = ::AccountId; @@ -451,6 +455,8 @@ impl pallet_session::Config for Runtime { type Keys = SessionKeys; type WeightInfo = pallet_session::weights::SubstrateWeight; type DisablingStrategy = (); + type Currency = Balances; + type KeyDeposit = SessionKeyDeposit; } impl pallet_session::historical::Config for Runtime { @@ -751,7 +757,7 @@ impl_runtime_apis! { VERSION } - fn execute_block(block: Block) { + fn execute_block(block: ::LazyBlock) { Executive::execute_block(block); } @@ -787,7 +793,7 @@ impl_runtime_apis! { data.create_extrinsics() } - fn check_inherents(block: Block, data: InherentData) -> CheckInherentsResult { + fn check_inherents(block: ::LazyBlock, data: InherentData) -> CheckInherentsResult { data.check_extrinsics(&block) } } diff --git a/examples/parachain/node/src/service.rs b/examples/parachain/node/src/service.rs index 6fdcefe..7cf3843 100644 --- a/examples/parachain/node/src/service.rs +++ b/examples/parachain/node/src/service.rs @@ -21,7 +21,6 @@ use cumulus_primitives_core::ParaId; use cumulus_relay_chain_interface::{OverseerHandle, RelayChainInterface}; // Substrate Imports -use cumulus_client_consensus_proposer::Proposer; use frame_benchmarking_cli::SUBSTRATE_REFERENCE_HARDWARE; use futures::FutureExt; use parachain_runtime::opaque::Hash; @@ -29,7 +28,7 @@ use polkadot_primitives::{CollatorPair, ValidationCode}; use sc_client_api::Backend; use sc_consensus::ImportQueue; use sc_executor::{RuntimeVersionOf, WasmExecutor}; -use sc_network::{NetworkBackend, NetworkBlock}; +use sc_network::{NetworkBackend, NetworkBlock, PeerId}; use sc_service::{Configuration, PartialComponents, TFullBackend, TFullClient, TaskManager}; use sc_simnode::parachain::ParachainSelectChain; use sc_telemetry::{Telemetry, TelemetryHandle, TelemetryWorker, TelemetryWorkerHandle}; @@ -245,6 +244,7 @@ async fn start_node_impl( system_rpc_tx, tx_handler_controller, telemetry: telemetry.as_mut(), + tracing_execute_block: None, })?; if let Some(hwbench) = hwbench { @@ -326,6 +326,7 @@ async fn start_node_impl( relay_chain_slot_duration, para_id, collator_key.expect("Command line arguments do not allow this. qed"), + network.local_peer_id(), overseer_handle, announce_block, )?; @@ -390,13 +391,14 @@ fn start_consensus( relay_chain_slot_duration: Duration, para_id: ParaId, collator_key: CollatorPair, + collator_peer_id: PeerId, overseer_handle: OverseerHandle, announce_block: Arc>) + Send + Sync>, ) -> Result<(), sc_service::Error> { // NOTE: because we use Aura here explicitly, we can use `CollatorSybilResistance::Resistant` // when starting the network. - let proposer_factory = sc_basic_authorship::ProposerFactory::with_proof_recording( + let proposer = sc_basic_authorship::ProposerFactory::with_proof_recording( task_manager.spawn_handle(), client.clone(), transaction_pool, @@ -404,8 +406,6 @@ fn start_consensus( telemetry.clone(), ); - let proposer = Proposer::new(proposer_factory); - let collator_service = CollatorService::new( client.clone(), Arc::new(task_manager.spawn_handle()), @@ -424,6 +424,7 @@ fn start_consensus( }, keystore, collator_key, + collator_peer_id, para_id, overseer_handle, reinitialize: true, diff --git a/examples/parachain/runtime/src/lib.rs b/examples/parachain/runtime/src/lib.rs index 281c745..dc7b84f 100644 --- a/examples/parachain/runtime/src/lib.rs +++ b/examples/parachain/runtime/src/lib.rs @@ -390,7 +390,6 @@ impl cumulus_pallet_parachain_system::Config for Runtime { type ReservedXcmpWeight = ReservedXcmpWeight; type CheckAssociatedRelayNumber = RelayNumberStrictlyIncreases; type ConsensusHook = ConsensusHook; - type SelectCore = cumulus_pallet_parachain_system::DefaultCoreSelector; type RelayParentOffset = ConstU32<0>; } @@ -450,6 +449,7 @@ impl cumulus_pallet_xcmp_queue::Config for Runtime { parameter_types! { pub const Period: u32 = 6 * HOURS; pub const Offset: u32 = 0; + pub const SessionKeyDeposit: Balance = EXISTENTIAL_DEPOSIT * 10; } impl pallet_session::Config for Runtime { @@ -466,6 +466,8 @@ impl pallet_session::Config for Runtime { type WeightInfo = (); // Disable validator slots when they get kicked from the CollatorSelection pallet type DisablingStrategy = (); + type Currency = Balances; + type KeyDeposit = SessionKeyDeposit; } impl pallet_aura::Config for Runtime { @@ -568,7 +570,7 @@ impl_runtime_apis! { VERSION } - fn execute_block(block: Block) { + fn execute_block(block: ::LazyBlock) { Executive::execute_block(block) } @@ -605,7 +607,7 @@ impl_runtime_apis! { } fn check_inherents( - block: Block, + block: ::LazyBlock, data: sp_inherents::InherentData, ) -> sp_inherents::CheckInherentsResult { data.check_extrinsics(&block) diff --git a/simnode/src/client/aura.rs b/simnode/src/client/aura.rs index 39b0b5c..ae4e7d0 100644 --- a/simnode/src/client/aura.rs +++ b/simnode/src/client/aura.rs @@ -178,6 +178,7 @@ where tx_handler_controller, sync_service, telemetry: telemetry.as_mut(), + tracing_execute_block: None, }; spawn_tasks(params)?; diff --git a/simnode/src/client/babe.rs b/simnode/src/client/babe.rs index 7f8a798..081131e 100644 --- a/simnode/src/client/babe.rs +++ b/simnode/src/client/babe.rs @@ -184,6 +184,7 @@ where tx_handler_controller, sync_service, telemetry: telemetry.as_mut(), + tracing_execute_block: None, }; spawn_tasks(params)?; diff --git a/simnode/src/client/parachain.rs b/simnode/src/client/parachain.rs index 81de9ee..23bc50a 100644 --- a/simnode/src/client/parachain.rs +++ b/simnode/src/client/parachain.rs @@ -317,6 +317,7 @@ where tx_handler_controller, sync_service, telemetry: telemetry.as_mut(), + tracing_execute_block: None, }; spawn_tasks(params)?; From b139c5e5693d783a20b55e9c7f0a93887286d1ca Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Fri, 30 Jan 2026 17:28:14 +0100 Subject: [PATCH 2/7] bump and ci deps --- .github/workflows/ci.yml | 8 ++++---- runtime-api/Cargo.toml | 2 +- simnode/Cargo.toml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0dcdfd..4073caa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,10 +25,10 @@ jobs: components: rust-src - uses: Swatinem/rust-cache@v2 - - name: Install protoc + - name: Install dependencies run: | sudo apt-get update - sudo apt-get install protobuf-compiler + sudo apt-get install -y protobuf-compiler llvm clang libclang-dev - name: cargo check simnode run: cargo check -p sc-simnode @@ -48,10 +48,10 @@ jobs: - uses: Swatinem/rust-cache@v2 - - name: Install protoc + - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y protobuf-compiler build-essential + sudo apt-get install -y protobuf-compiler build-essential llvm clang libclang-dev - name: Build all binaries run: | diff --git a/runtime-api/Cargo.toml b/runtime-api/Cargo.toml index bceffd0..8dc3feb 100644 --- a/runtime-api/Cargo.toml +++ b/runtime-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simnode-runtime-api" -version = "2506.0.0" +version = "2506.1.0" authors = ["Polytope Labs "] edition = "2021" license = "Apache-2.0" diff --git a/simnode/Cargo.toml b/simnode/Cargo.toml index de84269..5257382 100644 --- a/simnode/Cargo.toml +++ b/simnode/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sc-simnode" -version = "2506.0.0" +version = "2506.1.0" authors = ["Polytope Labs "] edition = "2021" license = "Apache-2.0" From 85e72bdef09b37501012a4e44c527d78984baf0f Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Fri, 30 Jan 2026 18:20:37 +0100 Subject: [PATCH 3/7] update version to match polkadot-sdk version number --- Cargo.toml | 4 ++-- runtime-api/Cargo.toml | 2 +- simnode/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a33087e..74bab87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,5 +37,5 @@ sc-network-sync = "0.54.0" sc-tracing = "44.0.0" # local crates -simnode-runtime-api = { path = "./runtime-api", version = "2506.0.0", default-features = false } -sc-simnode = { path = "./simnode", version = "2506.0.0" } +simnode-runtime-api = { path = "./runtime-api", version = "2512.1.0", default-features = false } +sc-simnode = { path = "./simnode", version = "2512.1.0" } diff --git a/runtime-api/Cargo.toml b/runtime-api/Cargo.toml index 8dc3feb..1585e0e 100644 --- a/runtime-api/Cargo.toml +++ b/runtime-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simnode-runtime-api" -version = "2506.1.0" +version = "2512.1.0" authors = ["Polytope Labs "] edition = "2021" license = "Apache-2.0" diff --git a/simnode/Cargo.toml b/simnode/Cargo.toml index 5257382..cd7a4d2 100644 --- a/simnode/Cargo.toml +++ b/simnode/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sc-simnode" -version = "2506.1.0" +version = "2512.1.0" authors = ["Polytope Labs "] edition = "2021" license = "Apache-2.0" From c9f117850ceaa1de816eae6d54465b773787ad3d Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Fri, 30 Jan 2026 18:23:33 +0100 Subject: [PATCH 4/7] nit --- Cargo.toml | 4 ++-- runtime-api/Cargo.toml | 2 +- simnode/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 74bab87..3278098 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,5 +37,5 @@ sc-network-sync = "0.54.0" sc-tracing = "44.0.0" # local crates -simnode-runtime-api = { path = "./runtime-api", version = "2512.1.0", default-features = false } -sc-simnode = { path = "./simnode", version = "2512.1.0" } +simnode-runtime-api = { path = "./runtime-api", version = "2512.0.0", default-features = false } +sc-simnode = { path = "./simnode", version = "2512.0.0" } diff --git a/runtime-api/Cargo.toml b/runtime-api/Cargo.toml index 1585e0e..f0aeb25 100644 --- a/runtime-api/Cargo.toml +++ b/runtime-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simnode-runtime-api" -version = "2512.1.0" +version = "2512.0.0" authors = ["Polytope Labs "] edition = "2021" license = "Apache-2.0" diff --git a/simnode/Cargo.toml b/simnode/Cargo.toml index cd7a4d2..ca165db 100644 --- a/simnode/Cargo.toml +++ b/simnode/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sc-simnode" -version = "2512.1.0" +version = "2512.0.0" authors = ["Polytope Labs "] edition = "2021" license = "Apache-2.0" From 3b22dab4f14e779a198764e0dee0468feb52cb9d Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Tue, 3 Feb 2026 00:57:36 +0100 Subject: [PATCH 5/7] introduce fork aware proposer factory --- simnode/Cargo.toml | 1 + simnode/src/client/aura.rs | 12 ++++++++---- simnode/src/client/babe.rs | 12 ++++++++---- simnode/src/client/parachain.rs | 23 +++++++++++++++-------- simnode/src/lib.rs | 2 ++ simnode/src/rpc.rs | 28 +++++++++++++++++++++------- 6 files changed, 55 insertions(+), 23 deletions(-) diff --git a/simnode/Cargo.toml b/simnode/Cargo.toml index ca165db..07deffa 100644 --- a/simnode/Cargo.toml +++ b/simnode/Cargo.toml @@ -36,6 +36,7 @@ features = [ "sc-network", "sc-cli", "sc-basic-authorship", + "sc-block-builder", "sc-rpc", "sc-offchain", "sc-tracing", diff --git a/simnode/src/client/aura.rs b/simnode/src/client/aura.rs index ae4e7d0..b6d4dcb 100644 --- a/simnode/src/client/aura.rs +++ b/simnode/src/client/aura.rs @@ -19,7 +19,10 @@ use polkadot_sdk::*; use super::*; -use crate::{timestamp::SlotTimestampProvider, ChainInfo, SimnodeApiServer, SimnodeRpcHandler}; +use crate::{ + proposer::ForkAwareProposerFactory, timestamp::SlotTimestampProvider, ChainInfo, + SimnodeApiServer, SimnodeRpcHandler, +}; use futures::{channel::mpsc, future::Either, FutureExt, StreamExt}; use num_traits::AsPrimitive; use sc_client_api::Backend; @@ -140,12 +143,13 @@ where } // Proposer object for block authorship. - let env = sc_basic_authorship::ProposerFactory::new( + // Use ForkAwareProposerFactory to properly handle building blocks on non-best-chain parents + // (fork scenarios). The standard ProposerFactory uses pool.ready_at(parent) which doesn't + // return transactions as "ready" for non-best parents. + let env = ForkAwareProposerFactory::new( task_manager.spawn_handle(), client.clone(), pool.clone(), - config.prometheus_registry(), - None, ); // Channel for the rpc handler to communicate with the authorship task. diff --git a/simnode/src/client/babe.rs b/simnode/src/client/babe.rs index 081131e..b236fdc 100644 --- a/simnode/src/client/babe.rs +++ b/simnode/src/client/babe.rs @@ -47,7 +47,10 @@ use std::sync::Arc; use simnode_runtime_api::CreateTransactionApi; -use crate::{timestamp::SlotTimestampProvider, ChainInfo, SimnodeApiServer, SimnodeRpcHandler}; +use crate::{ + proposer::ForkAwareProposerFactory, timestamp::SlotTimestampProvider, ChainInfo, + SimnodeApiServer, SimnodeRpcHandler, +}; use super::*; @@ -146,12 +149,13 @@ where } // Proposer object for block authorship. - let env = sc_basic_authorship::ProposerFactory::new( + // Use ForkAwareProposerFactory to properly handle building blocks on non-best-chain parents + // (fork scenarios). The standard ProposerFactory uses pool.ready_at(parent) which doesn't + // return transactions as "ready" for non-best parents. + let env = ForkAwareProposerFactory::new( task_manager.spawn_handle(), client.clone(), pool.clone(), - config.prometheus_registry(), - None, ); // Channel for the rpc handler to communicate with the authorship task. diff --git a/simnode/src/client/parachain.rs b/simnode/src/client/parachain.rs index 23bc50a..088a520 100644 --- a/simnode/src/client/parachain.rs +++ b/simnode/src/client/parachain.rs @@ -18,10 +18,12 @@ use polkadot_sdk::*; +use codec::Decode; + use super::*; use crate::{ - timestamp::SlotTimestampProvider, ChainInfo, ParachainSproofInherentProvider, SimnodeApiServer, - SimnodeRpcHandler, + proposer::ForkAwareProposerFactory, timestamp::SlotTimestampProvider, ChainInfo, + ParachainSproofInherentProvider, SimnodeApiServer, SimnodeRpcHandler, }; use async_trait::async_trait; use futures::{channel::mpsc, future::Either, lock::Mutex, FutureExt, StreamExt}; @@ -47,7 +49,7 @@ use sp_block_builder::BlockBuilder; use sp_blockchain::HeaderBackend; use sp_consensus::SelectChain; use sp_consensus_aura::AuraApi; -use sp_core::{crypto::AccountId32, traits::SpawnEssentialNamed, Bytes}; +use sp_core::{crypto::AccountId32, traits::SpawnEssentialNamed, Bytes, H256}; use sp_runtime::traits::{Block as BlockT, Header}; use sp_transaction_pool::runtime_api::TaggedTransactionQueue; use std::sync::Arc; @@ -78,8 +80,12 @@ where <::Header as Header>::Number: num_traits::cast::AsPrimitive, T::Runtime: staging_parachain_info::Config, { - fn author_extrinsic(&self, call: Bytes, account: String) -> RpcResult { - Ok(self.inner.author_extrinsic(call, account)?.into()) + fn author_extrinsic(&self, call: Bytes, account: String, at: Option) -> RpcResult { + let at = at.map(|h| { + let bytes: [u8; 32] = h.into(); + Decode::decode(&mut &bytes[..]).expect("H256 is 32 bytes, same as block hash") + }); + Ok(self.inner.author_extrinsic(call, account, at)?.into()) } fn revert_blocks(&self, n: u32) -> RpcResult<()> { @@ -273,12 +279,13 @@ where } // Proposer object for block authorship. - let env = sc_basic_authorship::ProposerFactory::new( + // Use ForkAwareProposerFactory to properly handle building blocks on non-best-chain parents + // (fork scenarios). The standard ProposerFactory uses pool.ready_at(parent) which doesn't + // return transactions as "ready" for non-best parents. + let env = ForkAwareProposerFactory::new( task_manager.spawn_handle(), client.clone(), pool.clone(), - config.prometheus_registry(), - None, ); // Channel for the rpc handler to communicate with the authorship task. diff --git a/simnode/src/lib.rs b/simnode/src/lib.rs index ecbc783..4b2c394 100644 --- a/simnode/src/lib.rs +++ b/simnode/src/lib.rs @@ -29,12 +29,14 @@ use std::sync::Arc; pub mod cli; pub mod client; pub mod overrides; +pub mod proposer; pub mod rpc; pub mod sproof; pub use cli::*; pub use client::*; pub use overrides::*; +pub use proposer::*; pub use rpc::*; pub use sproof::*; diff --git a/simnode/src/rpc.rs b/simnode/src/rpc.rs index ad99927..60e4a65 100644 --- a/simnode/src/rpc.rs +++ b/simnode/src/rpc.rs @@ -32,7 +32,7 @@ use sp_api::{ApiExt, ConstructRuntimeApi, ProvideRuntimeApi}; use sp_blockchain::HeaderBackend; use sp_core::{ crypto::{AccountId32, Ss58Codec}, - Bytes, + Bytes, H256, }; use sp_runtime::{ traits::{Block as BlockT, Header}, @@ -46,8 +46,13 @@ use std::sync::Arc; pub trait SimnodeApi { /// Constructs an extrinsic with an empty signature and the given AccountId as the Signer using /// simnode's runtime api. + /// + /// The optional `at` parameter specifies which block's state to use when creating the + /// transaction (for nonce lookup, etc.). If not provided, defaults to the best block. + /// This is useful when building transactions for fork branches that are not the current + /// best chain. #[method(name = "simnode_authorExtrinsic")] - fn author_extrinsic(&self, call: Bytes, account: String) -> RpcResult; + fn author_extrinsic(&self, call: Bytes, account: String, at: Option) -> RpcResult; /// reverts `n` number of blocks and their state from the chain. #[method(name = "simnode_revertBlocks")] @@ -83,8 +88,13 @@ where Self { client, backend } } - fn author_extrinsic(&self, call: Bytes, account: String) -> RpcResult> { - let at = self.client.info().best_hash; + fn author_extrinsic( + &self, + call: Bytes, + account: String, + at: Option<::Hash>, + ) -> RpcResult> { + let at = at.unwrap_or_else(|| self.client.info().best_hash); let has_api = self .client @@ -134,7 +144,7 @@ where None, ) })?; - let extra = self.with_state(None, || T::signed_extras(account.clone().into())); + let extra = self.with_state(Some(at), || T::signed_extras(account.clone().into())); let ext = UncheckedExtrinsicFor::::new_signed( call, MultiAddress::Id(account.into()), @@ -178,8 +188,12 @@ where ::AccountId: From, <::Header as Header>::Number: num_traits::cast::AsPrimitive, { - fn author_extrinsic(&self, call: Bytes, account: String) -> RpcResult { - Ok(self.author_extrinsic(call, account)?.into()) + fn author_extrinsic(&self, call: Bytes, account: String, at: Option) -> RpcResult { + let at = at.map(|h| { + let bytes: [u8; 32] = h.into(); + codec::Decode::decode(&mut &bytes[..]).expect("H256 is 32 bytes, same as block hash") + }); + Ok(self.author_extrinsic(call, account, at)?.into()) } fn revert_blocks(&self, n: u32) -> RpcResult<()> { From 97d4c525ca5bc6b5aa5f307075c35cdce2956479 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Tue, 3 Feb 2026 00:59:57 +0100 Subject: [PATCH 6/7] introduce fork aware proposer factory --- simnode/src/proposer.rs | 330 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 simnode/src/proposer.rs diff --git a/simnode/src/proposer.rs b/simnode/src/proposer.rs new file mode 100644 index 0000000..17fa02f --- /dev/null +++ b/simnode/src/proposer.rs @@ -0,0 +1,330 @@ +// Copyright (C) 2023 Polytope Labs (Caymans) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Fork-aware proposer factory for simnode. +//! +//! This module provides a custom proposer factory that properly handles building blocks +//! on non-best-chain parents (fork scenarios). The standard `sc_basic_authorship::ProposerFactory` +//! uses `pool.ready_at(parent)` which doesn't return transactions as "ready" for non-best parents +//! because the transaction pool validates against the best block. +//! +//! This factory detects when we're building on a fork and includes pending transactions directly. + +use polkadot_sdk::*; + +use codec::Encode; +use futures::future::{self, Future, FutureExt}; +use log::{debug, info, trace, warn}; +use sc_block_builder::{BlockBuilderApi, BlockBuilderBuilder}; +use sc_transaction_pool_api::{InPoolTransaction, TransactionPool}; +use sp_api::{ApiExt, CallApiAt, ProvideRuntimeApi}; +use sp_blockchain::{ApplyExtrinsicFailed::Validity, Error::ApplyExtrinsicFailed, HeaderBackend}; +use sp_consensus::{DisableProofRecording, Proposal}; +use sp_core::traits::SpawnNamed; +use sp_inherents::InherentData; +use sp_runtime::{ + traits::{Block as BlockT, Header as HeaderT}, + Digest, ExtrinsicInclusionMode, +}; +use std::{marker::PhantomData, pin::Pin, sync::Arc, time}; + +const LOG_TARGET: &str = "fork-aware-proposer"; + +/// Fork-aware proposer factory that properly handles building blocks on non-best-chain parents. +/// +/// When building on the best chain, this delegates to the standard `sc_basic_authorship::ProposerFactory`. +/// When building on a fork (non-best parent), it creates a custom proposer that includes +/// pending transactions directly without relying on `pool.ready_at()`. +pub struct ForkAwareProposerFactory { + spawn_handle: Box, + client: Arc, + transaction_pool: Arc, + default_block_size_limit: usize, +} + +impl Clone for ForkAwareProposerFactory { + fn clone(&self) -> Self { + Self { + spawn_handle: self.spawn_handle.clone(), + client: self.client.clone(), + transaction_pool: self.transaction_pool.clone(), + default_block_size_limit: self.default_block_size_limit, + } + } +} + +impl ForkAwareProposerFactory { + /// Create a new fork-aware proposer factory. + pub fn new( + spawn_handle: impl SpawnNamed + 'static, + client: Arc, + transaction_pool: Arc, + ) -> Self { + Self { + spawn_handle: Box::new(spawn_handle), + client, + transaction_pool, + default_block_size_limit: 4 * 1024 * 1024 + 512, + } + } +} + +impl ForkAwareProposerFactory +where + A: TransactionPool + 'static, + Block: BlockT, + C: HeaderBackend + ProvideRuntimeApi + Send + Sync + 'static, + C::Api: ApiExt + BlockBuilderApi, +{ + fn init_with_now( + &mut self, + parent_header: &::Header, + now: Box time::Instant + Send + Sync>, + ) -> ForkAwareProposer { + let parent_hash = parent_header.hash(); + let best_hash = self.client.info().best_hash; + let is_fork = parent_hash != best_hash; + + info!( + target: LOG_TARGET, + "🙌 Starting consensus session on top of parent {:?} (#{}), is_fork: {}", + parent_hash, + parent_header.number(), + is_fork + ); + + ForkAwareProposer { + spawn_handle: self.spawn_handle.clone(), + client: self.client.clone(), + parent_hash, + parent_number: *parent_header.number(), + transaction_pool: self.transaction_pool.clone(), + now, + default_block_size_limit: self.default_block_size_limit, + is_fork, + _phantom: PhantomData, + } + } +} + +impl sp_consensus::Environment for ForkAwareProposerFactory +where + A: TransactionPool + 'static, + Block: BlockT, + C: HeaderBackend + ProvideRuntimeApi + CallApiAt + Send + Sync + 'static, + C::Api: ApiExt + BlockBuilderApi, +{ + type CreateProposer = future::Ready>; + type Proposer = ForkAwareProposer; + type Error = sp_blockchain::Error; + + fn init(&mut self, parent_header: &::Header) -> Self::CreateProposer { + future::ready(Ok(self.init_with_now(parent_header, Box::new(time::Instant::now)))) + } +} + +/// Fork-aware proposer that handles both best-chain and fork scenarios. +pub struct ForkAwareProposer { + #[allow(dead_code)] + spawn_handle: Box, + client: Arc, + parent_hash: Block::Hash, + parent_number: <::Header as HeaderT>::Number, + transaction_pool: Arc, + now: Box time::Instant + Send + Sync>, + default_block_size_limit: usize, + is_fork: bool, + _phantom: PhantomData, +} + +impl sp_consensus::Proposer for ForkAwareProposer +where + A: TransactionPool + 'static, + Block: BlockT, + C: HeaderBackend + ProvideRuntimeApi + CallApiAt + Send + Sync + 'static, + C::Api: ApiExt + BlockBuilderApi, +{ + type Proposal = Pin, Self::Error>> + Send>>; + type Error = sp_blockchain::Error; + type ProofRecording = DisableProofRecording; + type Proof = (); + + fn propose( + self, + inherent_data: InherentData, + inherent_digests: Digest, + max_duration: time::Duration, + block_size_limit: Option, + ) -> Self::Proposal { + self.propose_with(inherent_data, inherent_digests, max_duration, block_size_limit) + .boxed() + } +} + +impl ForkAwareProposer +where + A: TransactionPool + 'static, + Block: BlockT, + C: HeaderBackend + ProvideRuntimeApi + CallApiAt + Send + Sync + 'static, + C::Api: ApiExt + BlockBuilderApi, +{ + async fn propose_with( + self, + inherent_data: InherentData, + inherent_digests: Digest, + max_duration: time::Duration, + block_size_limit: Option, + ) -> Result, sp_blockchain::Error> { + let deadline = (self.now)() + max_duration - max_duration / 10; + let block_timer = time::Instant::now(); + + let mut block_builder = BlockBuilderBuilder::new(&*self.client) + .on_parent_block(self.parent_hash) + .with_parent_block_number(self.parent_number) + .with_proof_recorder(None) + .with_inherent_digests(inherent_digests) + .build()?; + + // Apply inherents + let inherents = block_builder.create_inherents(inherent_data)?; + for inherent in inherents { + match block_builder.push(inherent) { + Err(ApplyExtrinsicFailed(Validity(e))) if e.exhausted_resources() => { + warn!( + target: LOG_TARGET, + "⚠️ Dropping non-mandatory inherent from overweight block." + ) + }, + Err(ApplyExtrinsicFailed(Validity(e))) if e.was_mandatory() => { + return Err(ApplyExtrinsicFailed(Validity(e))) + }, + Err(e) => { + warn!( + target: LOG_TARGET, + "❗️ Inherent extrinsic returned unexpected error: {}. Dropping.", e + ); + }, + Ok(_) => {}, + } + } + + let mode = block_builder.extrinsic_inclusion_mode(); + if mode == ExtrinsicInclusionMode::AllExtrinsics { + self.apply_extrinsics(&mut block_builder, deadline, block_size_limit).await?; + } + + let (block, storage_changes, _proof) = block_builder.build()?.into_inner(); + + info!( + target: LOG_TARGET, + "🎁 Prepared block for proposing at {} ({} ms) hash: {:?}; parent_hash: {}; extrinsics_count: {}; is_fork: {}", + block.header().number(), + block_timer.elapsed().as_millis(), + ::Hash::from(block.header().hash()), + block.header().parent_hash(), + block.extrinsics().len(), + self.is_fork + ); + + Ok(Proposal { block, proof: (), storage_changes }) + } + + async fn apply_extrinsics( + &self, + block_builder: &mut sc_block_builder::BlockBuilder<'_, Block, C>, + deadline: time::Instant, + block_size_limit: Option, + ) -> Result<(), sp_blockchain::Error> { + let block_size_limit = block_size_limit.unwrap_or(self.default_block_size_limit); + + // Always use pool.ready() to get all pending transactions. + // For fork scenarios (is_fork = true), the standard ready_at_with_timeout may not + // return transactions properly because they were validated against a different parent. + // For best-chain scenarios (is_fork = false), pool.ready() works the same. + info!( + target: LOG_TARGET, + "📦 Applying extrinsics: is_fork={}, parent_hash={:?}", + self.is_fork, + self.parent_hash + ); + + // Get all ready transactions from the pool + let pending: Vec<_> = self.transaction_pool.ready().collect(); + + info!( + target: LOG_TARGET, + "📦 Found {} pending transactions to try", + pending.len() + ); + + for pending_tx in pending { + let now = (self.now)(); + if now > deadline { + debug!( + target: LOG_TARGET, + "Deadline reached, stopping transaction inclusion" + ); + break; + } + + let pending_tx_data = (**pending_tx.data()).clone(); + let pending_tx_hash = pending_tx.hash().clone(); + + let block_size = block_builder.estimate_block_size(false); + if block_size + pending_tx_data.encoded_size() > block_size_limit { + debug!( + target: LOG_TARGET, + "[{:?}] Transaction would overflow block size limit, skipping", + pending_tx_hash + ); + continue; + } + + trace!( + target: LOG_TARGET, + "[{:?}] Trying to push transaction to block", + pending_tx_hash + ); + + match sc_block_builder::BlockBuilder::push(block_builder, pending_tx_data) { + Ok(()) => { + info!( + target: LOG_TARGET, + "✅ [{:?}] Successfully pushed transaction to block", + pending_tx_hash + ); + }, + Err(ApplyExtrinsicFailed(Validity(e))) if e.exhausted_resources() => { + info!( + target: LOG_TARGET, + "⚠️ [{:?}] Transaction exhausted resources, skipping", + pending_tx_hash + ); + }, + Err(e) => { + info!( + target: LOG_TARGET, + "❌ [{:?}] Transaction invalid: {}", + pending_tx_hash, + e + ); + }, + } + } + + Ok(()) + } +} From 6d47abc6cc3daeaa7911d86e11ce05deb194de92 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Tue, 3 Feb 2026 15:01:27 +0100 Subject: [PATCH 7/7] revert changes to simnode --- simnode/src/client/aura.rs | 12 +- simnode/src/client/babe.rs | 12 +- simnode/src/client/parachain.rs | 23 +-- simnode/src/lib.rs | 2 - simnode/src/proposer.rs | 330 -------------------------------- simnode/src/rpc.rs | 28 +-- 6 files changed, 23 insertions(+), 384 deletions(-) delete mode 100644 simnode/src/proposer.rs diff --git a/simnode/src/client/aura.rs b/simnode/src/client/aura.rs index b6d4dcb..ae4e7d0 100644 --- a/simnode/src/client/aura.rs +++ b/simnode/src/client/aura.rs @@ -19,10 +19,7 @@ use polkadot_sdk::*; use super::*; -use crate::{ - proposer::ForkAwareProposerFactory, timestamp::SlotTimestampProvider, ChainInfo, - SimnodeApiServer, SimnodeRpcHandler, -}; +use crate::{timestamp::SlotTimestampProvider, ChainInfo, SimnodeApiServer, SimnodeRpcHandler}; use futures::{channel::mpsc, future::Either, FutureExt, StreamExt}; use num_traits::AsPrimitive; use sc_client_api::Backend; @@ -143,13 +140,12 @@ where } // Proposer object for block authorship. - // Use ForkAwareProposerFactory to properly handle building blocks on non-best-chain parents - // (fork scenarios). The standard ProposerFactory uses pool.ready_at(parent) which doesn't - // return transactions as "ready" for non-best parents. - let env = ForkAwareProposerFactory::new( + let env = sc_basic_authorship::ProposerFactory::new( task_manager.spawn_handle(), client.clone(), pool.clone(), + config.prometheus_registry(), + None, ); // Channel for the rpc handler to communicate with the authorship task. diff --git a/simnode/src/client/babe.rs b/simnode/src/client/babe.rs index b236fdc..081131e 100644 --- a/simnode/src/client/babe.rs +++ b/simnode/src/client/babe.rs @@ -47,10 +47,7 @@ use std::sync::Arc; use simnode_runtime_api::CreateTransactionApi; -use crate::{ - proposer::ForkAwareProposerFactory, timestamp::SlotTimestampProvider, ChainInfo, - SimnodeApiServer, SimnodeRpcHandler, -}; +use crate::{timestamp::SlotTimestampProvider, ChainInfo, SimnodeApiServer, SimnodeRpcHandler}; use super::*; @@ -149,13 +146,12 @@ where } // Proposer object for block authorship. - // Use ForkAwareProposerFactory to properly handle building blocks on non-best-chain parents - // (fork scenarios). The standard ProposerFactory uses pool.ready_at(parent) which doesn't - // return transactions as "ready" for non-best parents. - let env = ForkAwareProposerFactory::new( + let env = sc_basic_authorship::ProposerFactory::new( task_manager.spawn_handle(), client.clone(), pool.clone(), + config.prometheus_registry(), + None, ); // Channel for the rpc handler to communicate with the authorship task. diff --git a/simnode/src/client/parachain.rs b/simnode/src/client/parachain.rs index 088a520..23bc50a 100644 --- a/simnode/src/client/parachain.rs +++ b/simnode/src/client/parachain.rs @@ -18,12 +18,10 @@ use polkadot_sdk::*; -use codec::Decode; - use super::*; use crate::{ - proposer::ForkAwareProposerFactory, timestamp::SlotTimestampProvider, ChainInfo, - ParachainSproofInherentProvider, SimnodeApiServer, SimnodeRpcHandler, + timestamp::SlotTimestampProvider, ChainInfo, ParachainSproofInherentProvider, SimnodeApiServer, + SimnodeRpcHandler, }; use async_trait::async_trait; use futures::{channel::mpsc, future::Either, lock::Mutex, FutureExt, StreamExt}; @@ -49,7 +47,7 @@ use sp_block_builder::BlockBuilder; use sp_blockchain::HeaderBackend; use sp_consensus::SelectChain; use sp_consensus_aura::AuraApi; -use sp_core::{crypto::AccountId32, traits::SpawnEssentialNamed, Bytes, H256}; +use sp_core::{crypto::AccountId32, traits::SpawnEssentialNamed, Bytes}; use sp_runtime::traits::{Block as BlockT, Header}; use sp_transaction_pool::runtime_api::TaggedTransactionQueue; use std::sync::Arc; @@ -80,12 +78,8 @@ where <::Header as Header>::Number: num_traits::cast::AsPrimitive, T::Runtime: staging_parachain_info::Config, { - fn author_extrinsic(&self, call: Bytes, account: String, at: Option) -> RpcResult { - let at = at.map(|h| { - let bytes: [u8; 32] = h.into(); - Decode::decode(&mut &bytes[..]).expect("H256 is 32 bytes, same as block hash") - }); - Ok(self.inner.author_extrinsic(call, account, at)?.into()) + fn author_extrinsic(&self, call: Bytes, account: String) -> RpcResult { + Ok(self.inner.author_extrinsic(call, account)?.into()) } fn revert_blocks(&self, n: u32) -> RpcResult<()> { @@ -279,13 +273,12 @@ where } // Proposer object for block authorship. - // Use ForkAwareProposerFactory to properly handle building blocks on non-best-chain parents - // (fork scenarios). The standard ProposerFactory uses pool.ready_at(parent) which doesn't - // return transactions as "ready" for non-best parents. - let env = ForkAwareProposerFactory::new( + let env = sc_basic_authorship::ProposerFactory::new( task_manager.spawn_handle(), client.clone(), pool.clone(), + config.prometheus_registry(), + None, ); // Channel for the rpc handler to communicate with the authorship task. diff --git a/simnode/src/lib.rs b/simnode/src/lib.rs index 4b2c394..ecbc783 100644 --- a/simnode/src/lib.rs +++ b/simnode/src/lib.rs @@ -29,14 +29,12 @@ use std::sync::Arc; pub mod cli; pub mod client; pub mod overrides; -pub mod proposer; pub mod rpc; pub mod sproof; pub use cli::*; pub use client::*; pub use overrides::*; -pub use proposer::*; pub use rpc::*; pub use sproof::*; diff --git a/simnode/src/proposer.rs b/simnode/src/proposer.rs deleted file mode 100644 index 17fa02f..0000000 --- a/simnode/src/proposer.rs +++ /dev/null @@ -1,330 +0,0 @@ -// Copyright (C) 2023 Polytope Labs (Caymans) Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! Fork-aware proposer factory for simnode. -//! -//! This module provides a custom proposer factory that properly handles building blocks -//! on non-best-chain parents (fork scenarios). The standard `sc_basic_authorship::ProposerFactory` -//! uses `pool.ready_at(parent)` which doesn't return transactions as "ready" for non-best parents -//! because the transaction pool validates against the best block. -//! -//! This factory detects when we're building on a fork and includes pending transactions directly. - -use polkadot_sdk::*; - -use codec::Encode; -use futures::future::{self, Future, FutureExt}; -use log::{debug, info, trace, warn}; -use sc_block_builder::{BlockBuilderApi, BlockBuilderBuilder}; -use sc_transaction_pool_api::{InPoolTransaction, TransactionPool}; -use sp_api::{ApiExt, CallApiAt, ProvideRuntimeApi}; -use sp_blockchain::{ApplyExtrinsicFailed::Validity, Error::ApplyExtrinsicFailed, HeaderBackend}; -use sp_consensus::{DisableProofRecording, Proposal}; -use sp_core::traits::SpawnNamed; -use sp_inherents::InherentData; -use sp_runtime::{ - traits::{Block as BlockT, Header as HeaderT}, - Digest, ExtrinsicInclusionMode, -}; -use std::{marker::PhantomData, pin::Pin, sync::Arc, time}; - -const LOG_TARGET: &str = "fork-aware-proposer"; - -/// Fork-aware proposer factory that properly handles building blocks on non-best-chain parents. -/// -/// When building on the best chain, this delegates to the standard `sc_basic_authorship::ProposerFactory`. -/// When building on a fork (non-best parent), it creates a custom proposer that includes -/// pending transactions directly without relying on `pool.ready_at()`. -pub struct ForkAwareProposerFactory { - spawn_handle: Box, - client: Arc, - transaction_pool: Arc, - default_block_size_limit: usize, -} - -impl Clone for ForkAwareProposerFactory { - fn clone(&self) -> Self { - Self { - spawn_handle: self.spawn_handle.clone(), - client: self.client.clone(), - transaction_pool: self.transaction_pool.clone(), - default_block_size_limit: self.default_block_size_limit, - } - } -} - -impl ForkAwareProposerFactory { - /// Create a new fork-aware proposer factory. - pub fn new( - spawn_handle: impl SpawnNamed + 'static, - client: Arc, - transaction_pool: Arc, - ) -> Self { - Self { - spawn_handle: Box::new(spawn_handle), - client, - transaction_pool, - default_block_size_limit: 4 * 1024 * 1024 + 512, - } - } -} - -impl ForkAwareProposerFactory -where - A: TransactionPool + 'static, - Block: BlockT, - C: HeaderBackend + ProvideRuntimeApi + Send + Sync + 'static, - C::Api: ApiExt + BlockBuilderApi, -{ - fn init_with_now( - &mut self, - parent_header: &::Header, - now: Box time::Instant + Send + Sync>, - ) -> ForkAwareProposer { - let parent_hash = parent_header.hash(); - let best_hash = self.client.info().best_hash; - let is_fork = parent_hash != best_hash; - - info!( - target: LOG_TARGET, - "🙌 Starting consensus session on top of parent {:?} (#{}), is_fork: {}", - parent_hash, - parent_header.number(), - is_fork - ); - - ForkAwareProposer { - spawn_handle: self.spawn_handle.clone(), - client: self.client.clone(), - parent_hash, - parent_number: *parent_header.number(), - transaction_pool: self.transaction_pool.clone(), - now, - default_block_size_limit: self.default_block_size_limit, - is_fork, - _phantom: PhantomData, - } - } -} - -impl sp_consensus::Environment for ForkAwareProposerFactory -where - A: TransactionPool + 'static, - Block: BlockT, - C: HeaderBackend + ProvideRuntimeApi + CallApiAt + Send + Sync + 'static, - C::Api: ApiExt + BlockBuilderApi, -{ - type CreateProposer = future::Ready>; - type Proposer = ForkAwareProposer; - type Error = sp_blockchain::Error; - - fn init(&mut self, parent_header: &::Header) -> Self::CreateProposer { - future::ready(Ok(self.init_with_now(parent_header, Box::new(time::Instant::now)))) - } -} - -/// Fork-aware proposer that handles both best-chain and fork scenarios. -pub struct ForkAwareProposer { - #[allow(dead_code)] - spawn_handle: Box, - client: Arc, - parent_hash: Block::Hash, - parent_number: <::Header as HeaderT>::Number, - transaction_pool: Arc, - now: Box time::Instant + Send + Sync>, - default_block_size_limit: usize, - is_fork: bool, - _phantom: PhantomData, -} - -impl sp_consensus::Proposer for ForkAwareProposer -where - A: TransactionPool + 'static, - Block: BlockT, - C: HeaderBackend + ProvideRuntimeApi + CallApiAt + Send + Sync + 'static, - C::Api: ApiExt + BlockBuilderApi, -{ - type Proposal = Pin, Self::Error>> + Send>>; - type Error = sp_blockchain::Error; - type ProofRecording = DisableProofRecording; - type Proof = (); - - fn propose( - self, - inherent_data: InherentData, - inherent_digests: Digest, - max_duration: time::Duration, - block_size_limit: Option, - ) -> Self::Proposal { - self.propose_with(inherent_data, inherent_digests, max_duration, block_size_limit) - .boxed() - } -} - -impl ForkAwareProposer -where - A: TransactionPool + 'static, - Block: BlockT, - C: HeaderBackend + ProvideRuntimeApi + CallApiAt + Send + Sync + 'static, - C::Api: ApiExt + BlockBuilderApi, -{ - async fn propose_with( - self, - inherent_data: InherentData, - inherent_digests: Digest, - max_duration: time::Duration, - block_size_limit: Option, - ) -> Result, sp_blockchain::Error> { - let deadline = (self.now)() + max_duration - max_duration / 10; - let block_timer = time::Instant::now(); - - let mut block_builder = BlockBuilderBuilder::new(&*self.client) - .on_parent_block(self.parent_hash) - .with_parent_block_number(self.parent_number) - .with_proof_recorder(None) - .with_inherent_digests(inherent_digests) - .build()?; - - // Apply inherents - let inherents = block_builder.create_inherents(inherent_data)?; - for inherent in inherents { - match block_builder.push(inherent) { - Err(ApplyExtrinsicFailed(Validity(e))) if e.exhausted_resources() => { - warn!( - target: LOG_TARGET, - "⚠️ Dropping non-mandatory inherent from overweight block." - ) - }, - Err(ApplyExtrinsicFailed(Validity(e))) if e.was_mandatory() => { - return Err(ApplyExtrinsicFailed(Validity(e))) - }, - Err(e) => { - warn!( - target: LOG_TARGET, - "❗️ Inherent extrinsic returned unexpected error: {}. Dropping.", e - ); - }, - Ok(_) => {}, - } - } - - let mode = block_builder.extrinsic_inclusion_mode(); - if mode == ExtrinsicInclusionMode::AllExtrinsics { - self.apply_extrinsics(&mut block_builder, deadline, block_size_limit).await?; - } - - let (block, storage_changes, _proof) = block_builder.build()?.into_inner(); - - info!( - target: LOG_TARGET, - "🎁 Prepared block for proposing at {} ({} ms) hash: {:?}; parent_hash: {}; extrinsics_count: {}; is_fork: {}", - block.header().number(), - block_timer.elapsed().as_millis(), - ::Hash::from(block.header().hash()), - block.header().parent_hash(), - block.extrinsics().len(), - self.is_fork - ); - - Ok(Proposal { block, proof: (), storage_changes }) - } - - async fn apply_extrinsics( - &self, - block_builder: &mut sc_block_builder::BlockBuilder<'_, Block, C>, - deadline: time::Instant, - block_size_limit: Option, - ) -> Result<(), sp_blockchain::Error> { - let block_size_limit = block_size_limit.unwrap_or(self.default_block_size_limit); - - // Always use pool.ready() to get all pending transactions. - // For fork scenarios (is_fork = true), the standard ready_at_with_timeout may not - // return transactions properly because they were validated against a different parent. - // For best-chain scenarios (is_fork = false), pool.ready() works the same. - info!( - target: LOG_TARGET, - "📦 Applying extrinsics: is_fork={}, parent_hash={:?}", - self.is_fork, - self.parent_hash - ); - - // Get all ready transactions from the pool - let pending: Vec<_> = self.transaction_pool.ready().collect(); - - info!( - target: LOG_TARGET, - "📦 Found {} pending transactions to try", - pending.len() - ); - - for pending_tx in pending { - let now = (self.now)(); - if now > deadline { - debug!( - target: LOG_TARGET, - "Deadline reached, stopping transaction inclusion" - ); - break; - } - - let pending_tx_data = (**pending_tx.data()).clone(); - let pending_tx_hash = pending_tx.hash().clone(); - - let block_size = block_builder.estimate_block_size(false); - if block_size + pending_tx_data.encoded_size() > block_size_limit { - debug!( - target: LOG_TARGET, - "[{:?}] Transaction would overflow block size limit, skipping", - pending_tx_hash - ); - continue; - } - - trace!( - target: LOG_TARGET, - "[{:?}] Trying to push transaction to block", - pending_tx_hash - ); - - match sc_block_builder::BlockBuilder::push(block_builder, pending_tx_data) { - Ok(()) => { - info!( - target: LOG_TARGET, - "✅ [{:?}] Successfully pushed transaction to block", - pending_tx_hash - ); - }, - Err(ApplyExtrinsicFailed(Validity(e))) if e.exhausted_resources() => { - info!( - target: LOG_TARGET, - "⚠️ [{:?}] Transaction exhausted resources, skipping", - pending_tx_hash - ); - }, - Err(e) => { - info!( - target: LOG_TARGET, - "❌ [{:?}] Transaction invalid: {}", - pending_tx_hash, - e - ); - }, - } - } - - Ok(()) - } -} diff --git a/simnode/src/rpc.rs b/simnode/src/rpc.rs index 60e4a65..ad99927 100644 --- a/simnode/src/rpc.rs +++ b/simnode/src/rpc.rs @@ -32,7 +32,7 @@ use sp_api::{ApiExt, ConstructRuntimeApi, ProvideRuntimeApi}; use sp_blockchain::HeaderBackend; use sp_core::{ crypto::{AccountId32, Ss58Codec}, - Bytes, H256, + Bytes, }; use sp_runtime::{ traits::{Block as BlockT, Header}, @@ -46,13 +46,8 @@ use std::sync::Arc; pub trait SimnodeApi { /// Constructs an extrinsic with an empty signature and the given AccountId as the Signer using /// simnode's runtime api. - /// - /// The optional `at` parameter specifies which block's state to use when creating the - /// transaction (for nonce lookup, etc.). If not provided, defaults to the best block. - /// This is useful when building transactions for fork branches that are not the current - /// best chain. #[method(name = "simnode_authorExtrinsic")] - fn author_extrinsic(&self, call: Bytes, account: String, at: Option) -> RpcResult; + fn author_extrinsic(&self, call: Bytes, account: String) -> RpcResult; /// reverts `n` number of blocks and their state from the chain. #[method(name = "simnode_revertBlocks")] @@ -88,13 +83,8 @@ where Self { client, backend } } - fn author_extrinsic( - &self, - call: Bytes, - account: String, - at: Option<::Hash>, - ) -> RpcResult> { - let at = at.unwrap_or_else(|| self.client.info().best_hash); + fn author_extrinsic(&self, call: Bytes, account: String) -> RpcResult> { + let at = self.client.info().best_hash; let has_api = self .client @@ -144,7 +134,7 @@ where None, ) })?; - let extra = self.with_state(Some(at), || T::signed_extras(account.clone().into())); + let extra = self.with_state(None, || T::signed_extras(account.clone().into())); let ext = UncheckedExtrinsicFor::::new_signed( call, MultiAddress::Id(account.into()), @@ -188,12 +178,8 @@ where ::AccountId: From, <::Header as Header>::Number: num_traits::cast::AsPrimitive, { - fn author_extrinsic(&self, call: Bytes, account: String, at: Option) -> RpcResult { - let at = at.map(|h| { - let bytes: [u8; 32] = h.into(); - codec::Decode::decode(&mut &bytes[..]).expect("H256 is 32 bytes, same as block hash") - }); - Ok(self.author_extrinsic(call, account, at)?.into()) + fn author_extrinsic(&self, call: Bytes, account: String) -> RpcResult { + Ok(self.author_extrinsic(call, account)?.into()) } fn revert_blocks(&self, n: u32) -> RpcResult<()> {