use crate::LOG_TARGET;
use codec::Codec;
use cumulus_primitives_aura::Slot;
use cumulus_primitives_core::BlockT;
use sc_client_api::UsageProvider;
use sc_consensus_aura::SlotDuration;
use sp_api::ProvideRuntimeApi;
use sp_application_crypto::AppPublic;
use sp_consensus_aura::AuraApi;
use sp_core::Pair;
use sp_runtime::traits::Member;
use sp_timestamp::Timestamp;
use std::{
cmp::{max, min},
sync::Arc,
time::Duration,
};
const BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS: Duration = Duration::from_millis(500);
#[derive(Debug)]
pub(crate) struct SlotInfo {
pub timestamp: Timestamp,
pub slot: Slot,
}
#[derive(Debug)]
pub(crate) struct SlotTimer<Block, Client, P> {
client: Arc<Client>,
time_offset: Duration,
last_reported_core_num: Option<u32>,
relay_slot_duration: Duration,
_marker: std::marker::PhantomData<(Block, Box<dyn Fn(P) + Send + Sync + 'static>)>,
}
fn compute_next_wake_up_time(
para_slot_duration: SlotDuration,
relay_slot_duration: Duration,
core_count: Option<u32>,
time_now: Duration,
time_offset: Duration,
) -> (Duration, Timestamp, Slot) {
let para_slots_per_relay_block =
(relay_slot_duration.as_millis() / para_slot_duration.as_millis() as u128) as u32;
let assigned_core_num = core_count.unwrap_or(1);
let mut block_production_interval = min(para_slot_duration.as_duration(), relay_slot_duration);
if assigned_core_num > para_slots_per_relay_block &&
para_slot_duration.as_duration() >= relay_slot_duration
{
block_production_interval =
max(relay_slot_duration / assigned_core_num, BLOCK_PRODUCTION_MINIMUM_INTERVAL_MS);
tracing::debug!(
target: LOG_TARGET,
?block_production_interval,
"Expected to produce for {assigned_core_num} cores but only have {para_slots_per_relay_block} slots. Attempting to produce multiple blocks per slot."
);
}
let (duration, timestamp) =
time_until_next_attempt(time_now, block_production_interval, time_offset);
let aura_slot = Slot::from_timestamp(timestamp, para_slot_duration);
(duration, timestamp, aura_slot)
}
fn duration_now() -> Duration {
use std::time::SystemTime;
let now = SystemTime::now();
now.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_else(|e| {
panic!("Current time {:?} is before Unix epoch. Something is wrong: {:?}", now, e)
})
}
fn time_until_next_attempt(
now: Duration,
block_production_interval: Duration,
offset: Duration,
) -> (Duration, Timestamp) {
let now = now.as_millis().saturating_sub(offset.as_millis());
let next_slot_time = ((now + block_production_interval.as_millis()) /
block_production_interval.as_millis()) *
block_production_interval.as_millis();
let remaining_millis = next_slot_time - now;
(Duration::from_millis(remaining_millis as u64), Timestamp::from(next_slot_time as u64))
}
impl<Block, Client, P> SlotTimer<Block, Client, P>
where
Block: BlockT,
Client: ProvideRuntimeApi<Block> + Send + Sync + 'static + UsageProvider<Block>,
Client::Api: AuraApi<Block, P::Public>,
P: Pair,
P::Public: AppPublic + Member + Codec,
P::Signature: TryFrom<Vec<u8>> + Member + Codec,
{
pub fn new_with_offset(
client: Arc<Client>,
time_offset: Duration,
relay_slot_duration: Duration,
) -> Self {
Self {
client,
time_offset,
last_reported_core_num: None,
relay_slot_duration,
_marker: Default::default(),
}
}
pub fn update_scheduling(&mut self, num_cores_next_block: u32) {
self.last_reported_core_num = Some(num_cores_next_block);
}
pub async fn wait_until_next_slot(&self) -> Option<SlotInfo> {
let Ok(slot_duration) = crate::slot_duration(&*self.client) else {
tracing::error!(target: LOG_TARGET, "Failed to fetch slot duration from runtime.");
return None
};
let (time_until_next_attempt, timestamp, aura_slot) = compute_next_wake_up_time(
slot_duration,
self.relay_slot_duration,
self.last_reported_core_num,
duration_now(),
self.time_offset,
);
tokio::time::sleep(time_until_next_attempt).await;
tracing::debug!(
target: LOG_TARGET,
?slot_duration,
?timestamp,
?aura_slot,
"New block production opportunity."
);
Some(SlotInfo { slot: aura_slot, timestamp })
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use sc_consensus_aura::SlotDuration;
const RELAY_CHAIN_SLOT_DURATION: u64 = 6000;
#[rstest]
#[case(6000, Some(1), 1000, 0, 5000, 6000, 1)]
#[case(6000, Some(1), 0, 0, 6000, 6000, 1)]
#[case(6000, Some(1), 6000, 0, 6000, 12000, 2)]
#[case(6000, Some(0), 6000, 0, 6000, 12000, 2)]
#[case(6000, None, 1000, 0, 5000, 6000, 1)]
#[case(6000, None, 0, 0, 6000, 6000, 1)]
#[case(6000, None, 6000, 0, 6000, 12000, 2)]
#[case(6000, Some(1), 1000, 1000, 6000, 6000, 1)]
#[case(6000, Some(1), 12000, 2000, 2000, 12000, 2)]
#[case(6000, Some(1), 12000, 6000, 6000, 12000, 2)]
#[case(6000, Some(1), 12000, 7000, 1000, 6000, 1)]
#[case(6000, Some(3), 12000, 0, 2000, 14000, 2)]
#[case(6000, Some(2), 12000, 0, 3000, 15000, 2)]
#[case(6000, Some(3), 11999, 0, 1, 12000, 2)]
#[case(6000, Some(12), 0, 0, 500, 500, 0)]
#[case(6000, Some(100), 0, 0, 500, 500, 0)]
#[case(2000, Some(1), 1000, 0, 1000, 2000, 1)]
#[case(2000, Some(1), 3000, 0, 1000, 4000, 2)]
#[case(2000, Some(1), 10000, 0, 2000, 12000, 6)]
#[case(2000, Some(2), 1000, 0, 1000, 2000, 1)]
#[case(2000, Some(3), 3000, 0, 1000, 4000, 2)]
#[case(12000, None, 0, 0, 6000, 6000, 0)]
#[case(12000, None, 6100, 0, 5900, 12000, 1)]
#[case(12000, None, 6000, 2000, 2000, 6000, 0)]
#[case(12000, Some(2), 6000, 0, 3000, 9000, 0)]
#[case(12000, Some(3), 6000, 0, 2000, 8000, 0)]
#[case(12000, Some(3), 8100, 0, 1900, 10000, 0)]
fn test_get_next_slot(
#[case] para_slot_millis: u64,
#[case] core_count: Option<u32>,
#[case] time_now: u64,
#[case] offset_millis: u64,
#[case] expected_wait_duration: u128,
#[case] expected_timestamp: u64,
#[case] expected_slot: u64,
) {
let para_slot_duration = SlotDuration::from_millis(para_slot_millis); let relay_slot_duration = Duration::from_millis(RELAY_CHAIN_SLOT_DURATION);
let time_now = Duration::from_millis(time_now); let offset = Duration::from_millis(offset_millis);
let expected_slot = Slot::from(expected_slot);
let (wait_duration, timestamp, aura_slot) = compute_next_wake_up_time(
para_slot_duration,
relay_slot_duration,
core_count,
time_now,
offset,
);
assert_eq!(wait_duration.as_millis(), expected_wait_duration, "Wait time mismatch."); assert_eq!(timestamp.as_millis(), expected_timestamp, "Timestamp mismatch.");
assert_eq!(aura_slot, expected_slot, "AURA slot mismatch.");
}
}