> + Member + Codec,
+{
+ let relay_slot: Slot = get_relay_slot(internal_scheduling_parent_header)?;
+
+ // Authorities and slot duration are read at `authority_anchor` (the walked segment's tip),
+ // which is on the same fork as the blocks we are resubmitting.
+ let para_slot_duration = crate::slot_duration_at(&**para_client, authority_anchor).ok()?;
+ let para_slot_duration_ms = para_slot_duration.as_millis();
+ let para_slot = relay_slot_to_para_slot(relay_slot, para_slot_duration_ms)?;
+
+ let mut runtime_api = para_client.runtime_api();
+ runtime_api.set_call_context(sp_core::traits::CallContext::Onchain { import: false });
+ let authorities = runtime_api.authorities(authority_anchor).ok()?;
+
+ // claim_slot returns the author public key only if we control its key in the keystore.
+ let author_pub =
+ aura_internal::claim_slot::(Slot::from(para_slot), &authorities, keystore).await?;
+
+ let signature = keystore
+ .sign_with(
+ <
::Public as AppCrypto>::ID,
+ <
::Public as AppCrypto>::CRYPTO_ID,
+ author_pub.as_slice(),
+ &payload.encode(),
+ )
+ .ok()
+ .flatten()?;
+ let signature: [u8; 64] = signature.try_into().ok()?;
+
+ Some(SignedSchedulingInfo { payload, signature })
+}
+
+/// Map a relay-chain slot to the parachain slot used for eligible-author selection. Mirrors the
+/// formula used by the on-chain verifier in
+/// . Returns `None` for a zero slot
+/// duration so the caller skips signing rather than dividing by zero.
+fn relay_slot_to_para_slot(relay_slot: Slot, para_slot_duration_ms: u64) -> Option {
+ if para_slot_duration_ms == 0 {
+ return None;
+ }
+ u64::from(relay_slot)
+ .saturating_mul(RELAY_CHAIN_SLOT_DURATION_MILLIS)
+ .checked_div(para_slot_duration_ms)
+}
+
+/// Convert a libp2p [`PeerId`] into the on-chain [`ApprovedPeerId`] (a 64-byte-bounded vector).
+fn peer_id_to_approved(peer_id: PeerId) -> ApprovedPeerId {
+ ApprovedPeerId::truncate_from(peer_id.to_bytes())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn relay_slot_to_para_slot_matches_verifier_formula() {
+ // 6s relay slot, 2s para slots: three para slots per relay slot.
+ assert_eq!(relay_slot_to_para_slot(Slot::from(10), 2_000), Some(30));
+ // Equal durations: 1:1 mapping.
+ assert_eq!(relay_slot_to_para_slot(Slot::from(7), 6_000), Some(7));
+ // 6s para slot duration with a 12s "double" duration: floor division.
+ assert_eq!(relay_slot_to_para_slot(Slot::from(5), 12_000), Some(2));
+ // Zero duration is guarded rather than panicking.
+ assert_eq!(relay_slot_to_para_slot(Slot::from(1), 0), None);
+ }
+
+ #[test]
+ fn peer_id_round_trips_through_approved_peer_id() {
+ let peer_id = PeerId::random();
+ // PeerId encodings are ~38 bytes, well under the 64-byte bound, so no truncation.
+ assert_eq!(peer_id_to_approved(peer_id).into_inner(), peer_id.to_bytes());
+ }
+}
diff --git a/cumulus/client/consensus/aura/src/collators/slot_based/tests.rs b/cumulus/client/consensus/aura/src/collators/slot_based/tests.rs
index cb1e3cf9985dc..9639d9c0fc683 100644
--- a/cumulus/client/consensus/aura/src/collators/slot_based/tests.rs
+++ b/cumulus/client/consensus/aura/src/collators/slot_based/tests.rs
@@ -506,6 +506,16 @@ impl RelayChainInterface for TestRelayClient {
unimplemented!("Not needed for test")
}
+ async fn ancestor_relay_parent_info(
+ &self,
+ _at: RelayHash,
+ _session_index: SessionIndex,
+ _relay_parent: RelayHash,
+ ) -> RelayChainResult