From 96b7cec1ceb8df01b59ebaf353a8b3b60132a97c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
 <123550+andresilva@users.noreply.github.com>
Date: Thu, 23 Apr 2020 08:42:53 +0100
Subject: [PATCH] slots: fix slot lenience methods (#5742)

* slots: extract slot lenience from babe and aura

* slots: add tests for slot lenience

* slots: fix comment in test
---
 substrate/client/consensus/aura/src/lib.rs  |  29 ++---
 substrate/client/consensus/babe/src/lib.rs  |  36 +++---
 substrate/client/consensus/slots/src/lib.rs | 117 ++++++++++++++++++++
 3 files changed, 145 insertions(+), 37 deletions(-)

diff --git a/substrate/client/consensus/aura/src/lib.rs b/substrate/client/consensus/aura/src/lib.rs
index 56674546d37..038e9e458cf 100644
--- a/substrate/client/consensus/aura/src/lib.rs
+++ b/substrate/client/consensus/aura/src/lib.rs
@@ -299,27 +299,28 @@ impl<B, C, E, I, P, Error, SO> sc_consensus_slots::SimpleSlotWorker<B> for AuraW
 	fn proposing_remaining_duration(
 		&self,
 		head: &B::Header,
-		slot_info: &SlotInfo
+		slot_info: &SlotInfo,
 	) -> Option<std::time::Duration> {
-		// never give more than 20 times more lenience.
-		const BACKOFF_CAP: u64 = 20;
-
 		let slot_remaining = self.slot_remaining_duration(slot_info);
+
 		let parent_slot = match find_pre_digest::<B, P>(head) {
 			Err(_) => return Some(slot_remaining),
 			Ok(d) => d,
 		};
 
-		// we allow a lenience of the number of slots since the head of the
-		// chain was produced, minus 1 (since there is always a difference of at least 1)
-		//
-		// linear back-off.
-		// in normal cases we only attempt to issue blocks up to the end of the slot.
-		// when the chain has been stalled for a few slots, we give more lenience.
-		let slot_lenience = slot_info.number.saturating_sub(parent_slot + 1);
-		let slot_lenience = std::cmp::min(slot_lenience, BACKOFF_CAP);
-		let slot_lenience = Duration::from_secs(slot_lenience * slot_info.duration);
-		Some(slot_lenience + slot_remaining)
+		if let Some(slot_lenience) =
+			sc_consensus_slots::slot_lenience_exponential(parent_slot, slot_info)
+		{
+			debug!(target: "aura",
+				"No block for {} slots. Applying linear lenience of {}s",
+				slot_info.number.saturating_sub(parent_slot + 1),
+				slot_lenience.as_secs(),
+			);
+
+			Some(slot_remaining + slot_lenience)
+		} else {
+			Some(slot_remaining)
+		}
 	}
 }
 
diff --git a/substrate/client/consensus/babe/src/lib.rs b/substrate/client/consensus/babe/src/lib.rs
index 092bf8153b9..59b49541821 100644
--- a/substrate/client/consensus/babe/src/lib.rs
+++ b/substrate/client/consensus/babe/src/lib.rs
@@ -510,38 +510,28 @@ impl<B, C, E, I, Error, SO> sc_consensus_slots::SimpleSlotWorker<B> for BabeWork
 	fn proposing_remaining_duration(
 		&self,
 		head: &B::Header,
-		slot_info: &SlotInfo
+		slot_info: &SlotInfo,
 	) -> Option<std::time::Duration> {
-		// never give more than 2^this times the lenience.
-		const BACKOFF_CAP: u64 = 8;
-
-		// how many slots it takes before we double the lenience.
-		const BACKOFF_STEP: u64 = 2;
-
 		let slot_remaining = self.slot_remaining_duration(slot_info);
+
 		let parent_slot = match find_pre_digest::<B>(head) {
 			Err(_) => return Some(slot_remaining),
 			Ok(d) => d.slot_number(),
 		};
 
-		// we allow a lenience of the number of slots since the head of the
-		// chain was produced, minus 1 (since there is always a difference of at least 1)
-		//
-		// exponential back-off.
-		// in normal cases we only attempt to issue blocks up to the end of the slot.
-		// when the chain has been stalled for a few slots, we give more lenience.
-		let slot_lenience = slot_info.number.saturating_sub(parent_slot + 1);
-
-		let slot_lenience = std::cmp::min(slot_lenience, BACKOFF_CAP);
-		let slot_duration = slot_info.duration << (slot_lenience / BACKOFF_STEP);
+		if let Some(slot_lenience) =
+			sc_consensus_slots::slot_lenience_exponential(parent_slot, slot_info)
+		{
+			debug!(target: "babe",
+				"No block for {} slots. Applying exponential lenience of {}s",
+				slot_info.number.saturating_sub(parent_slot + 1),
+				slot_lenience.as_secs(),
+			);
 
-		if slot_lenience >= 1 {
-			debug!(target: "babe", "No block for {} slots. Applying 2^({}/{}) lenience",
-				slot_lenience, slot_lenience, BACKOFF_STEP);
+			Some(slot_remaining + slot_lenience)
+		} else {
+			Some(slot_remaining)
 		}
-
-		let slot_lenience = Duration::from_secs(slot_duration);
-		Some(slot_lenience + slot_remaining)
 	}
 }
 
