Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collator: Fix can_build_upon by always allowing to build on included block #7205

Merged
merged 10 commits into from
Jan 20, 2025
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions cumulus/client/consensus/aura/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ polkadot-node-subsystem-util = { workspace = true, default-features = true }
polkadot-overseer = { workspace = true, default-features = true }
polkadot-primitives = { workspace = true, default-features = true }

[dev-dependencies]
cumulus-test-client = { workspace = true }
cumulus-test-relay-sproof-builder = { workspace = true }
sp-keyring = { workspace = true }

[features]
# Allows collator to use full PoV size for block building
full-pov-size = []
136 changes: 128 additions & 8 deletions cumulus/client/consensus/aura/src/collators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ async fn check_validation_code_or_log(
%para_id,
"Failed to fetch validation code hash",
);
return
return;
skunert marked this conversation as resolved.
Show resolved Hide resolved
},
};

Expand Down Expand Up @@ -147,7 +147,7 @@ async fn cores_scheduled_for_para(
?relay_parent,
"Failed to query claim queue runtime API",
);
return Vec::new()
return Vec::new();
},
};

Expand Down Expand Up @@ -179,12 +179,19 @@ where
let authorities = runtime_api.authorities(parent_hash).ok()?;
let author_pub = aura_internal::claim_slot::<P>(para_slot, &authorities, keystore).await?;

let Ok(Some(api_version)) =
runtime_api.api_version::<dyn AuraUnincludedSegmentApi<Block>>(parent_hash)
else {
return (parent_hash == included_block)
.then(|| SlotClaim::unchecked::<P>(author_pub, para_slot, timestamp));
};
// This function is typically called when we want to build block N. At that point, the
// unincluded segment in the runtime is unaware of the hash of block N-1. If the unincluded
// segment in the runtime is full, but block N-1 is the included block, the unincluded segment
// should have length 0 and we can build. Since the hash is not available to the runtime
// however, we need this extra check here.
if parent_hash == included_block {
return Some(SlotClaim::unchecked::<P>(author_pub, para_slot, timestamp));
}

let api_version = runtime_api
.api_version::<dyn AuraUnincludedSegmentApi<Block>>(parent_hash)
.ok()
.flatten()?;

let slot = if api_version > 1 { relay_slot } else { para_slot };

Expand Down Expand Up @@ -243,3 +250,116 @@ where
.max_by_key(|a| a.depth)
.map(|parent| (included_block, parent))
}

#[cfg(test)]
mod tests {
use crate::collators::can_build_upon;
use codec::Encode;
use cumulus_primitives_aura::Slot;
use cumulus_primitives_core::BlockT;
use cumulus_relay_chain_interface::PHash;
use cumulus_test_client::{
runtime::{Block, Hash},
Client, DefaultTestClientBuilderExt, InitBlockBuilder, TestClientBuilder,
TestClientBuilderExt,
};
use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder;
use polkadot_primitives::HeadData;
use sc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy};
use sp_consensus::BlockOrigin;
use sp_keystore::{Keystore, KeystorePtr};
use sp_timestamp::Timestamp;
use std::sync::Arc;

async fn import_block<I: BlockImport<Block>>(
importer: &I,
block: Block,
origin: BlockOrigin,
import_as_best: bool,
) {
let (header, body) = block.deconstruct();

let mut block_import_params = BlockImportParams::new(origin, header);
block_import_params.fork_choice = Some(ForkChoiceStrategy::Custom(import_as_best));
block_import_params.body = Some(body);
importer.import_block(block_import_params).await.unwrap();
}

fn sproof_with_parent_by_hash(client: &Client, hash: PHash) -> RelayStateSproofBuilder {
let header = client.header(hash).ok().flatten().expect("No header for parent block");
let included = HeadData(header.encode());
let mut x = RelayStateSproofBuilder::default();
x.para_id = cumulus_test_client::runtime::PARACHAIN_ID.into();
x.included_para_head = Some(included);

x
skunert marked this conversation as resolved.
Show resolved Hide resolved
}
async fn build_and_import_block(client: &Client, included: Hash) -> Block {
let sproof = sproof_with_parent_by_hash(client, included);

let block_builder = client.init_block_builder(None, sproof).block_builder;

let block = block_builder.build().unwrap().block;

let origin = BlockOrigin::NetworkInitialSync;
import_block(client, block.clone(), origin, true).await;
block
}

fn set_up_components() -> (Arc<Client>, KeystorePtr) {
let keystore = Arc::new(sp_keystore::testing::MemoryKeystore::new()) as Arc<_>;
for key in sp_keyring::Sr25519Keyring::iter() {
Keystore::sr25519_generate_new(
&*keystore,
sp_application_crypto::key_types::AURA,
Some(&key.to_seed()),
)
.expect("Can insert key into MemoryKeyStore");
}
(Arc::new(TestClientBuilder::new().build()), keystore)
}

/// This tests a special scenario where the unincluded segment in the runtime
/// is full. We are calling `can_build_upon`, passing the last built block as the
/// included one. In the runtime we will not find the hash of the included block in the
/// unincluded segment. The `can_build_upon` runtime API would therefore return `false`, but
/// we are ensuring on the node side that we are are always able to build on the included block.
#[tokio::test]
async fn test_can_build_upon() {
let (client, keystore) = set_up_components();

let genesis_hash = client.chain_info().genesis_hash;
let mut last_hash = genesis_hash;

// Fill up the unincluded segment tracker in the runtime.
while can_build_upon::<_, _, sp_consensus_aura::sr25519::AuthorityPair>(
Slot::from(u64::MAX),
Slot::from(u64::MAX),
Timestamp::default(),
last_hash,
genesis_hash,
&*client,
&keystore,
)
.await
.is_some()
{
let block = build_and_import_block(&client, genesis_hash).await;
last_hash = block.header().hash();
}

// Blocks were built with the genesis hash set as included block.
// We call `can_build_upon` with the last built block as the included block.
let result = can_build_upon::<_, _, sp_consensus_aura::sr25519::AuthorityPair>(
Slot::from(u64::MAX),
Slot::from(u64::MAX),
Timestamp::default(),
last_hash,
last_hash,
&*client,
&keystore,
)
.await;
assert!(result.is_some());
}
}
10 changes: 10 additions & 0 deletions prdoc/pr_7205.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
title: 'Collator: Fix `can_build_upon` by always allowing to build on included block'
doc:
- audience: Node Dev
description: |-
Fixes a bug introduced in #6825.
We should always allow building on the included block of parachains. In situations where the unincluded segment
is full, but the included block moved to the most recent block, building was wrongly disallowed.
crates:
- name: cumulus-client-consensus-aura
bump: minor
Loading