|
| 1 | +--- |
| 2 | +title: Asynchronous Backing |
| 3 | +description: Learn how to increase the efficiency and throughput of your parachain by upgrading it to leverage asynchronous backing. |
| 4 | +--- |
| 5 | + |
| 6 | +# Upgrade Parachain for Asynchronous Backing Compatibility |
| 7 | + |
| 8 | +## Introduction |
| 9 | + |
| 10 | +This guide is relevant for Cumulus based parachain projects started in 2023 or before, whose |
| 11 | +backing process is synchronous where parablocks can only be built on the latest relay chain |
| 12 | +block. Async backing allows collators to build parablocks on older relay chain blocks and create |
| 13 | +pipelines of multiple pending parablocks. This parallel block generation increases efficiency |
| 14 | +and throughput. |
| 15 | + |
| 16 | +!!!note |
| 17 | + If starting a new parachain project, please use an async backing compatible template such as |
| 18 | + the [parachain template](https://github.com/paritytech/polkadot-sdk-parachain-template). |
| 19 | + The rollout process for async backing has three phases. Phases 1 and 2 below put new |
| 20 | + infrastructure in place. Then we can simply turn on async backing in phase 3. |
| 21 | + |
| 22 | +## Prerequisite |
| 23 | + |
| 24 | +The relay chain needs to have async backing enabled so double-check that the relay chain |
| 25 | +configuration contains the following three parameters (especially when testing locally e.g. with |
| 26 | +zombienet): |
| 27 | + |
| 28 | +```json |
| 29 | +"async_backing_params": { |
| 30 | + "max_candidate_depth": 3, |
| 31 | + "allowed_ancestry_len": 2 |
| 32 | +}, |
| 33 | +"scheduling_lookahead": 2 |
| 34 | +``` |
| 35 | + |
| 36 | +!!! warning |
| 37 | + `scheduling_lookahead` must be set to 2, otherwise parachain |
| 38 | + block times will degrade to worse than with sync backing! |
| 39 | + |
| 40 | +## Phase 1 - Update Parachain Runtime |
| 41 | + |
| 42 | +This phase involves configuring your parachain's runtime `/runtime/src/lib.rs` to make use of |
| 43 | +async backing system. |
| 44 | + |
| 45 | +1. Establish and ensure constants for `capacity` and `velocity` are both set to 1 in the |
| 46 | + runtime. |
| 47 | +2. Establish and ensure the constant relay chain slot duration measured in milliseconds equal to |
| 48 | + `6000` in the runtime. |
| 49 | +```rust |
| 50 | +// Maximum number of blocks simultaneously accepted by the Runtime, not yet included into the |
| 51 | +// relay chain. |
| 52 | +pub const UNINCLUDED_SEGMENT_CAPACITY: u32 = 1; |
| 53 | +// How many parachain blocks are processed by the relay chain per parent. Limits the number of |
| 54 | +// blocks authored per slot. |
| 55 | +pub const BLOCK_PROCESSING_VELOCITY: u32 = 1; |
| 56 | +// Relay chain slot duration, in milliseconds. |
| 57 | +pub const RELAY_chain_SLOT_DURATION_MILLIS: u32 = 6000; |
| 58 | +``` |
| 59 | + |
| 60 | +3. Establish constants `MILLISECS_PER_BLOCK` and `SLOT_DURATION` if not already present in the |
| 61 | + runtime. |
| 62 | +```rust |
| 63 | +// `SLOT_DURATION` is picked up by `pallet_timestamp` which is in turn picked |
| 64 | +// up by `pallet_aura` to implement `fn slot_duration()`. |
| 65 | +// |
| 66 | +// Change this to adjust the block time. |
| 67 | +pub const MILLISECS_PER_BLOCK: u64 = 12000; |
| 68 | +pub const SLOT_DURATION: u64 = MILLISECS_PER_BLOCK; |
| 69 | +``` |
| 70 | + |
| 71 | +4. Configure `cumulus_pallet_parachain_system` in the runtime. |
| 72 | + |
| 73 | +Define a `FixedVelocityConsensusHook` using our capacity, velocity, and relay slot duration |
| 74 | +constants. Use this to set the parachain system `ConsensusHook` property. |
| 75 | + |
| 76 | +```rust |
| 77 | +type ConsensusHook = cumulus_pallet_aura_ext::FixedVelocityConsensusHook< |
| 78 | + Runtime, |
| 79 | + RELAY_CHAIN_SLOT_DURATION_MILLIS, |
| 80 | + BLOCK_PROCESSING_VELOCITY, |
| 81 | + UNINCLUDED_SEGMENT_CAPACITY, |
| 82 | +>; |
| 83 | +``` |
| 84 | + |
| 85 | +```rust |
| 86 | +impl cumulus_pallet_parachain_system::Config for Runtime { |
| 87 | + .. |
| 88 | + type ConsensusHook = ConsensusHook; |
| 89 | + .. |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +Set the parachain system property `CheckAssociatedRelayNumber` to |
| 94 | +`RelayNumberMonotonicallyIncreases` |
| 95 | +```rust |
| 96 | +impl cumulus_pallet_parachain_system::Config for Runtime { |
| 97 | + .. |
| 98 | + type CheckAssociatedRelayNumber = RelayNumberMonotonicallyIncreases; |
| 99 | + .. |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +5. Configure `pallet_aura` in the runtime. |
| 104 | + |
| 105 | +Set `AllowMultipleBlocksPerSlot` to `false` (don't worry, we will set it to `true` when we |
| 106 | +activate async backing in phase 3). |
| 107 | + |
| 108 | +Define `pallet_aura::SlotDuration` using our constant `SLOT_DURATION` |
| 109 | + |
| 110 | +```rust |
| 111 | +impl pallet_aura::Config for Runtime { |
| 112 | + .. |
| 113 | + type AllowMultipleBlocksPerSlot = ConstBool<false>; |
| 114 | + #[cfg(feature = "experimental")] |
| 115 | + type SlotDuration = ConstU64<SLOT_DURATION>; |
| 116 | + .. |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +6. Update `sp_consensus_aura::AuraApi::slot_duration` in `sp_api::impl_runtime_apis` to match |
| 121 | + the constant `SLOT_DURATION` |
| 122 | + |
| 123 | +```rust |
| 124 | +fn impl_slot_duration() -> sp_consensus_aura::SlotDuration { |
| 125 | + sp_consensus_aura::SlotDuration::from_millis(SLOT_DURATION) |
| 126 | +} |
| 127 | +``` |
| 128 | + |
| 129 | +7. Implement the `AuraUnincludedSegmentApi`, which allows the collator client to query its |
| 130 | + runtime to determine whether it should author a block. |
| 131 | + |
| 132 | + - Add the dependency `cumulus-primitives-aura` to the `runtime/Cargo.toml` file for your |
| 133 | + runtime |
| 134 | + |
| 135 | +```rust |
| 136 | +.. |
| 137 | +cumulus-primitives-aura = { path = "../../../../primitives/aura", default-features = false } |
| 138 | +.. |
| 139 | +``` |
| 140 | + |
| 141 | +In the same file, add `"cumulus-primitives-aura/std",` to the `std` feature. |
| 142 | + |
| 143 | +Inside the `impl_runtime_apis!` block for your runtime, implement the |
| 144 | + `cumulus_primitives_aura::AuraUnincludedSegmentApi` as shown below. |
| 145 | + |
| 146 | +```rust |
| 147 | +fn impl_can_build_upon( |
| 148 | + included_hash: <Block as BlockT>::Hash, |
| 149 | + slot: cumulus_primitives_aura::Slot, |
| 150 | +) -> bool { |
| 151 | + ConsensusHook::can_build_upon(included_hash, slot) |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +!!!note |
| 156 | + With a capacity of 1 we have an effective velocity of ½ even when velocity is |
| 157 | + configured to some larger value. This is because capacity will be filled after a single block is |
| 158 | + produced and will only be freed up after that block is included on the relay chain, which takes |
| 159 | + 2 relay blocks to accomplish. Thus with capacity 1 and velocity 1 we get the customary 12 second |
| 160 | + parachain block time. |
| 161 | + |
| 162 | +8. If your `runtime/src/lib.rs` provides a `CheckInherents` type to `register_validate_block`, |
| 163 | + remove it. `FixedVelocityConsensusHook` makes it unnecessary. The following example shows how |
| 164 | + `register_validate_block` should look after removing `CheckInherents`. |
| 165 | + |
| 166 | +```rust |
| 167 | +cumulus_pallet_parachain_system::register_validate_block! { |
| 168 | + Runtime = Runtime, |
| 169 | + BlockExecutor = cumulus_pallet_aura_ext::BlockExecutor::<Runtime, Executive>, |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +## Phase 2 - Update Parachain Nodes |
| 174 | + |
| 175 | +This phase consists of plugging in the new lookahead collator node. |
| 176 | + |
| 177 | +1. Import `cumulus_primitives_core::ValidationCode` to `node/src/service.rs`. |
| 178 | + |
| 179 | +```rust |
| 180 | +use cumulus_primitives_core::{ |
| 181 | + relay_chain::{CollatorPair, ValidationCode}, |
| 182 | + GetParachainInfo, ParaId, |
| 183 | +}; |
| 184 | +``` |
| 185 | + |
| 186 | +2. In `node/src/service.rs`, modify `sc_service::spawn_tasks` to use a clone of `Backend` rather |
| 187 | + than the original |
| 188 | + |
| 189 | +```rust |
| 190 | +sc_service::spawn_tasks(sc_service::SpawnTasksParams { |
| 191 | + .. |
| 192 | + backend: backend.clone(), |
| 193 | + .. |
| 194 | +})?; |
| 195 | +``` |
| 196 | + |
| 197 | +3. Add `backend` as a parameter to `start_consensus()` in `node/src/service.rs` |
| 198 | + |
| 199 | +```rust |
| 200 | +fn start_consensus( |
| 201 | + .. |
| 202 | + backend: Arc<ParachainBackend>, |
| 203 | + .. |
| 204 | +``` |
| 205 | +```rust |
| 206 | +if validator { |
| 207 | + start_consensus( |
| 208 | + .. |
| 209 | + backend.clone(), |
| 210 | + .. |
| 211 | + )?; |
| 212 | +} |
| 213 | +``` |
| 214 | + |
| 215 | +4. In `node/src/service.rs` import the lookahead collator rather than the basic collator |
| 216 | + |
| 217 | +```rust |
| 218 | +use cumulus_client_consensus_aura::collators::lookahead::{self as aura, Params as AuraParams}; |
| 219 | +``` |
| 220 | + |
| 221 | +5. In `start_consensus()` replace the `BasicAuraParams` struct with `AuraParams` |
| 222 | + - Change the struct type from `BasicAuraParams` to `AuraParams` |
| 223 | + - In the `para_client` field, pass in a cloned para client rather than the original |
| 224 | + - Add a `para_backend` parameter after `para_client`, passing in our para backend |
| 225 | + - Provide a `code_hash_provider` closure like that shown below |
| 226 | + - Increase `authoring_duration` from 500 milliseconds to 2000 |
| 227 | +```rust |
| 228 | +let params = AuraParams { |
| 229 | + .. |
| 230 | + para_client: client.clone(), |
| 231 | + para_backend: backend.clone(), |
| 232 | + .. |
| 233 | + code_hash_provider: move |block_hash| { |
| 234 | + client.code_at(block_hash).ok().map(|c| ValidationCode::from(c).hash()) |
| 235 | + }, |
| 236 | + .. |
| 237 | + authoring_duration: Duration::from_millis(2000), |
| 238 | + .. |
| 239 | +}; |
| 240 | +``` |
| 241 | + |
| 242 | +!!!note |
| 243 | + Set `authoring_duration` to whatever you want, taking your own hardware into account. |
| 244 | + But if the backer who should be slower than you due to reading from disk, times out at two |
| 245 | + seconds your candidates will be rejected. |
| 246 | + |
| 247 | +6. In `start_consensus()` replace `basic_aura::run` with `aura::run` |
| 248 | +```rust |
| 249 | +let fut = |
| 250 | +aura::run::<Block, sp_consensus_aura::sr25519::AuthorityPair, _, _, _, _, _, _, _, _, _>( |
| 251 | + params, |
| 252 | +); |
| 253 | +task_manager.spawn_essential_handle().spawn("aura", None, fut); |
| 254 | +``` |
| 255 | + |
| 256 | +## Phase 3 - Activate Async Backing |
| 257 | + |
| 258 | +This phase consists of changes to your parachain's runtime that activate async backing feature. |
| 259 | + |
| 260 | +1. Configure `pallet_aura`, setting `AllowMultipleBlocksPerSlot` to true in |
| 261 | + `runtime/src/lib.rs`. |
| 262 | + |
| 263 | +```rust |
| 264 | +impl pallet_aura::Config for Runtime { |
| 265 | + type AuthorityId = AuraId; |
| 266 | + type DisabledValidators = (); |
| 267 | + type MaxAuthorities = ConstU32<100_000>; |
| 268 | + type AllowMultipleBlocksPerSlot = ConstBool<true>; |
| 269 | + type SlotDuration = ConstU64<SLOT_DURATION>; |
| 270 | +} |
| 271 | +``` |
| 272 | + |
| 273 | +2. Increase the maximum `UNINCLUDED_SEGMENT_CAPACITY` in `runtime/src/lib.rs`. |
| 274 | + |
| 275 | +```rust |
| 276 | +mod async_backing_params { |
| 277 | + /// Maximum number of blocks simultaneously accepted by the Runtime, not yet included |
| 278 | + /// into the relay chain. |
| 279 | + pub(crate) const UNINCLUDED_SEGMENT_CAPACITY: u32 = 3; |
| 280 | + /// How many parachain blocks are processed by the relay chain per parent. Limits the |
| 281 | + /// number of blocks authored per slot. |
| 282 | + pub(crate) const BLOCK_PROCESSING_VELOCITY: u32 = 1; |
| 283 | + /// Relay chain slot duration, in milliseconds. |
| 284 | + pub(crate) const RELAY_CHAIN_SLOT_DURATION_MILLIS: u32 = 6000; |
| 285 | +} |
| 286 | +``` |
| 287 | + |
| 288 | +3. Decrease `MILLISECS_PER_BLOCK` to 6000. |
| 289 | + |
| 290 | +!!!note |
| 291 | + For a parachain which measures time in terms of its own block number rather than by relay block |
| 292 | + number it may be preferable to increase velocity. Changing block time may cause complications, |
| 293 | + requiring additional changes. See the section "Timing by Block Number". |
| 294 | + |
| 295 | +```rust |
| 296 | +mod block_times { |
| 297 | + /// This determines the average expected block time that we are targeting. Blocks will be |
| 298 | + /// produced at a minimum duration defined by `SLOT_DURATION`. `SLOT_DURATION` is picked up by |
| 299 | + /// `pallet_timestamp` which is in turn picked up by `pallet_aura` to implement `fn |
| 300 | + /// slot_duration()`. |
| 301 | + /// |
| 302 | + /// Change this to adjust the block time. |
| 303 | + pub const MILLI_SECS_PER_BLOCK: u64 = 6000; |
| 304 | + |
| 305 | + // NOTE: Currently it is not possible to change the slot duration after the chain has started. |
| 306 | + // Attempting to do so will brick block production. |
| 307 | + pub const SLOT_DURATION: u64 = MILLI_SECS_PER_BLOCK; |
| 308 | +} |
| 309 | +``` |
| 310 | + |
| 311 | +4. Update `MAXIMUM_BLOCK_WEIGHT` to reflect the increased time available for block production. |
| 312 | + |
| 313 | +```rust |
| 314 | +const MAXIMUM_BLOCK_WEIGHT: Weight = Weight::from_parts( |
| 315 | + WEIGHT_REF_TIME_PER_SECOND.saturating_mul(2), |
| 316 | + cumulus_primitives_core::relay_chain::MAX_POV_SIZE as u64, |
| 317 | +); |
| 318 | +``` |
| 319 | + |
| 320 | +5. Add a feature flagged alternative for `MinimumPeriod` in `pallet_timestamp`. The type should |
| 321 | + be `ConstU64<0>` with the feature flag experimental, and `ConstU64<{SLOT_DURATION / 2}>` |
| 322 | + without. |
| 323 | + |
| 324 | +```rust |
| 325 | +impl pallet_timestamp::Config for Runtime { |
| 326 | + .. |
| 327 | + #[cfg(feature = "experimental")] |
| 328 | + type MinimumPeriod = ConstU64<0>; |
| 329 | + #[cfg(not(feature = "experimental"))] |
| 330 | + type MinimumPeriod = ConstU64<{ SLOT_DURATION / 2 }>; |
| 331 | + .. |
| 332 | +} |
| 333 | +``` |
| 334 | + |
| 335 | +## Timing by Block Number |
| 336 | + |
| 337 | +With asynchronous backing it will be possible for parachains to opt for a block time of 6 |
| 338 | +seconds rather than 12 seconds. But modifying block duration isn't so simple for a parachain |
| 339 | +which was measuring time in terms of its own block number. It could result in expected and |
| 340 | +actual time not matching up, stalling the parachain. |
| 341 | + |
| 342 | +One strategy to deal with this issue is to instead rely on relay chain block numbers for timing. |
| 343 | +Relay block number is kept track of by each parachain in `pallet-parachain-system` with the |
| 344 | +storage value `LastRelaychainBlockNumber`. This value can be obtained and used wherever timing |
| 345 | +based on block number is needed. |
0 commit comments