diff --git a/substrate/client/consensus/slots/src/lib.rs b/substrate/client/consensus/slots/src/lib.rs
index 2a0739a831b..611e0fbb7b8 100644
--- a/substrate/client/consensus/slots/src/lib.rs
+++ b/substrate/client/consensus/slots/src/lib.rs
@@ -483,3 +483,120 @@ impl<T: Clone> SlotDuration<T> {
 		self.0.clone()
 	}
 }
+
+/// Calculate a slot duration lenience based on the number of missed slots from current
+/// to parent. If the number of skipped slots is greated than 0 this method will apply
+/// an exponential backoff of at most `2^7 * slot_duration`, if no slots were skipped
+/// this method will return `None.`
+pub fn slot_lenience_exponential(parent_slot: u64, slot_info: &SlotInfo) -> Option<Duration> {
+	// never give more than 2^this times the lenience.
+	const BACKOFF_CAP: u64 = 7;
+
+	// how many slots it takes before we double the lenience.
+	const BACKOFF_STEP: u64 = 2;
+
+	// we allow a lenience of the number of slots since the head of the
+	// chain was produced, minus 1 (since there is always a difference of at least 1)
+	//
+	// exponential back-off.
+	// in normal cases we only attempt to issue blocks up to the end of the slot.
+	// when the chain has been stalled for a few slots, we give more lenience.
+	let skipped_slots = slot_info.number.saturating_sub(parent_slot + 1);
+
+	if skipped_slots == 0 {
+		None
+	} else {
+		let slot_lenience = skipped_slots / BACKOFF_STEP;
+		let slot_lenience = std::cmp::min(slot_lenience, BACKOFF_CAP);
+		let slot_lenience = 1 << slot_lenience;
+		Some(Duration::from_millis(slot_lenience * slot_info.duration))
+	}
+}
+
+/// Calculate a slot duration lenience based on the number of missed slots from current
+/// to parent. If the number of skipped slots is greated than 0 this method will apply
+/// a linear backoff of at most `20 * slot_duration`, if no slots were skipped
+/// this method will return `None.`
+pub fn slot_lenience_linear(parent_slot: u64, slot_info: &SlotInfo) -> Option<Duration> {
+	// never give more than 20 times more lenience.
+	const BACKOFF_CAP: u64 = 20;
+
+	// we allow a lenience of the number of slots since the head of the
+	// chain was produced, minus 1 (since there is always a difference of at least 1)
+	//
+	// linear back-off.
+	// in normal cases we only attempt to issue blocks up to the end of the slot.
+	// when the chain has been stalled for a few slots, we give more lenience.
+	let skipped_slots = slot_info.number.saturating_sub(parent_slot + 1);
+
+	if skipped_slots == 0 {
+		None
+	} else {
+		let slot_lenience = std::cmp::min(skipped_slots, BACKOFF_CAP);
+		Some(Duration::from_millis(slot_lenience * slot_info.duration))
+	}
+}
+
+#[cfg(test)]
+mod test {
+	use std::time::{Duration, Instant};
+
+	const SLOT_DURATION: Duration = Duration::from_millis(6000);
+
+	fn slot(n: u64) -> super::slots::SlotInfo {
+		super::slots::SlotInfo {
+			number: n,
+			last_number: n - 1,
+			duration: SLOT_DURATION.as_millis() as u64,
+			timestamp: Default::default(),
+			inherent_data: Default::default(),
+			ends_at: Instant::now(),
+		}
+	}
+
+	#[test]
+	fn linear_slot_lenience() {
+		// if no slots are skipped there should be no lenience
+		assert_eq!(super::slot_lenience_linear(1, &slot(2)), None);
+
+		// otherwise the lenience is incremented linearly with
+		// the number of skipped slots.
+		for n in 3..=22 {
+			assert_eq!(
+				super::slot_lenience_linear(1, &slot(n)),
+				Some(SLOT_DURATION * (n - 2) as u32),
+			);
+		}
+
+		// but we cap it to a maximum of 20 slots
+		assert_eq!(
+			super::slot_lenience_linear(1, &slot(23)),
+			Some(SLOT_DURATION * 20),
+		);
+	}
+
+	#[test]
+	fn exponential_slot_lenience() {
+		// if no slots are skipped there should be no lenience
+		assert_eq!(super::slot_lenience_exponential(1, &slot(2)), None);
+
+		// otherwise the lenience is incremented exponentially every two slots
+		for n in 3..=17 {
+			assert_eq!(
+				super::slot_lenience_exponential(1, &slot(n)),
+				Some(SLOT_DURATION * 2u32.pow((n / 2 - 1) as u32)),
+			);
+		}
+
+		// but we cap it to a maximum of 14 slots
+		assert_eq!(
+			super::slot_lenience_exponential(1, &slot(18)),
+			Some(SLOT_DURATION * 2u32.pow(7)),
+		);
+
+		assert_eq!(
+			super::slot_lenience_exponential(1, &slot(19)),
+			Some(SLOT_DURATION * 2u32.pow(7)),
+		);
+	}
+}
-- 
GitLab