Skip to main content

pop_fork/inherent/
timestamp.rs

1// SPDX-License-Identifier: GPL-3.0
2
3//! Timestamp inherent provider for block building.
4//!
5//! This module provides [`TimestampInherent`] which generates the mandatory
6//! timestamp inherent extrinsic for each block. The timestamp pallet requires
7//! this inherent to advance the chain's notion of time.
8//!
9//! # How It Works
10//!
11//! 1. Look up the Timestamp pallet and `set` call indices from runtime metadata
12//! 2. Detect slot duration using the following fallback chain:
13//!    - `AuraApi_slot_duration` runtime API (Aura-based chains)
14//!    - `Babe::ExpectedBlockTime` metadata constant (Babe-based chains)
15//!    - Configured default slot duration
16//! 3. Read the current timestamp from `Timestamp::Now` storage
17//! 4. Add the slot duration to get the new timestamp
18//! 5. Encode a `timestamp.set(new_timestamp)` call using the dynamic indices
19//! 6. Wrap it as an unsigned inherent extrinsic
20//!
21//! # Example
22//!
23//! ```ignore
24//! use pop_fork::inherent::TimestampInherent;
25//!
26//! // Create with default 6-second slots (relay chain)
27//! let provider = TimestampInherent::default_relay();
28//!
29//! // Create with custom slot duration
30//! let provider = TimestampInherent::new(2_000); // 2 seconds
31//! ```
32
33use crate::{
34	Block, BlockBuilderError, RuntimeExecutor,
35	inherent::InherentProvider,
36	strings::inherent::timestamp::{
37		self as strings,
38		slot_duration::{
39			PARACHAIN_FALLBACK_MS as DEFAULT_PARA_SLOT_DURATION_MS,
40			RELAY_CHAIN_FALLBACK_MS as DEFAULT_RELAY_SLOT_DURATION_MS,
41		},
42	},
43};
44use async_trait::async_trait;
45use scale::{Compact, Decode, Encode};
46use std::sync::atomic::{AtomicU64, Ordering};
47use subxt::Metadata;
48
49/// Extrinsic format version for unsigned/bare extrinsics.
50/// Version 5 is current; version 4 is legacy.
51const EXTRINSIC_FORMAT_VERSION: u8 = 5;
52
53/// Timestamp inherent provider.
54///
55/// Generates the `timestamp.set(now)` inherent extrinsic that advances
56/// the chain's timestamp by the slot duration.
57///
58/// # Slot Duration Detection
59///
60/// The provider attempts to detect the slot duration dynamically using
61/// the following fallback chain:
62/// 1. `AuraApi_slot_duration` runtime API (Aura-based chains)
63/// 2. `Babe::ExpectedBlockTime` metadata constant (Babe-based chains)
64/// 3. Configured default slot duration (default: 6 seconds)
65///
66/// # Dynamic Metadata Lookup
67///
68/// The pallet and call indices are looked up dynamically from the runtime
69/// metadata, making this provider work across different runtimes without
70/// manual configuration.
71pub struct TimestampInherent {
72	/// Slot duration in milliseconds (fallback value).
73	slot_duration_ms: u64,
74	/// Cached slot duration detected from the runtime.
75	/// Initialized during warmup or lazily on first `provide()` call.
76	/// A value of 0 means "not yet detected".
77	cached_slot_duration: AtomicU64,
78}
79
80impl TimestampInherent {
81	/// Create a new timestamp inherent provider.
82	///
83	/// # Arguments
84	///
85	/// * `slot_duration_ms` - Slot duration in milliseconds
86	pub fn new(slot_duration_ms: u64) -> Self {
87		Self { slot_duration_ms, cached_slot_duration: AtomicU64::new(0) }
88	}
89
90	/// Create with default settings for relay chains (6-second slots).
91	pub fn default_relay() -> Self {
92		Self::new(DEFAULT_RELAY_SLOT_DURATION_MS)
93	}
94
95	/// Create with default settings for parachains (12-second slots).
96	pub fn default_para() -> Self {
97		Self::new(DEFAULT_PARA_SLOT_DURATION_MS)
98	}
99
100	/// Compute the storage key for `Timestamp::Now`.
101	///
102	/// The key is constructed as: `twox_128("Timestamp") ++ twox_128("Now")`
103	///
104	/// # Returns
105	///
106	/// A 32-byte storage key.
107	pub fn timestamp_now_key() -> Vec<u8> {
108		let pallet_hash = sp_core::twox_128(strings::storage_keys::PALLET_NAME);
109		let storage_hash = sp_core::twox_128(strings::storage_keys::NOW);
110		[pallet_hash.as_slice(), storage_hash.as_slice()].concat()
111	}
112
113	/// Encode the `timestamp.set(now)` call.
114	///
115	/// The call is encoded as: `[pallet_index, call_index, Compact<u64>]`
116	fn encode_timestamp_set_call(pallet_index: u8, call_index: u8, timestamp: u64) -> Vec<u8> {
117		let mut call = vec![pallet_index, call_index];
118		// Timestamp argument is encoded as Compact<u64>
119		call.extend(Compact(timestamp).encode());
120		call
121	}
122
123	/// Wrap a call as an unsigned inherent extrinsic.
124	///
125	/// Unsigned extrinsics have the format:
126	/// - Compact length prefix
127	/// - Version byte
128	/// - Call data
129	fn encode_inherent_extrinsic(call: Vec<u8>) -> Vec<u8> {
130		let mut extrinsic = vec![EXTRINSIC_FORMAT_VERSION];
131		extrinsic.extend(call);
132
133		// Prefix with compact length
134		let len = Compact(extrinsic.len() as u32);
135		let mut result = len.encode();
136		result.extend(extrinsic);
137		result
138	}
139
140	/// Get slot duration from the runtime, falling back to the provided default.
141	///
142	/// This function uses a three-tier detection mechanism:
143	/// 1. `AuraApi_slot_duration` runtime API (Aura-based chains)
144	/// 2. `Babe::ExpectedBlockTime` metadata constant (Babe-based chains)
145	/// 3. Fallback to the provided default value
146	///
147	/// # Arguments
148	///
149	/// * `executor` - Runtime executor for calling runtime APIs
150	/// * `storage` - Storage layer for state access
151	/// * `metadata` - Runtime metadata for constant lookup
152	/// * `fallback` - Default slot duration if detection fails (in milliseconds)
153	///
154	/// # Returns
155	///
156	/// The slot duration in milliseconds.
157	pub async fn get_slot_duration_from_runtime(
158		executor: &RuntimeExecutor,
159		storage: &crate::LocalStorageLayer,
160		metadata: &Metadata,
161		fallback: u64,
162	) -> u64 {
163		// 1. Try AuraApi_slot_duration runtime API
164		if let Some(duration) = executor
165			.call(strings::slot_duration::AURA_API_METHOD, &[], storage)
166			.await
167			.ok()
168			.and_then(|r| u64::decode(&mut r.output.as_slice()).ok())
169		{
170			return duration;
171		}
172
173		// 2. Try Babe::ExpectedBlockTime metadata constant
174		if let Some(duration) = Self::get_constant_from_metadata(
175			metadata,
176			strings::slot_duration::BABE_PALLET,
177			strings::slot_duration::BABE_EXPECTED_BLOCK_TIME,
178		) {
179			return duration;
180		}
181
182		// 3. Fall back to configured default
183		fallback
184	}
185
186	/// Attempt to read a u64 constant from metadata.
187	fn get_constant_from_metadata(
188		metadata: &Metadata,
189		pallet_name: &str,
190		constant_name: &str,
191	) -> Option<u64> {
192		metadata
193			.pallet_by_name(pallet_name)?
194			.constant_by_name(constant_name)
195			.and_then(|c| u64::decode(&mut &c.value()[..]).ok())
196	}
197}
198
199impl Default for TimestampInherent {
200	fn default() -> Self {
201		Self::default_relay()
202	}
203}
204
205#[async_trait]
206impl InherentProvider for TimestampInherent {
207	fn identifier(&self) -> &'static str {
208		strings::IDENTIFIER
209	}
210
211	async fn provide(
212		&self,
213		parent: &Block,
214		executor: &RuntimeExecutor,
215	) -> Result<Vec<Vec<u8>>, BlockBuilderError> {
216		// Look up pallet and call indices from metadata
217		let metadata = parent.metadata().await?;
218
219		let pallet = metadata.pallet_by_name(strings::metadata::PALLET_NAME).ok_or_else(|| {
220			BlockBuilderError::InherentProvider {
221				provider: self.identifier().to_string(),
222				message: format!(
223					"{}: {}",
224					strings::errors::PALLET_NOT_FOUND,
225					strings::metadata::PALLET_NAME
226				),
227			}
228		})?;
229
230		let pallet_index = pallet.index();
231
232		let call_variant = pallet
233			.call_variant_by_name(strings::metadata::SET_CALL_NAME)
234			.ok_or_else(|| BlockBuilderError::InherentProvider {
235				provider: self.identifier().to_string(),
236				message: format!(
237					"{}: {}",
238					strings::errors::CALL_NOT_FOUND,
239					strings::metadata::SET_CALL_NAME
240				),
241			})?;
242
243		let call_index = call_variant.index;
244
245		// Get slot duration from cache (populated during warmup) or detect from runtime.
246		let storage = parent.storage();
247		let slot_duration = match self.cached_slot_duration.load(Ordering::Acquire) {
248			0 => {
249				let duration = Self::get_slot_duration_from_runtime(
250					executor,
251					storage,
252					&metadata,
253					self.slot_duration_ms,
254				)
255				.await;
256				self.cached_slot_duration.store(duration, Ordering::Release);
257				duration
258			},
259			cached => cached,
260		};
261
262		// Read current timestamp from parent block storage
263		let key = Self::timestamp_now_key();
264
265		let current_timestamp = match storage.get(parent.number, &key).await? {
266			Some(value) if value.value.is_some() => u64::decode(
267				&mut value
268					.value
269					.as_ref()
270					.expect("The match guard ensures it's Some; qed;")
271					.as_slice(),
272			)
273			.map_err(|e| BlockBuilderError::InherentProvider {
274				provider: self.identifier().to_string(),
275				message: format!("{}: {}", strings::errors::DECODE_FAILED, e),
276			})?,
277			_ => {
278				// No timestamp set yet (genesis or very early block)
279				// Use current system time as fallback
280				std::time::SystemTime::now()
281					.duration_since(std::time::UNIX_EPOCH)
282					.map(|d| d.as_millis() as u64)
283					.unwrap_or(0)
284			},
285		};
286
287		// Calculate new timestamp
288		let new_timestamp = current_timestamp.saturating_add(slot_duration);
289
290		log::debug!(
291			"[Timestamp] current_timestamp={current_timestamp}, slot_duration={slot_duration}, new_timestamp={new_timestamp}"
292		);
293
294		// Encode the timestamp.set call with dynamic indices
295		let call = Self::encode_timestamp_set_call(pallet_index, call_index, new_timestamp);
296
297		// Wrap as unsigned extrinsic
298		let extrinsic = Self::encode_inherent_extrinsic(call);
299
300		Ok(vec![extrinsic])
301	}
302
303	async fn warmup(&self, parent: &Block, executor: &RuntimeExecutor) {
304		let metadata = match parent.metadata().await {
305			Ok(m) => m,
306			Err(e) => {
307				log::warn!("[Timestamp] Warmup: failed to get metadata: {e}");
308				return;
309			},
310		};
311		let storage = parent.storage();
312		let duration = Self::get_slot_duration_from_runtime(
313			executor,
314			storage,
315			&metadata,
316			self.slot_duration_ms,
317		)
318		.await;
319		self.cached_slot_duration.store(duration, Ordering::Release);
320		log::debug!("[Timestamp] Warmup: cached slot_duration={duration}ms");
321	}
322
323	fn invalidate_cache(&self) {
324		self.cached_slot_duration.store(0, Ordering::Release);
325		log::debug!("[Timestamp] Cache invalidated (runtime upgrade detected)");
326	}
327}
328
329#[cfg(test)]
330mod tests {
331	use super::*;
332
333	/// Custom slot duration for testing (1 second).
334	const TEST_SLOT_DURATION_MS: u64 = 1_000;
335
336	/// Test pallet index (arbitrary value for encoding tests).
337	const TEST_PALLET_INDEX: u8 = 3;
338
339	/// Test call index (arbitrary value for encoding tests).
340	const TEST_CALL_INDEX: u8 = 0;
341
342	#[test]
343	fn new_creates_provider_with_slot_duration() {
344		let provider = TimestampInherent::new(TEST_SLOT_DURATION_MS);
345		assert_eq!(provider.slot_duration_ms, TEST_SLOT_DURATION_MS);
346	}
347
348	#[test]
349	fn default_relay_uses_configured_slot_duration() {
350		let provider = TimestampInherent::default_relay();
351		assert_eq!(provider.slot_duration_ms, DEFAULT_RELAY_SLOT_DURATION_MS);
352	}
353
354	#[test]
355	fn default_para_uses_configured_slot_duration() {
356		let provider = TimestampInherent::default_para();
357		assert_eq!(provider.slot_duration_ms, DEFAULT_PARA_SLOT_DURATION_MS);
358	}
359
360	#[test]
361	fn timestamp_now_key_is_32_bytes() {
362		let key = TimestampInherent::timestamp_now_key();
363		// twox128 produces 16 bytes per hash, storage key = pallet hash + item hash
364		const TWOX128_OUTPUT_BYTES: usize = 16;
365		const STORAGE_KEY_LEN: usize = TWOX128_OUTPUT_BYTES * 2;
366		assert_eq!(key.len(), STORAGE_KEY_LEN);
367	}
368
369	#[test]
370	fn encode_timestamp_set_call_produces_valid_encoding() {
371		let call = TimestampInherent::encode_timestamp_set_call(
372			TEST_PALLET_INDEX,
373			TEST_CALL_INDEX,
374			1_000_000,
375		);
376
377		// First byte is pallet index
378		assert_eq!(call[0], TEST_PALLET_INDEX);
379		// Second byte is call index
380		assert_eq!(call[1], TEST_CALL_INDEX);
381		// Rest is compact-encoded timestamp
382		assert!(call.len() > 2);
383	}
384
385	#[test]
386	fn encode_inherent_extrinsic_includes_version_and_length() {
387		// Create fake call data
388		let call = vec![TEST_PALLET_INDEX, TEST_CALL_INDEX, 1, 2, 3];
389		let extrinsic = TimestampInherent::encode_inherent_extrinsic(call.clone());
390
391		// Should start with compact length (6 = version byte + 5 call bytes)
392		// Compact encoding of 6 is (6 << 2) = 0x18
393		const EXPECTED_COMPACT_LEN: u8 = 0x18;
394		assert_eq!(extrinsic[0], EXPECTED_COMPACT_LEN);
395		// Next byte is extrinsic format version
396		assert_eq!(extrinsic[1], EXTRINSIC_FORMAT_VERSION);
397		// Rest is the call
398		assert_eq!(&extrinsic[2..], &call[..]);
399	}
400
401	#[test]
402	fn identifier_returns_timestamp() {
403		let provider = TimestampInherent::default();
404		assert_eq!(provider.identifier(), strings::IDENTIFIER);
405	}
406}