From 095f4bd9ae6ed691b19768f7daac9e6833f425ed Mon Sep 17 00:00:00 2001
From: Davide Galassi <davxy@datawok.net>
Date: Fri, 1 Dec 2023 16:39:07 +0100
Subject: [PATCH] Sassafras Consensus Pallet (#1577)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This PR introduces the pallet for Sassafras consensus.

## Non Goals

The pallet delivers only the bare-bones and doesn't deliver support for
auxiliary functionalities such as equivocation report and support for
epoch change via session pallet.

These functionalities were drafted in the [main
PR](https://github.com/paritytech/polkadot-sdk/pull/1336), but IMO is
better to introduce this auxiliary stuff in a follow up PR and after
client code.

## Potential follow ups

https://github.com/paritytech/polkadot-sdk/issues/2364

---------

Co-authored-by: Sebastian Kunert <skunert49@gmail.com>
Co-authored-by: Koute <koute@users.noreply.github.com>
Co-authored-by: Michal Kucharczyk <1728078+michalkucharczyk@users.noreply.github.com>
Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com>
Co-authored-by: Bastian Köcher <git@kchr.de>
---
 Cargo.lock                                    |   50 +-
 Cargo.toml                                    |    1 +
 substrate/frame/sassafras/Cargo.toml          |   59 +
 substrate/frame/sassafras/README.md           |    8 +
 substrate/frame/sassafras/src/benchmarking.rs |  272 +++++
 .../src/data/25_tickets_100_auths.bin         |  Bin 0 -> 24728 bytes
 .../sassafras/src/data/benchmark-results.md   |   99 ++
 .../frame/sassafras/src/data/tickets-sort.md  |  274 +++++
 .../frame/sassafras/src/data/tickets-sort.png |  Bin 0 -> 33919 bytes
 substrate/frame/sassafras/src/lib.rs          | 1081 +++++++++++++++++
 substrate/frame/sassafras/src/mock.rs         |  343 ++++++
 substrate/frame/sassafras/src/tests.rs        |  874 +++++++++++++
 substrate/frame/sassafras/src/weights.rs      |  425 +++++++
 .../primitives/consensus/sassafras/Cargo.toml |   12 +-
 .../primitives/consensus/sassafras/README.md  |   12 +-
 .../consensus/sassafras/src/digests.rs        |    6 +-
 .../primitives/consensus/sassafras/src/lib.rs |   38 +-
 .../consensus/sassafras/src/ticket.rs         |   43 +-
 .../primitives/consensus/sassafras/src/vrf.rs |    2 +-
 substrate/primitives/core/Cargo.toml          |    2 +-
 substrate/primitives/core/src/bandersnatch.rs |  289 +++--
 21 files changed, 3763 insertions(+), 127 deletions(-)
 create mode 100644 substrate/frame/sassafras/Cargo.toml
 create mode 100644 substrate/frame/sassafras/README.md
 create mode 100644 substrate/frame/sassafras/src/benchmarking.rs
 create mode 100644 substrate/frame/sassafras/src/data/25_tickets_100_auths.bin
 create mode 100644 substrate/frame/sassafras/src/data/benchmark-results.md
 create mode 100644 substrate/frame/sassafras/src/data/tickets-sort.md
 create mode 100644 substrate/frame/sassafras/src/data/tickets-sort.png
 create mode 100644 substrate/frame/sassafras/src/lib.rs
 create mode 100644 substrate/frame/sassafras/src/mock.rs
 create mode 100644 substrate/frame/sassafras/src/tests.rs
 create mode 100644 substrate/frame/sassafras/src/weights.rs

diff --git a/Cargo.lock b/Cargo.lock
index 9c5a2c57da0..241a2fd3d15 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -544,7 +544,7 @@ dependencies = [
 [[package]]
 name = "ark-secret-scalar"
 version = "0.0.2"
-source = "git+https://github.com/w3f/ring-vrf?rev=3ddc205#3ddc2051066c4b3f0eadd0ba5700df12500d9754"
+source = "git+https://github.com/w3f/ring-vrf?rev=2019248#2019248785389b3246d55b1c3b0e9bdef4454cb7"
 dependencies = [
  "ark-ec",
  "ark-ff",
@@ -552,7 +552,7 @@ dependencies = [
  "ark-std",
  "ark-transcript",
  "digest 0.10.7",
- "rand_core 0.6.4",
+ "getrandom_or_panic",
  "zeroize",
 ]
 
@@ -593,7 +593,7 @@ dependencies = [
 [[package]]
 name = "ark-transcript"
 version = "0.0.2"
-source = "git+https://github.com/w3f/ring-vrf?rev=3ddc205#3ddc2051066c4b3f0eadd0ba5700df12500d9754"
+source = "git+https://github.com/w3f/ring-vrf?rev=2019248#2019248785389b3246d55b1c3b0e9bdef4454cb7"
 dependencies = [
  "ark-ff",
  "ark-serialize",
@@ -1225,7 +1225,7 @@ dependencies = [
 [[package]]
 name = "bandersnatch_vrfs"
 version = "0.0.4"
-source = "git+https://github.com/w3f/ring-vrf?rev=3ddc205#3ddc2051066c4b3f0eadd0ba5700df12500d9754"
+source = "git+https://github.com/w3f/ring-vrf?rev=2019248#2019248785389b3246d55b1c3b0e9bdef4454cb7"
 dependencies = [
  "ark-bls12-381",
  "ark-ec",
@@ -2716,7 +2716,7 @@ dependencies = [
 [[package]]
 name = "common"
 version = "0.1.0"
-source = "git+https://github.com/burdges/ring-proof?branch=patch-1#05a756076cb20f981a52afea3a620168de49f95f"
+source = "git+https://github.com/w3f/ring-proof#61e7b528bc0170d6bf541be32440d569b784425d"
 dependencies = [
  "ark-ec",
  "ark-ff",
@@ -2724,6 +2724,7 @@ dependencies = [
  "ark-serialize",
  "ark-std",
  "fflonk",
+ "getrandom_or_panic",
  "merlin 3.0.0",
  "rand_chacha 0.3.1",
 ]
@@ -4525,7 +4526,7 @@ checksum = "86e3bdc80eee6e16b2b6b0f87fbc98c04bee3455e35174c0de1a125d0688c632"
 [[package]]
 name = "dleq_vrf"
 version = "0.0.2"
-source = "git+https://github.com/w3f/ring-vrf?rev=3ddc205#3ddc2051066c4b3f0eadd0ba5700df12500d9754"
+source = "git+https://github.com/w3f/ring-vrf?rev=2019248#2019248785389b3246d55b1c3b0e9bdef4454cb7"
 dependencies = [
  "ark-ec",
  "ark-ff",
@@ -4535,7 +4536,6 @@ dependencies = [
  "ark-std",
  "ark-transcript",
  "arrayvec 0.7.4",
- "rand_core 0.6.4",
  "zeroize",
 ]
 
@@ -4869,9 +4869,9 @@ dependencies = [
 
 [[package]]
 name = "env_logger"
-version = "0.10.0"
+version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
+checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece"
 dependencies = [
  "humantime",
  "is-terminal",
@@ -5132,7 +5132,7 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "84f2e425d9790201ba4af4630191feac6dcc98765b118d4d18e91d23c2353866"
 dependencies = [
- "env_logger 0.10.0",
+ "env_logger 0.10.1",
  "log",
 ]
 
@@ -5912,6 +5912,16 @@ dependencies = [
  "wasi 0.11.0+wasi-snapshot-preview1",
 ]
 
+[[package]]
+name = "getrandom_or_panic"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9"
+dependencies = [
+ "rand 0.8.5",
+ "rand_core 0.6.4",
+]
+
 [[package]]
 name = "ghash"
 version = "0.4.4"
@@ -10509,6 +10519,24 @@ dependencies = [
  "sp-std 8.0.0",
 ]
 
+[[package]]
+name = "pallet-sassafras"
+version = "0.3.5-dev"
+dependencies = [
+ "array-bytes 6.1.0",
+ "frame-benchmarking",
+ "frame-support",
+ "frame-system",
+ "log",
+ "parity-scale-codec",
+ "scale-info",
+ "sp-consensus-sassafras",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-std 8.0.0",
+]
+
 [[package]]
 name = "pallet-scheduler"
 version = "4.0.0-dev"
@@ -14164,7 +14192,7 @@ dependencies = [
 [[package]]
 name = "ring"
 version = "0.1.0"
-source = "git+https://github.com/burdges/ring-proof?branch=patch-1#05a756076cb20f981a52afea3a620168de49f95f"
+source = "git+https://github.com/w3f/ring-proof#61e7b528bc0170d6bf541be32440d569b784425d"
 dependencies = [
  "ark-ec",
  "ark-ff",
diff --git a/Cargo.toml b/Cargo.toml
index 0a7bf912e48..5fb7c0f2315 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -346,6 +346,7 @@ members = [
 	"substrate/frame/root-testing",
 	"substrate/frame/safe-mode",
 	"substrate/frame/salary",
+	"substrate/frame/sassafras",
 	"substrate/frame/scheduler",
 	"substrate/frame/scored-pool",
 	"substrate/frame/session",
diff --git a/substrate/frame/sassafras/Cargo.toml b/substrate/frame/sassafras/Cargo.toml
new file mode 100644
index 00000000000..7ab2e2e1770
--- /dev/null
+++ b/substrate/frame/sassafras/Cargo.toml
@@ -0,0 +1,59 @@
+[package]
+name = "pallet-sassafras"
+version = "0.3.5-dev"
+authors = ["Parity Technologies <admin@parity.io>"]
+edition = "2021"
+license = "Apache-2.0"
+homepage = "https://substrate.io"
+repository = "https://github.com/paritytech/substrate/"
+description = "Consensus extension module for Sassafras consensus."
+readme = "README.md"
+publish = false
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[dependencies]
+scale-codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] }
+scale-info = { version = "2.5.0", default-features = false, features = ["derive"] }
+frame-benchmarking = { path = "../benchmarking", default-features = false, optional = true  }
+frame-support = { path = "../support", default-features = false  }
+frame-system = { path = "../system", default-features = false  }
+log = { version = "0.4.17", default-features = false }
+sp-consensus-sassafras = { path = "../../primitives/consensus/sassafras", default-features = false, features = ["serde"] }
+sp-io = { path = "../../primitives/io", default-features = false  }
+sp-runtime = { path = "../../primitives/runtime", default-features = false }
+sp-std = { path = "../../primitives/std", default-features = false }
+
+[dev-dependencies]
+array-bytes = "6.1"
+sp-core = { path = "../../primitives/core" }
+
+[features]
+default = [ "std" ]
+std = [
+	"frame-benchmarking?/std",
+	"frame-support/std",
+	"frame-system/std",
+	"log/std",
+	"scale-codec/std",
+	"scale-info/std",
+	"sp-consensus-sassafras/std",
+	"sp-io/std",
+	"sp-runtime/std",
+	"sp-std/std",
+]
+runtime-benchmarks = [
+	"frame-benchmarking/runtime-benchmarks",
+	"frame-support/runtime-benchmarks",
+	"frame-system/runtime-benchmarks",
+	"sp-runtime/runtime-benchmarks",
+]
+try-runtime = [
+	"frame-support/try-runtime",
+	"frame-system/try-runtime",
+	"sp-runtime/try-runtime",
+]
+# Construct dummy ring context on genesis.
+# Mostly used for testing and development.
+construct-dummy-ring-context = []
diff --git a/substrate/frame/sassafras/README.md b/substrate/frame/sassafras/README.md
new file mode 100644
index 00000000000..f0e24a05355
--- /dev/null
+++ b/substrate/frame/sassafras/README.md
@@ -0,0 +1,8 @@
+Runtime module for SASSAFRAS consensus.
+
+- Tracking issue: https://github.com/paritytech/polkadot-sdk/issues/41
+- Protocol RFC proposal: https://github.com/polkadot-fellows/RFCs/pull/26
+
+# ⚠️ WARNING ⚠️
+
+The crate interfaces and structures are experimental and may be subject to changes.
diff --git a/substrate/frame/sassafras/src/benchmarking.rs b/substrate/frame/sassafras/src/benchmarking.rs
new file mode 100644
index 00000000000..95a2b4bbce4
--- /dev/null
+++ b/substrate/frame/sassafras/src/benchmarking.rs
@@ -0,0 +1,272 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Benchmarks for the Sassafras pallet.
+
+use crate::*;
+use sp_consensus_sassafras::{vrf::VrfSignature, EphemeralPublic, EpochConfiguration};
+
+use frame_benchmarking::v2::*;
+use frame_support::traits::Hooks;
+use frame_system::RawOrigin;
+
+const LOG_TARGET: &str = "sassafras::benchmark";
+
+const TICKETS_DATA: &[u8] = include_bytes!("data/25_tickets_100_auths.bin");
+
+fn make_dummy_vrf_signature() -> VrfSignature {
+	// This leverages our knowledge about serialized vrf signature structure.
+	// Mostly to avoid to import all the bandersnatch primitive just for this test.
+	let buf = [
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xb5, 0x5f, 0x8e, 0xc7, 0x68, 0xf5, 0x05, 0x3f, 0xa9,
+		0x18, 0xca, 0x07, 0x13, 0xc7, 0x4b, 0xa3, 0x9a, 0x97, 0xd3, 0x76, 0x8f, 0x0c, 0xbf, 0x2e,
+		0xd4, 0xf9, 0x3a, 0xae, 0xc1, 0x96, 0x2a, 0x64, 0x80,
+	];
+	VrfSignature::decode(&mut &buf[..]).unwrap()
+}
+
+#[benchmarks]
+mod benchmarks {
+	use super::*;
+
+	// For first block (#1) we do some extra operation.
+	// But is a one shot operation, so we don't account for it here.
+	// We use 0, as it will be the path used by all the blocks with n != 1
+	#[benchmark]
+	fn on_initialize() {
+		let block_num = BlockNumberFor::<T>::from(0u32);
+
+		let slot_claim = SlotClaim {
+			authority_idx: 0,
+			slot: Default::default(),
+			vrf_signature: make_dummy_vrf_signature(),
+			ticket_claim: None,
+		};
+		frame_system::Pallet::<T>::deposit_log((&slot_claim).into());
+
+		// We currently don't account for the potential weight added by the `on_finalize`
+		// incremental sorting of the tickets.
+
+		#[block]
+		{
+			// According to `Hooks` trait docs, `on_finalize` `Weight` should be bundled
+			// together with `on_initialize` `Weight`.
+			Pallet::<T>::on_initialize(block_num);
+			Pallet::<T>::on_finalize(block_num)
+		}
+	}
+
+	// Weight for the default internal epoch change trigger.
+	//
+	// Parameters:
+	// - `x`: number of authorities (1:100).
+	// - `y`: epoch length in slots (1000:5000)
+	//
+	// This accounts for the worst case which includes:
+	// - load the full ring context.
+	// - recompute the ring verifier.
+	// - sorting the epoch tickets in one shot
+	//  (here we account for the very unluky scenario where we haven't done any sort work yet)
+	// - pending epoch change config.
+	//
+	// For this bench we assume a redundancy factor of 2 (suggested value to be used in prod).
+	#[benchmark]
+	fn enact_epoch_change(x: Linear<1, 100>, y: Linear<1000, 5000>) {
+		let authorities_count = x as usize;
+		let epoch_length = y as u32;
+		let redundancy_factor = 2;
+
+		let unsorted_tickets_count = epoch_length * redundancy_factor;
+
+		let mut meta = TicketsMetadata { unsorted_tickets_count, tickets_count: [0, 0] };
+		let config = EpochConfiguration { redundancy_factor, attempts_number: 32 };
+
+		// Triggers ring verifier computation for `x` authorities
+		let mut raw_data = TICKETS_DATA;
+		let (authorities, _): (Vec<AuthorityId>, Vec<TicketEnvelope>) =
+			Decode::decode(&mut raw_data).expect("Failed to decode tickets buffer");
+		let next_authorities: Vec<_> = authorities[..authorities_count].to_vec();
+		let next_authorities = WeakBoundedVec::force_from(next_authorities, None);
+		NextAuthorities::<T>::set(next_authorities);
+
+		// Triggers JIT sorting tickets
+		(0..meta.unsorted_tickets_count)
+			.collect::<Vec<_>>()
+			.chunks(SEGMENT_MAX_SIZE as usize)
+			.enumerate()
+			.for_each(|(segment_id, chunk)| {
+				let segment = chunk
+					.iter()
+					.map(|i| {
+						let id_bytes = crate::hashing::blake2_128(&i.to_le_bytes());
+						TicketId::from_le_bytes(id_bytes)
+					})
+					.collect::<Vec<_>>();
+				UnsortedSegments::<T>::insert(
+					segment_id as u32,
+					BoundedVec::truncate_from(segment),
+				);
+			});
+
+		// Triggers some code related to config change (dummy values)
+		NextEpochConfig::<T>::set(Some(config));
+		PendingEpochConfigChange::<T>::set(Some(config));
+
+		// Triggers the cleanup of the "just elapsed" epoch tickets (i.e. the current one)
+		let epoch_tag = EpochIndex::<T>::get() & 1;
+		meta.tickets_count[epoch_tag as usize] = epoch_length;
+		(0..epoch_length).for_each(|i| {
+			let id_bytes = crate::hashing::blake2_128(&i.to_le_bytes());
+			let id = TicketId::from_le_bytes(id_bytes);
+			TicketsIds::<T>::insert((epoch_tag as u8, i), id);
+			let body = TicketBody {
+				attempt_idx: i,
+				erased_public: EphemeralPublic([i as u8; 32]),
+				revealed_public: EphemeralPublic([i as u8; 32]),
+			};
+			TicketsData::<T>::set(id, Some(body));
+		});
+
+		TicketsMeta::<T>::set(meta);
+
+		#[block]
+		{
+			Pallet::<T>::should_end_epoch(BlockNumberFor::<T>::from(3u32));
+			let next_authorities = Pallet::<T>::next_authorities();
+			// Using a different set of authorities triggers the recomputation of ring verifier.
+			Pallet::<T>::enact_epoch_change(Default::default(), next_authorities);
+		}
+	}
+
+	#[benchmark]
+	fn submit_tickets(x: Linear<1, 25>) {
+		let tickets_count = x as usize;
+
+		let mut raw_data = TICKETS_DATA;
+		let (authorities, tickets): (Vec<AuthorityId>, Vec<TicketEnvelope>) =
+			Decode::decode(&mut raw_data).expect("Failed to decode tickets buffer");
+
+		log::debug!(target: LOG_TARGET, "PreBuiltTickets: {} tickets, {} authorities", tickets.len(), authorities.len());
+
+		// Set `NextRandomness` to the same value used for pre-built tickets
+		// (see `make_tickets_data` test).
+		NextRandomness::<T>::set([0; 32]);
+
+		Pallet::<T>::update_ring_verifier(&authorities);
+
+		// Set next epoch config to accept all the tickets
+		let next_config = EpochConfiguration { attempts_number: 1, redundancy_factor: u32::MAX };
+		NextEpochConfig::<T>::set(Some(next_config));
+
+		// Use the authorities in the pre-build tickets
+		let authorities = WeakBoundedVec::force_from(authorities, None);
+		NextAuthorities::<T>::set(authorities);
+
+		let tickets = tickets[..tickets_count].to_vec();
+		let tickets = BoundedVec::truncate_from(tickets);
+
+		log::debug!(target: LOG_TARGET, "Submitting {} tickets", tickets_count);
+
+		#[extrinsic_call]
+		submit_tickets(RawOrigin::None, tickets);
+	}
+
+	#[benchmark]
+	fn plan_config_change() {
+		let config = EpochConfiguration { redundancy_factor: 1, attempts_number: 10 };
+
+		#[extrinsic_call]
+		plan_config_change(RawOrigin::Root, config);
+	}
+
+	// Construction of ring verifier
+	#[benchmark]
+	fn update_ring_verifier(x: Linear<1, 100>) {
+		let authorities_count = x as usize;
+
+		let mut raw_data = TICKETS_DATA;
+		let (authorities, _): (Vec<AuthorityId>, Vec<TicketEnvelope>) =
+			Decode::decode(&mut raw_data).expect("Failed to decode tickets buffer");
+		let authorities: Vec<_> = authorities[..authorities_count].to_vec();
+
+		#[block]
+		{
+			Pallet::<T>::update_ring_verifier(&authorities);
+		}
+	}
+
+	// Bare loading of ring context.
+	//
+	// It is interesting to see how this compares to 'update_ring_verifier', which
+	// also recomputes and stores the new verifier.
+	#[benchmark]
+	fn load_ring_context() {
+		#[block]
+		{
+			let _ring_ctx = RingContext::<T>::get().unwrap();
+		}
+	}
+
+	// Tickets segments sorting function benchmark.
+	#[benchmark]
+	fn sort_segments(x: Linear<1, 100>) {
+		let segments_count = x as u32;
+		let tickets_count = segments_count * SEGMENT_MAX_SIZE;
+
+		// Construct a bunch of dummy tickets
+		let tickets: Vec<_> = (0..tickets_count)
+			.map(|i| {
+				let body = TicketBody {
+					attempt_idx: i,
+					erased_public: EphemeralPublic([i as u8; 32]),
+					revealed_public: EphemeralPublic([i as u8; 32]),
+				};
+				let id_bytes = crate::hashing::blake2_128(&i.to_le_bytes());
+				let id = TicketId::from_le_bytes(id_bytes);
+				(id, body)
+			})
+			.collect();
+
+		for (chunk_id, chunk) in tickets.chunks(SEGMENT_MAX_SIZE as usize).enumerate() {
+			let segment: Vec<TicketId> = chunk
+				.iter()
+				.map(|(id, body)| {
+					TicketsData::<T>::set(id, Some(body.clone()));
+					*id
+				})
+				.collect();
+			let segment = BoundedVec::truncate_from(segment);
+			UnsortedSegments::<T>::insert(chunk_id as u32, segment);
+		}
+
+		// Update metadata
+		let mut meta = TicketsMeta::<T>::get();
+		meta.unsorted_tickets_count = tickets_count;
+		TicketsMeta::<T>::set(meta.clone());
+
+		log::debug!(target: LOG_TARGET, "Before sort: {:?}", meta);
+		#[block]
+		{
+			Pallet::<T>::sort_segments(u32::MAX, 0, &mut meta);
+		}
+		log::debug!(target: LOG_TARGET, "After sort: {:?}", meta);
+	}
+}
diff --git a/substrate/frame/sassafras/src/data/25_tickets_100_auths.bin b/substrate/frame/sassafras/src/data/25_tickets_100_auths.bin
new file mode 100644
index 0000000000000000000000000000000000000000..6e81f216455ae9dc61be31a9edef583a652721a8
GIT binary patch
literal 24728
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
KlxmB?4E!GvdmYaJ

literal 0
HcmV?d00001

diff --git a/substrate/frame/sassafras/src/data/benchmark-results.md b/substrate/frame/sassafras/src/data/benchmark-results.md
new file mode 100644
index 00000000000..8682f96cbe5
--- /dev/null
+++ b/substrate/frame/sassafras/src/data/benchmark-results.md
@@ -0,0 +1,99 @@
+# Benchmarks High Level Results
+
+- **Ring size**: the actual number of validators for an epoch
+- **Domain size**: a value which bounds the max size of the ring (max_ring_size = domain_size - 256)
+
+## Verify Submitted Tickets (extrinsic)
+
+`x` = Number of tickets
+
+### Domain=1024, Uncompressed (~ 13 ms + 11·x ms)
+
+    Time ~=    13400
+        + x    11390
+                  µs
+
+### Domain=1024, Compressed (~ 13 ms + 11·x ms)
+
+    Time ~=    13120
+        + x    11370
+                  µs
+
+### Domain=2048, Uncompressed (~ 26 ms + 11·x ms)
+
+    Time ~=    26210
+        + x    11440
+                  µs
+
+### Domain=2048, Compressed (~ 26 ms + 11·x ms)
+
+    Time ~=    26250
+        + x    11460
+                  µs
+
+### Conclusions
+
+- Verification doesn't depend on ring size as verification key is already constructed.
+- The call is fast as far as the max number of tickets which can be submitted in one shot
+  is appropriately bounded.
+- Currently, the bound is set equal epoch length, which iirc for Polkadot is 3600.
+  In this case if all the tickets are submitted in one shot timing is expected to be
+  ~39 seconds, which is not acceptable. TODO: find a sensible bound
+
+---
+
+## Recompute Ring Verifier Key (on epoch change)
+
+`x` = Ring size
+
+### Domain=1024, Uncompressed (~ 50 ms)
+
+    Time ~=    54070
+        + x    98.53
+                  µs
+
+### Domain=1024, Compressed (~ 700 ms)
+
+    Time ~=   733700
+        + x    90.49
+                  µs
+
+### Domain=2048, Uncompressed (~ 100 ms)
+
+    Time ~=    107700
+        + x    108.5
+                  µs
+
+### Domain=2048, Compressed (~ 1.5 s)
+
+    Time ~=   1462400
+        + x    65.14
+                  µs
+
+### Conclusions
+
+- Here we load the full ring context data to recompute verification key for the epoch
+- Ring size influence is marginal (e.g. for 1500 validators → ~98 ms to be added to the base time)
+- This step is performed at most once per epoch (if validator set changes).
+- Domain size for ring context influence the PoV size (see next paragraph)
+- Decompression heavily influence timings (1.5sec vs 100ms for same domain size)
+
+---
+
+## Ring Context Data Size
+
+### Domain=1024, Uncompressed
+
+    295412 bytes = ~ 300 KiB
+
+### Domain=1024, Compressed
+
+    147716 bytes = ~ 150 KiB
+
+### Domain=2048, Uncompressed
+
+    590324 bytes = ~ 590 KiB
+
+### Domain=2048, Compressed
+
+    295172 bytes = ~ 300 KiB
diff --git a/substrate/frame/sassafras/src/data/tickets-sort.md b/substrate/frame/sassafras/src/data/tickets-sort.md
new file mode 100644
index 00000000000..4d96a6825c8
--- /dev/null
+++ b/substrate/frame/sassafras/src/data/tickets-sort.md
@@ -0,0 +1,274 @@
+# Segments Incremental Sorting Strategy Empirical Results
+
+Parameters:
+- 128 segments
+- segment max length 128
+- 32767 random tickets ids
+- epoch length 3600 (== max tickets to keep)
+
+The table shows the comparison between the segments left in the unsorted segments buffer
+and the number of new tickets which are added from the last segment to the sorted tickets
+buffer (i.e. how many tickets we retain from the last processed segment)
+
+| Segments Left | Tickets Pushed |
+|-----|-----|
+| 255 | 128 |
+| 254 | 128 |
+| 253 | 128 |
+| 252 | 128 |
+| 251 | 128 |
+| 250 | 128 |
+| 249 | 128 |
+| 248 | 128 |
+| 247 | 128 |
+| 246 | 128 |
+| 245 | 128 |
+| 244 | 128 |
+| 243 | 128 |
+| 242 | 128 |
+| 241 | 128 |
+| 240 | 128 |
+| 239 | 128 |
+| 238 | 128 |
+| 237 | 128 |
+| 236 | 128 |
+| 235 | 128 |
+| 234 | 128 |
+| 233 | 128 |
+| 232 | 128 |
+| 231 | 128 |
+| 230 | 128 |
+| 229 | 128 |
+| 228 | 128 |
+| 227 | 128 |
+| 226 | 126 |
+| 225 | 117 |
+| 224 | 120 |
+| 223 | 110 |
+| 222 | 110 |
+| 221 | 102 |
+| 220 | 107 |
+| 219 | 96 |
+| 218 | 105 |
+| 217 | 92 |
+| 216 | 91 |
+| 215 | 85 |
+| 214 | 84 |
+| 213 | 88 |
+| 212 | 77 |
+| 211 | 86 |
+| 210 | 73 |
+| 209 | 73 |
+| 208 | 81 |
+| 207 | 83 |
+| 206 | 70 |
+| 205 | 84 |
+| 204 | 71 |
+| 203 | 63 |
+| 202 | 60 |
+| 201 | 53 |
+| 200 | 73 |
+| 199 | 55 |
+| 198 | 65 |
+| 197 | 62 |
+| 196 | 55 |
+| 195 | 63 |
+| 194 | 61 |
+| 193 | 48 |
+| 192 | 67 |
+| 191 | 61 |
+| 190 | 55 |
+| 189 | 49 |
+| 188 | 60 |
+| 187 | 49 |
+| 186 | 51 |
+| 185 | 53 |
+| 184 | 47 |
+| 183 | 51 |
+| 182 | 51 |
+| 181 | 53 |
+| 180 | 42 |
+| 179 | 43 |
+| 178 | 48 |
+| 177 | 46 |
+| 176 | 39 |
+| 175 | 54 |
+| 174 | 39 |
+| 173 | 44 |
+| 172 | 51 |
+| 171 | 49 |
+| 170 | 48 |
+| 169 | 48 |
+| 168 | 41 |
+| 167 | 39 |
+| 166 | 41 |
+| 165 | 40 |
+| 164 | 43 |
+| 163 | 53 |
+| 162 | 51 |
+| 161 | 36 |
+| 160 | 45 |
+| 159 | 40 |
+| 158 | 29 |
+| 157 | 37 |
+| 156 | 31 |
+| 155 | 38 |
+| 154 | 31 |
+| 153 | 38 |
+| 152 | 39 |
+| 151 | 30 |
+| 150 | 37 |
+| 149 | 42 |
+| 148 | 35 |
+| 147 | 33 |
+| 146 | 35 |
+| 145 | 37 |
+| 144 | 38 |
+| 143 | 31 |
+| 142 | 38 |
+| 141 | 38 |
+| 140 | 27 |
+| 139 | 31 |
+| 138 | 25 |
+| 137 | 31 |
+| 136 | 26 |
+| 135 | 30 |
+| 134 | 31 |
+| 133 | 37 |
+| 132 | 29 |
+| 131 | 24 |
+| 130 | 31 |
+| 129 | 34 |
+| 128 | 31 |
+| 127 | 28 |
+| 126 | 28 |
+| 125 | 19 |
+| 124 | 27 |
+| 123 | 29 |
+| 122 | 36 |
+| 121 | 32 |
+| 120 | 29 |
+| 119 | 28 |
+| 118 | 33 |
+| 117 | 18 |
+| 116 | 28 |
+| 115 | 27 |
+| 114 | 28 |
+| 113 | 21 |
+| 112 | 23 |
+| 111 | 19 |
+| 110 | 21 |
+| 109 | 20 |
+| 108 | 26 |
+| 107 | 23 |
+| 106 | 30 |
+| 105 | 31 |
+| 104 | 19 |
+| 103 | 25 |
+| 102 | 23 |
+| 101 | 29 |
+| 100 | 18 |
+| 99 | 19 |
+| 98 | 20 |
+| 97 | 21 |
+| 96 | 23 |
+| 95 | 20 |
+| 94 | 27 |
+| 93 | 20 |
+| 92 | 22 |
+| 91 | 23 |
+| 90 | 23 |
+| 89 | 20 |
+| 88 | 15 |
+| 87 | 17 |
+| 86 | 28 |
+| 85 | 25 |
+| 84 | 10 |
+| 83 | 20 |
+| 82 | 23 |
+| 81 | 28 |
+| 80 | 17 |
+| 79 | 23 |
+| 78 | 24 |
+| 77 | 22 |
+| 76 | 18 |
+| 75 | 25 |
+| 74 | 31 |
+| 73 | 27 |
+| 72 | 19 |
+| 71 | 13 |
+| 70 | 17 |
+| 69 | 24 |
+| 68 | 20 |
+| 67 | 12 |
+| 66 | 17 |
+| 65 | 16 |
+| 64 | 26 |
+| 63 | 24 |
+| 62 | 12 |
+| 61 | 19 |
+| 60 | 18 |
+| 59 | 20 |
+| 58 | 18 |
+| 57 | 12 |
+| 56 | 15 |
+| 55 | 17 |
+| 54 | 14 |
+| 53 | 25 |
+| 52 | 22 |
+| 51 | 15 |
+| 50 | 17 |
+| 49 | 15 |
+| 48 | 17 |
+| 47 | 18 |
+| 46 | 17 |
+| 45 | 23 |
+| 44 | 17 |
+| 43 | 13 |
+| 42 | 15 |
+| 41 | 18 |
+| 40 | 11 |
+| 39 | 19 |
+| 38 | 18 |
+| 37 | 12 |
+| 36 | 19 |
+| 35 | 18 |
+| 34 | 15 |
+| 33 | 12 |
+| 32 | 25 |
+| 31 | 20 |
+| 30 | 24 |
+| 29 | 20 |
+| 28 | 10 |
+| 27 | 15 |
+| 26 | 16 |
+| 25 | 15 |
+| 24 | 15 |
+| 23 | 13 |
+| 22 | 12 |
+| 21 | 14 |
+| 20 | 19 |
+| 19 | 17 |
+| 18 | 17 |
+| 17 | 18 |
+| 16 | 15 |
+| 15 | 13 |
+| 14 | 11 |
+| 13 | 16 |
+| 12 | 13 |
+| 11 | 18 |
+| 10 | 19 |
+| 9 | 10 |
+| 8 | 7 |
+| 7 | 15 |
+| 6 | 12 |
+| 5 | 12 |
+| 4 | 17 |
+| 3 | 14 |
+| 2 | 17 |
+| 1 | 9 |
+| 0 | 13
+
+# Graph of the same data
+
+![graph](tickets-sort.png)
diff --git a/substrate/frame/sassafras/src/data/tickets-sort.png b/substrate/frame/sassafras/src/data/tickets-sort.png
new file mode 100644
index 0000000000000000000000000000000000000000..b34ce3f37ba9d39aa649cc6d5a216373048c0064
GIT binary patch
literal 33919
zcmeFZbzGHA_cyu;>5>qnrMtUh)7?l)hcugRX%J9BkW@fgHX@}UT_Pea(xr5YgycIL
z?)!P}=bZPP^Z9)~=l$>5y7!*zx@OkQtXb>3VrCPst*L~IMUDl5KyX!*p}G(VvL*zA
zG=YHz?x=nrUj|=8cE&39Dk^Lc3~&KzBB4T%KuHrB6#ptufE&n2C=e8IKL)%BK^aj8
z@g_&Qb^ZMqlqvoyr-3rdUv&T(2rm*c1Q&egfHxl~6NB$^@OFy5>8k~l5igAEf6AbQ
zp#1l@l8S~lEgu&z4;Q}>XwSpPFUHF&#w$q6D=H?)BPPHLY9qb+_lOXbCO<WO092x!
z<mI(h<mG8Syxi@bT<jna|D2GglFFSjl--tk3d5>8;&CFjrJBBFbjfvC3Z(?j(S}s&
zTo;d+a8(i#3l~g0_H3#*7M9*rL{`vZgoeLw6k%;tj=xgF;<T0&+39TxN@?;7*XHIQ
z*hoOia?37*ntib~>drIzO&@m;%FDvv|04A@gIID^(8)<4>Eiv|!3$Bev7BzmOJz!%
z7$+ikf<c!lwAGL$0`|{r`=a}=RY(~^e>5)bQ>;GfM;^q<TpyG0j5Fq_tsd!l<LKNq
z+GS-h%4+$^|Bct&(wpZMZvwO~J7hF1@2`jr@G`uOV^k){f0aP^`R(m|1LkhIgrjAu
z^Fp{KU4|T-kWBQBb_CkwVdPc&u3P1~{+Jq8SHuo!?;kSsK;H<(0ne5csuEA+la~?f
z%|ENgA||y)9&&O9qoOuMByOe`Zy{xfw|z>zm;H|Yj}z7(iM>$8`LtUx?h!45RShdr
znl#}8KF>cQ;o(KZBimK^`TMyCxnJcu9UY^*t@CPih8AZ48T5D3H-;H&sEgUSyK-6C
zx?9_E`MY``77QXG?eAe_^Uw}PYi;M?<R(eC+tf-&>triQXC$b>qv0WM=jfyy=w+uD
zsHtxg_|QhwmQGp<OTu3a5OB4FS<(8ty103Z`AgDW(-i|{#BFXm+UqK?hmv&0K#t_y
zz3gZOxCFR(I2HVzeEI04uxKT`Z0*H#p^ATz09TT9jxd;q7&o_{pC6YWKbO0g12?az
zs3<oNA2%N#C#b>c9pDDD^5=B(rbm#tp#in?w()ZEfH}Fl(IRMCS-bndB<bkDbK1Yh
z=jx%M@h|dj-hXic@Ppgm%7dGii-+6QmHVG9ykQEyfXH71`ro$j)(`No<JPtFcK7kJ
zu~YE1bA!?UlZ36!zuJ5Fc)46p$JU11&c)6ZRP_eE^8QDc2#xz!3j_-soLoJwTLEVO
zM@^WM{lA&@A7ev2xt`8H2LhV^i|&8a{(J7%l|d~H4Kb*@jSpgYDo{x}ME_#8?lw-g
zV%J4JenEZ_QCmJv0b5%eP62CcdrlEPK|4-SJ7HT9YilcOYa9E2kWz8;hFQ7U*da&(
z;#^LE4!^Axzb%ijJ*PE~J)pzGXUi$V&u7IcWFu(9$1lQfW5a9p4-#5lPCzQHT>d#K
z1SwlU%8Fk|&`Q{jhf~m=j|YrK2oSXr5abjTwY9aj;pev!<+r{jWosj*=<ele1(wsv
z)ylz++r!P_`T>IBVzSyQl5~7rJpaC<?P3M92Mr|Y)ScXX{Qvzx-^tZZ4`zknCa*9L
z509__p8zi}FDMB8yON=ump70^1kG#gyncdM7BMgxz*sAUoB{&Z_rYAm<h|^yVD4V}
z?(Qy<bpJx#e?8U!>SSvLvw~W|>;Tb!;jsSSILyw&E5>s(d;~cScUvd>fd8-B2<4%b
z0QJF=D?53E{sXRW-Ds4ao#)Nf&83UewJOolUMq!|mCel*ysdogY_G=&Xx%)rakO%C
zumkAvm$?3Y-|2rR3WA~{HnxaW<FN+oXC-LEDavOj!YRbdD`;<LYb|ImBJiKoz1{6$
zepX&~vJQZcfLA~~uPqcU%k@IB{%37JM?1tS0ETh$@^bPD{$Cl!{nv!K5t#A!j3v1L
zZ#<E>uJ8|#0qt(?0q_D?$o(%c{EKG@*!e&B`fDxz5B2~^|F@I>5r6;7uK%*@f5d_R
zQRV+i*MHgdKjOgusPccM>;E@)Vf{OtvU3AokRJ$^Ue|=wf<Oz+T1^Rh9dbeb$Tx+7
z5~hdpeQyW^n+Wknf@I~8ft%<s6%7USRcw4hK2~Ezia7{`7NP=`)%Ty;nhki8vl_yD
zw0kfYqIGQ6M)@8I8C4<vwf~C}WzXg`$G+<3ch*iJE=AA0a>_60RmPc~pQo0eKOKni
z6nfa?m1d?nnZ9i@TI`}r>YhrBL5PY^zyMi#LGhwycjq|tSMT)bs3iXjY!pKNgNsrO
zt5g4`)qwe);8oGkl=rapcc{0)t5Vg9hyZ-^iXg83-#>3h5+N%6<8xSpII)*3Cl0)X
zi2iZ^pLPB><^ShyT+QoUm(it9kn%!~CUGUhU@+oAi31#|^F4W?6}IBy;=M2YgA2KS
zb01iHrTum&^8!}xntk+tnxp}R;%Gh;6cvqk$O}L#(abeDtS-{UjFv-MUbcCn>sEHG
z357z1GkTaDCq_pj-9t7_l^F;L<lf362H)<)sFX};MGrVJ{1r=6UfyVLt0rvX{Pb|b
zvUjtpb?*E$X#R}acXtjK;=i0EJhd|Q`!{U4A?(aAozmOQ4bQ#h50j#j(&8Dl*9Xd-
zmNRNf>CoWr<6pgD=e?io#rK+bLs|o-(P@IiM@zH{_1W%asxQEsc6ua_XKHqiE!duW
zEOlW%PD(1!Et65Dy;HMyvaI!n9W_ttg4!bFQ1E<i7ZgJO*qJqpMZm-&m8b(yWAUhv
zAtx;<VOP6XlF2mE$t8J#<$q=n(^pEFiz#_|`FzEdChVM7>hi?q%h#_RK2&Rs5nsP*
zo_}+{Fr40uv5H%L8AhGKZK)_h-HC!P5iY)SqC0H^rm^2c;r<{n5UbH`;nUMj|E=mO
z*@?py8>!PZ`W%S>!oxt^v!m_K_Q=>1OlCSdw352dKgIS>mM!H!nDvzD?Jf@GN0L!e
zy3D=QEXfX=Zw-@HA4P?HJf8b?5TDRZcezm&iqY%&)_q>8YZ=q5CHSuN#ky9$6S-T{
zcKxQ*V7)8M&rjH0Jm`o6G36g#qBEYoo@UK~xK+(Z<V*Wx$y7}t1H0y`@vRrrS8>5D
z;d65qTXW46S;d{l7xlaDl5-))RKJgp7r$2*iiDgBT<+6c-EKW=YE3lbYFM!e{?*5P
zGC3HE*8gs3em=$c3o<Q1Vt2a4QO%%Kq}AHVuZ-5m*rZ>VSEa8+1B3hor`cZ)E>AkV
zjJ8@6@!j&(uOJCNSyaN1kGH)1X^95G!N*_jCbfhzrS|0dh2G`o9~B9^q?BHSLc11f
z4cB4he)A#tOw7!l8)HFhyEJXHe)Bb3Ht1+*Al@9p5$q3G%}sslimoMjs9zIXW>~Sf
z-7w!1fr1V{llq!>K?a$kJ~*0Y9-5s^+6}up(>5T{RXyK}mF{ZV&FiSPX{h?soY8Oh
z<KPyhR5026<xzuh(X5!*JO70Ubn<P!IO&T~>28GFG3WVP5A>4Vy^911K5m*<Ffy7*
zNHH+DHGi=ah6NGv-=n=;alb^9mzN({`2AIUR|X>j^k+$z+6RGH1Z@j`yt;If`1OfC
zuG`xG>SA<Y`L!-9q)qy~GX)_81sdI6sGB@uVln;_b3z=XDZTq$L|V1DcL!=*DQH4|
z-vdix4)a~{SZI@lgfEM*5GRNPpAeW$C^TEdx=sUWfl5g?z>kSjdadr?QB;iCU&(fV
z0JLA0U}orea$;iQJV+>8K}iX{xTIvl=P?jtNLxi!%cB(OOR^%}^~RLh8V9ZEO(dB*
z6W2K*LBaUT%TThIh`{D#@V~tsDJmZ-_0nC!p1jHLSWun_vNhfCl9coIZC5r;JWKRx
zc*Dg5i#|2(=BNydz_LGw!mWQ`-p?(HbdezEOQg(uiIzFWzsv>?(s8Z(^JFwS{aOyV
z_r967ww39Xc>MYy&ZB|Sn=&7vL=!?>UJgGyGdrJ3x63!IsN5twp7F--;AzX`ci5|W
zd4U5Nnx2mAr3s_FxIl4D5ybKCJ1|v~)n_A7*U(tjy8NM)j#~oJu9}!awuG5QB8JE-
zQkhWzrFv-PZzIW@P9k5XACu-au6qjw<zj7nTcO&xqdSq5S4=D+FE7uY04bba32*tU
zc<V=NkNf>WyX))SRTooL?FSM7UKYP(@*_0jSs%kr^YNSp=HqxMAte2xWwo<^NvQSe
z65ZqK0{?-V+Z`YWA_2b`@`83m&o6hcGB&Yb>2~b`8o?KvT3jEh8X6h`+$eijn6u*I
z+}w)4W6u8w<{myd>B6OQc<=xMK)e%Q+W4?qDTTUoqUA^=FK~m^I6b;yKD6mB4`Dni
zK*^q-o~Hx$)qcBk^;bftb3W$1mltRId!6`55Ul9)n3h57>Y?+qFJEx5qOHBH-<OvI
z9(#LZ%O@DY&F=5}e=uW4;4BW_PK2v?>BQz{pmbzpkTwf30c!iR`YinX+}vC;nwO5^
zN2;BX0QxDa=>u~(`a2dDXt^v~9=D$Nw?d7KNF@e4_m_LC!@>mci|hCrTqp8boDb?z
z(2wXdHoHQOXJfHRxfXtAShja3W(<vwNAuw5R2uG8x^qhNP77$vgk4?uUmQ+&0`!)+
zJXwZ>_fiMp3`!nTUTzIu;sA)2<I9X%79k@eYpb^D{dPX5Rzzswzj*8QsMA^_-k>*F
zwkaG+3Qr(HKsBSS$N+XoL_u~MH{mN6LB~6t(b%MW!eIv!$_xd^u4H10ii&$cGZq$O
zY1%2=KSpF`-tAiEg&+evXJcdY>E}CTrc}U*<9V98Gw;C*$)oM*6gXV-Boxxgvj=#K
z5;r%$x5smNT6IN^j!zA(GwB{1BmN7lZqJ|e==mRx8-Y>9^!2F#{gDj1lYZRrP1z*a
zl<u|?*F=1BGG6d*P-}F<Tp%_*eIXe&bqs+FUnaEK!qT_w;vE0*cwRaQU-}$k`RB9I
zlL33rg*K$%Rq^@uzEoz<Kff47lCd@PG9-IccV|5*0h9u%r^7&Y%u03<G9mH%y*7+n
zH*VB=Y5?Ho>_uj#tXOUbLKjV0qXB>+%P!aeOvrqkSn@KId{F9)<MU6xn3xzKH7RZN
zuIlQKJ3Y-X?h&N)dQ7`2?R}z8fuCKx)^!TLI-7T|J>!Xqfdg2(yu4ho!K0RlLueuJ
z6gtj!U-s<GhWMW>#_r8{o84D}LbH|38e#X}-Ni6w37<4EOO1<jWRB~$DV;EUsoLR%
z3gij;uxkPFb@N&KL8LbxI1;`RUp?u-YswY};coz+<mNMbCFOEW`hl(OYghUEhze-H
za3V}61wAS^GALPlAg--Frp&gy5GAw{15ZtaPX@F<a)AL+`(IV`x32s6i^N}qxKWih
zZd{q|;dl+1fA$?$fq3FrS(|&YU1cG5j_z)8Uo1=bN}zg8sQSyiQP3ZS6x06EsP|uk
zjDN?huos417~>zU5PqBjQzFZ8gI{eeUYH&=pXGPawD?mOdJ7`DF3@PpZ*-N=FJVPq
zf00~6#Ava8-DzzWbIPH$Exad&7BS|6amp=CIUUqrf*DD&2zJ42d^^e;BWG1U@(c7<
z<RZMUctdX`OEl@T6LN}2i0k#9tte{dqJ7SoXQUF>9%m(sm2)`689vx^L-aYB2`p=t
z*474iY`h|_Gn!=O{1okd`*#t(=#4`P2HukUwzmqdSTeb+O?5-&%ZLFaeEXIQ&NYiE
zsxO*yILvP`JZQRRDHtuudIAJ>A^LjVa5RM<Fuy5BfxJCoylFm=pT|)A8SiG4PRqWk
zDXC@bd!qV4Vt^b}P<-m)G4-4zxZwjiQoAA;ZKLdzVNj<mL7OL)GHX5?8_sBy7fPqA
zxLv8H9tr9_hdAI=Z@*a3XCdA@eOP5Ze;ER7{BzY?=_H*-EncL9KiE#6k!xDiBSAVx
z8Mn4IYjnUA>%Be5si`}JUtTh)kGjkqO+BpI=YY#PFd<Fj^BQ^YGrQv~h6SJczE)4d
zm4`xW@l2hQ_24x`gVI;XOjR0mz@zE-gYDtMRc)CFe;uzoD?fR(7<-RP%V?4XjDEAW
zsF4y49qp-Rkv7X6ow9EPY7BdI%z5wgZLse7YTj0d5Jig>l%wYCg37fPrscf|$K{I>
z&EixSs_weL>DlT2(qP!vn)iyVYMo{IqROm`x2}1*SyvR8!)0-f)B1Zz9)da-vdW<U
zQz+_J4|1F4hU+uJ_X6ff4B9>+78f_e2+UC*?f5-Ltgp~+wA+lI!DZf2S78)zXX4Wc
z30W3$K1T8wSQ^*1^bOC(?fPk+jF(As@5`4eESpL9{U1AcCcz9M^2@%b*6`?6Ptsn~
z{K*!(`YzYiaz6BoN*MMrS3n;f({@=5W+3zK8PK@lM%PL)P5MqUs=N0E#u)a8<WpQY
zl(t?ivLVsl9UZ#O|G*im9B&n&o35J+Y5hO<7SU-Ij9L!2DW>f0O2kG|J5qZC_kVp*
zQO~%4l>ij4xqeLBoGr5>RqL`wLvlrvygcZaF?W?e&>@PqKaXbOdY2_03bPQ6>Kr2r
zfv9X)jMe2)ZVA7YM=~PdiX1(~{QCSF6qp<HF3eTF=$j#`As%dfr)Ku81!Z25;VA|j
z5^upHv74)Mn1SrT&(C7lg6^~M1A#9H`S$68J^pI&w~VqK-88P0mfjkJ+zU7a9RgA&
zur8d(KQ~=4u3Ohlmq}{MK|()SAg~6F*~CDsc-eW_UjAD7Kz9fgY|Q=mUR_o5HmCx+
zG4Ns35DCTL#+W1nJO`0kop)0XrF2{7J{|nI$OHl^+vDL6-MNyjXFFfEN<PjQ)Tl;;
zVq`gMfk!`<PNYK**!?$7xb&%B;>z7<mr52nrSyZFPKV3i+g+>lXqI;;-u#v?p!A`<
zR|^>oCX%R9QI90*+4v0>I2tsK-bwH|x&hPSskfasy?-m_cY5GhpCF@pzEH{TLU28`
z#ump%=XC{1i<4~Gk#TWrVzpE>cTp#nR|&;pR?9KuJX!#QhOxdcL$%Y^_&!aL0skk?
zu%H@~1<wA0wdqC02L!Yfki6}U2V%mOoRY9DFS9VBehZ@Q8O-&b{+c<4lkX$g`3?Ld
zpbuRiU9G<4TJF}Ws*bpxJzWkZbzQ`AY-tb@L-=F@3dAFlAa76#fwuuU%g|4ZIAi8C
zJ`+k><;~ATY!HJ%#v8wFt0wW8@v*&07MJ*gFiONtFkLTClyI5PBTwoDeo#nihLSCf
z2w+<i-o8CxlR_l85}@+hMvO2$CV0Ga{8>_Ife8BM^#Dp)Nk)+%lB`6IiK;jtss@WU
zrf7r;i9nfEfiY{CWpNd*oIt|+M@fVbd4ouzfz7yef&`fwzDU%B`rddD_kbp~Io$ZL
z1$aA0IExHP`CM=e;BEmLyqq4=wtF#R0g+)UrcY~g8IMRqpzS-}9Dty;HPyZGaJ4u`
zbMt}2zvQOYSlx)0k;lOIfioK|>MBCE1zkSmH<qol{#adE5W#!*K@;ZZ)BcVluTR6;
z5eovKEV7IpFfH^`Cc^f%-P!bP^k1g~Uc%7CL@WwAKJ9b$g`Y+iOZR7>R%FNor5}*c
z#H_WWWV(;O^MD55ttB;|tCC#1ZqFa52YKQ+jbbXuvM|cA=09YkU1d*1Fg+bh6C@0J
zq|eusl#6tSe5il})<Dym>)=UbXD#*`wLA&=g|5eoWT5?X>Ez{!y-O!w-7lF=Pfwqy
zGkG4*`Yk6fTY@3L2%oe)5JE$WC?|TW0{CBV;z3S9)7cLK5o3v%ygX_U7<@b#84S21
z{IpDv3lr%@;P2z`RD=jaT&nXM*UOwFj=tOQ;Lx-2F))VyN`1M<;YO#X2g2^$MP~)T
zbWVz#<QZR7?&jZw5`&TD&TqHHcHT$I`v`MF*V(Wi0C{j7Ur38%-upe;iVS&6MUfr=
zcDc<s>OZCx6}ki|XNu9bi6J<NYYO*z?{e;tWq7b#`=L>fiMEi$_mQhQ6gpOm!1Q2W
zBqJ_MMY^YsO&7pDUCjK5iR42qzJi$51COZ&KN_wa=@fLDK#YV1p&;S@_8nAbH~mKo
zGJb@5cG*cl$)h-h^c~31TJ{G@?vZ6lSyEO2eL;SEOZblgNlbBeCB551^5vHf-0S;O
zS?%#|EwOmMf>)t$#UmCc$tj}a;xJ4-3=p{RucVW!U1PaN)inw-n<Pr+dVVo9zX4CM
zjEHKk-)_-C`s5HZ1Y+BGkqcm(@~;||nAZ#ffJpB78muM2tZz*ArJp7~yt}zF9G}14
z+1Fq5K=c~+=uEmhW1IF5YEoGysVM+{08hlwYvKXA+w}8}mn{LA2oDID!HPc)4Z9tm
z%%~!M_=C}cbXeu5F=EGZnd+~LV5q>+Gtv1n^r`|2OArnRJV0YEAi-3|!0<o}0zuPi
zolXB1i%02Smv=@DN)y`5p3D1OBW117HHSc}@$F4@j&;u^Ht`#(J(qA1dp7L$^~ho5
z%WbW}c3e3jyz2pQGC|KKLdm??@7&bn$l&1_<<l3oC{+ksti73?^TcxjUL(&e%M4c<
zSy~-$l)u{6P36Ydg@o7@e^$JAJ=o2@9^A)Y6e7x&PB4)Ctpp5@mr7Dg8>J|K*plVc
zGq<xpXl`e<df*R7lTIsbLVBIHckXB~i%p}sFj`?J1Y{Dh1j$howbk(5)B4|Q50Ug|
z!@axW;~b|)zoXVt7sFpUpPl@6#%#Vz6p>acD)p$xW7f9Mp8jUxAvpHS{>w`^N4`u1
z`~on>B*jCIBVnMmN4@!CCK*a8FnTvfL~|<M=76BdmDj*F0-igk3J3;oD+>!Kykd8<
zwlqRs{v&qIlSSK-p5Ms7tYBu(ih`(Dkh{<>Z+?;#gtoWP4qtKg5jHy@@9yU}_;}JM
zD8vBtNfZF}CKE*BZL4?R_QRJy89be`B!Pg{9s;xArmSwXh$ic8kUL$vUi;m$g&%nu
zUx>Ko8hjm1zdx_$(eunE={(@fh73}0k9h_<;-z*wUXH5whx*+J>9T%qPkEyqlbCr<
zIqef{Np&j>Wla6|lGxXVnCg)@eC9216CST_mWoN?Xhe|K;<rab=01v`K*+L|KQdj<
z78$bD*Uj7HNahCLD6kFsR67=&RneI97kqnbq`OdVsBi@}m9Z+DV7K?-Gro4YJ9lHh
za%Buqmtqnsx_@F@h6)(UXnmF&86${s;l5@MmDYpCtyfj_G4J!O>+S4jp>sVb=w6?C
zd>(%p|E_;6pyHx@NY2Y_eB-6)<vacZXN(MwXk|oN)9~70lW4D_en79~t?>_okcAZQ
z53S9C=5S&pJw1?8FBft%egCHCm*Cc}ro0Yu+@EPi0@YV5Xf84!=F+X7NoQxClZvpe
z#*m@IkednG4Ov^?u{b!89E^5)A#otw-kwLyVXn}!Q&2_!&DWC4YH7Q_M#qg1!Y&Jp
zS>&jO@6pzMkbPW^INL=hIHYdg>o_<$Y6$h&Mz@L`El!BpK&|cZ&xNy<3I{&3XT#$#
zkA`#omJw_)w*paH0jHxH?szm`6r9^uQsK61r`+#9dL&@r1QX+%t7GX;1oS+g)Wf25
zg&LfIqD-;X>!*wkPbV*>YDu-9e8*H)W|3<LX_*GN4Oz_aFOnunp?)u_0FvHVIE!|)
zjF43uw%m7%p*P>ZuG=H{2;;|$0IAQ?4Y$&)80^(S`53R<*ayZ_E(vt6*mRnTIdF+?
zQGCZvbERo4m(jZ=qNoamPA@sk83xw8G=CpXyXUz+qVO?fiS%S7qa}*pL2Bv#IdYj?
z+n52vmm+zeKUvu3L_@?8f99GWDxj<Ab>^8#u&IH_67=jH#TR8oHspiS2&d1Bt3h8+
zr?o0&w^aS`9uH{rcWQkF86yzf1mz<@bG=wJc#TIhM+C{hz;k-o<|I-23L6H4?Qw**
zpYrp*XhcS$b#i)d@&+Z2=}~sp6k$188Zev>r**x7_utjf+N>S8fXKD`qq!pmi7$)H
zd|xn<c|(>`%!Wr;90PwnSuY4=AAtEbhUMKaLRVa-)_VCnRy-8$L2ou=!4zJ=Uv0dl
zL;||W$?*u9tt!Lu_*35s4e+K;BBHhxZt)b^@&aEMC^r>c*tKBg6yr5?;Bff4%j8_K
z9i8;*Z(uAUNAGmf%n4_ZM#bIbw8MH0oy7$R{j9kD?FH6N=dkFk*h`n%Nq420aGIzW
zEZ^fO*aAoI{4z*Mx=~#3!{SQEQn*f`Q;VK^i?9FY4^K@*v=5LP0O8y8$#Wwk65~d!
z3H(XrYehkY7)&eq_5aw5Fw@U^MPF>=#}xCSyi@VI&D4iy#c0A$UQ{;5WpCSc2t;R4
zTmz>dqdG}4jrBWyr5WvvFm0PbO9hORiODLsi}w<6f`K)wXMWIxba^yJwlkIL+El`A
zQ*Y*v%xm7QZny4XUcIx3Z`l|goU{ng+Kp2@xSxb;bIPS@<j!SmNs1MGV@byE-L{l`
zqF3PN$C95XR&lzs?qdNXlcm=~o;Ct-p!M0r>ugC@^d71lYZXP_Bbj%+@o{VE9r<FX
zGja7ps9m@udW_kK$P`$^L`I5=t-z5xe;dq^y3aGk0W}60Neb6Cd6X098*qFYAIV*u
zvacG`f@BO?j$?1ud}y~e2?mB^!lOn~v*Ufi35P;}@tn~W8*AR9sX$lxs!O|M1VqnI
zn^m8U(*bY(PEBD@4r&-rSPQE;iT`hpW4SYVTC-v=|MB?1EYynI2n-<F)^}3tt(un{
zIyadhM$I?Vo$ou613Bwh7l9k#42Qo)n#T5MPA)3rV~#~g2GMN@t(`M)Iv1V*=Nn<p
zIh>q4E(m^!^~DE(2oRX;Z*ktofS>L0rB+}K6DS_+OSRS{b!3oa`yj%U_IQl8V9_``
zv>)WcKOfEn0#^n!f<T&Cu?>1HN0w`KNK4pHhGU6bjIYIXyKhWie|B@*sG;S)CJ;M4
z`2b)dan-IFc451W#-w)i?zj*4O$D|sMn*LMb=UQU>gt`p3*<5cg=K``HZXWS+{VD1
zMi#>#39)wv1Ve~eSRjqGGS52_f+=r)TusRhCHq`j%5)>&WE31rKVqNVco{IY3s|v6
z4ZF?a2(0L$M?o*!Ldp-f%JPN90k7Jds~dr*a!LrVc^|6`K}Jp>*prV>=~X*%DgW3*
zull3n?>e|XOd5|lvrsL{&xQ<;%nh_1iQCWpKfd3nR`u2Y_TZgMjn(z&ePd|T$Ud2<
zDe6{zF*FL!qfxX8o+-a(l%uI?7I_rGz#QEx-fk7uFB!zGd_=d3>u}mx6WS<a5!tWy
zc;Ck>)EwL9nlH^b1+Of8P*4+(ou2~dJGnvuL{JF*E+#V5KaN&=K^_HB^{LwK0{I0!
zBOaYnd5nl@-=e9&XLB@t%VB@|J`|DGLi82d>{XC&7Q^1E)XmC5w`HS1+6D>YYynY#
zG&^C}H@rp9T}FSwHBIK6c`RV+<M~P))0{*t!TH+389u_$Y<sqCaK{ggdbj^Ufwm=v
zQC(@QbMc7SOBFdIR1WiQ0i$o%oOe}f4G!W~JgBQ5Bi0~5$1)%-ryI)T8A`49@DwXw
z)%Po$D>;r?nL^1R&?af$05%+Cqozk&;LtP0*2I<I!LZp>K2JL_@~kGqcl&FLZf3^%
zWO@b=dBE&kexPND>fMWZ#7t_w{9DS<#ojTJ$19uKs1F>mP>0M^NLAzMn{<_XVL6#`
z2{zwm5VK2%*P`>@;E*QTJI|cfJI^UFItYsMk@&JuxrmW842H6+Jv`YqJhMJcSUF?8
z^F(>62_3ANg*d6j=CfkR^(is}<SQBI4n%E8cwJ`oj`)!9`KHtntSIC0_qsV+)S-#n
zN+V<GIlXyl(dGRHH@@vOK(#i9R7O-?Ipin0a&p%24LK5IPxB<ESgq$e<xNAxx8xwY
zF;DnhR@erQhckF}-qac4w@R4z826V;02@tpJLkMjsm@To$V9f;LWxC`p@wSftMQ)P
zgV3mfF@594n8^g}$u%9_sBi*g1L1HuD~Jt+tgPP<Jf|jrz%(M^Wom&HU$qSOYHB9F
z4?&?Y`P$0g_N%`FKK}aLFY-1_THiq7gDx8F-kv=Efx4S#t_n&rX&b&tKL&Kaq)G>e
z-^uw2;dW7!!czF(X92KH1QoY@iz*p!@hI7E4(a0!5Z)ff!_w^Rls|c1@-oILYk)fX
z<@rgkMB=pa<i6FqvngTLmB|EDAlS(BDUUu&qSug1cXyr{B}CgyH(AEIyz99y7?}+s
z@#NsZoENqjVAkwQ^5>iq#9;Ua2ExG-I|hE6Ybu!_&%l%@L|8n9BVNg&af{ZY@&~P(
zqxN&hHM}QkF)L97j^@U_3V>McU4*xi*Dk4CGRkr%@vSRq8I0UA?3YSjw<|3X|H7%2
z&DW{mf~3n<VOL~`wIXrp24~PWA10jOK*1=|DA&Zz##A(rL-{_R34xepmT|4;1L-lT
z;v8QV5R!^XR!3jjNBx>78F@S>9__vQb)Ulx#lG-JchmN&-9=#Qjyo!oh7LqopHh0m
z)g(U$>i4<p=ep{&D_(=uJ-UKi;v9Q>dkCatE`(G|`a+=Pu+klzUe0mw6n{D(o!(WD
zMncj}*+21M<MRkL177d7zCV&j$6xG~4n+#@GIei`=6CoiV;~5oV@*rZIpk7#H*>{%
z=n`%l^yHb0k~(k)IYceO)$~Yy5GtyD8u2Wvt}^dFyhkB<f2I?}VZQ1fs!XZ=8x-?h
zu1YC%nd<kb6PNp{ypu<LhhHWw)*a}rsgrNZmI+E6E8b&s<HGuqNS#w(yP|ErX)b6;
z47N}~F0k<-@jZ_RS4epuTvcmPb7!e^E{fX~<B=fWCo<c*`_ezCP2A1v@4^uRi0m4Q
zx0vG8Xz-e|nG@Y9Nk!eH-<GM;v->M)xB3h6b8W84F=49F+p4cNPFbcyYbe|O^QPg~
z7S0u=(mQRXEYUuk!laO_nB2UPS3a6X6}Xm1-__M4(+CZiA9MnY0>QDgq|?JbS5`Im
zv%nTUe?PYJZ2{?8zdA>HqR|XVH~nK3Iw!Atz^_8sj(k=uVg-)r1A$#_Ei8gz0;;z|
z%eDTcUtL^&Z{E7sp)DszYhTJDo<j*jArNZgu9b(=-T7={zl63k{nGT;d$1*7OD?H{
zDIZA_1^ll~_5Bz5a;j}^tbtBW8+<|E!J1VrXPoDr4|rEKnkMG6x)Qag>}oE1XuZ40
zK{|=7qB1!&X4R8tVRTNW*>+%NTmk_p6X0tHHFKjKbh@<Gh>T~fD5JFgOR+b7O&z0r
zFnUw!3yw(uTZA%PR&J-|jWVic?2{4!*M5oR<HI9VycI&Syh^SQ7Z3c9jS_WAZ_CsK
zp4c~v7_?uV^6z9QBcSuQXyQ7XfCjD8Mz+_-KQ9FCdwIMP??VG|78>)EeY=Xz1Lw_V
zj-T&})QMuSdhsE*6-wu7Z%tz_2pcYlhQ@s;Z`!);z=l`M{*`nDkKSAG%ea{Z*h9e4
zY#{%N*E$aJbjjPARhyz0RMkBv7H5BEp2y#_^eKdw^q3deQSL3r(;;GcPNL3m2d$K4
zB^vcN_ue68OCq0~D-oUefBcjPQj6{Jj&@gvsY(U3wRwr}dGAZO)mCLq1F3=z_tZU0
zlApz7;KX&Sfg$Y`;`c0~ExzmhGGV4q+4|@EW~S?JRgH9E_ecKY`}<4G6WRyHhrnlP
zk6&ZD&)3BaF~BfmpL)WFWhhH~kCDgXO`-nrJ)|#O<nWqiNGfBHM(Wt=rktyNRqA&1
zUPoyfNYMfh$(3vJS=tnr8&bbEuetAAX42TS@W*HqJbb2+`B-G-?`-o%Et~lteT|C9
zTBgS_3UFfeB)YTN?kR7U1>c(?ye0u}@456z#JsIRa4e5$uJ?pztmh6#TTvNS<LDg@
zb0>;q??(g<Jqc@?_c5|%A2K2n$k-7D#X9Guh@<nLc)V##RYR;^?c|SwD@zPb)qa1|
zj<JPXgb``LM|r$E!w=EZ<1zImP>yd6bU?j=^WG0O>M<E*x6Feh6PA9*Zg*;2THw~i
z>%<oRMq=QHa=u}7>bc|WG%jj*ZBc7AbwpHqe=9U3tVHe4W*Q$nO)V0GL`ddhu9ZKM
zn~mIz6$z?~6G!JenXr_8e36U!Jv3Y!zFsu_J~R$r5t}Tx!tF$yPmqLFt=A53;)ZL*
ze+>MyMnXl|={D?;A!<e-5p>SerS;XUbH9a{7%Hh7N<fzuzNr?x?Z|!P$2gSDYm_?=
zll0*R0f~Q@?S;H6{U66oG6ksVkUvroSViDv3f-^G1!>}8lLl7c#9la#9JcQeLmtpl
z$mj1Y{({||RUvZnMme)cJo=!mi%fuQB>X-$moBZx>K1#_&8VcR)sfU8YZ}>aVRZq6
z=gTXrJU%7r`cyQjKabcJsd!P6X=u&tl80@F-EgirC9a&SljqW+)$}%L?6X<Lm9w1r
zpYVzzNX^Pgy96M)Bqkc)c1>+F-TvD3iWU3#(68&s+uD^qi#OFHJ6rykma`eIQeVf2
zPZl?d(8_myRjh+-1d2K4$CLFRjV&3C6fzUU824VWAt%%PLM6Hu6>52dSFFe%qN)Wb
zxerdj+Jd76!1c49?cFnTRe5sfq4GDmxSlZV26Il)?-S44E_Z%3u8JzKBNguUj_9Yk
z-f3q*q=*_Z)B8m&?{2m2O8dT@-2B~qcD$f=k-2j5Au6JLQy%g?Tdh{Ip>u>s!qogw
zA)0{Y8!7TvvvFCY&t3GfO}u)Z9!tNrvRY%G22<s#gbcJN-NXh|FAP7ERR+x;+F5Ix
zJ`}$@9ObD1=_%kewJxr=Z4B7F^kkfledK5i|Hfx)<b(WL9rvzUr?hkn_SJqfuX=i}
z;ErU&-W&r%$#r-KbW1)lrvAu>N$B^vSw}dWZ7q%>kxo6xCBRQ4g{FmnB+rvPzw)p;
zm60MO>8<blC8ch0jCAlm^5v}7piRO1U#d;}!MHN|32j4ZjAv`twvu$x2xRRjJ{gN$
z4(KUOGeuMZ-;6eW-E%6J*VE44%5KI%?`Ns5KQ`;|J9G;MjNxEzR=v)!vqAUHUCH<O
zcD<3!s#U2pG0LWa^r@Jr_cs3;>btZe_mdBv@{vUqLm8MBl@%Li+|b#>eXW-oN=U3v
zfQzRGrI$eBHz4Jecf$4rG>6SuqX_pNZB{+Qrl@KdhJZ*J1d+n#z#*Us-X3~)JfIcp
z6-iK-WL0|{a{viXSeDfZm;B)Gefca$OPgZh&%JXQIx2VE2*j2zVojsZPt7mZ%!<=w
zEtTGg3XpzYYZX7`e*qWyG||jr+^6{78K$A}oXWTnS;PEvcxvKq@3?hH_c2{H!AHVk
ztX>-`kgWm{JZH}l^83(mZhTv9@8B<nIf_H^M_t*wj#!<WRZ-LP&)cBw7WV`c!oH9m
zY{jj=y$=C#Z2P)lJD+InGqaht@g6pjUi`&dgpnUa0{M9Xq?U@w7GCO6lJo+LJW7I|
z4tI0Rd$x=Y!c3~9e6h)2%-6~{JA$Jj+pP1F6$k84JQWb6oPFM9+yc@GNtHTxq!jB(
zWemFB<x4m@yB_8l(*N8rZMpUVhB+BjWh@BjFc=79?nGzR$9`H_R`mgiqFR#49FXVb
zF*<~wsV0+l9CogJ<Gq+9OE@e|9xa(r-3q$lnonjHmI8FVqZi9(YyB|_1c)0?UKPp{
z&Wg1HW4+^ZyW=k?;UVTK>cntqQL}<(>alZ)eS)@%)#AOYizO^AkTZ!S8m?5)D-2mP
zlkWO%hRZ8|3u4@e)}$i_K_>guqvMJENinzh1)M8L0!hZ8;E!%UW)YMHso6)tBCU8K
zmXM^^q3Sv>LmFSx>2#<Dg}h)H^Bm0MfV^V1vo3ZK7DB(n<HNWSfUI2~vbFOz{pu|%
za&a*Tv_dJKmnQ4QrIpovyzCG(@j;Ht`F#!9F!lrrHhoBRV`66c=og-Hx>sdbY+py>
z=X``^xLVG&bLmiF8nTl|6KOxim&9BqVxyuqW4Al~q0ryaL*G%M>;%Z6v^qY>Tdpp2
z60Jz?lgUhho8^!GaQ>X3g?5a={aDr#9N}Z8E^oAZ=Vm$-yH)fei9W3mJQz=kL7CK1
zh5qigN+B9fV8HUhFD{mOm`0^df~05Q;Z&X=`GKsFkZHqB;HuR6Y)!+IozgY>alIMS
zv*DSNEQ(h>qSs!PrH2FZu+Mrq@;y`wDdG62vi6bZ^=U-PnC1rAq@zFAv8;Oz6D`XV
zWM;H^?|Y>;hY489ixqPx=we5xA+wB`<&j;>vZ<g5QmO)atJmtzinFM>!Gm?S;?J6<
zq)%yL5+7-aYhW<o;HDP+Y%sr@RkoDZJh?J)LB*R!-Wg%VV&#aXi90yEcTD2(2w*x`
zWBbIIuc$jy(-BQ*H4`CUK3bY(fmriLj75d^F*q*4`y!G119zItEVjql&Tm{J!ejY(
znvVf?lkwY#HK~oENrLt#Qn0mn_Hv!~pB>fD!r4KxDjM=6S%IkC&w(Q+m!)pvfr+A}
zlFQ@}pPuKdhA)NA7Ws0omQIvkko)>|c?1*(%Fm}R$C3?Bcm^^I5~*L?{SVZVGtI$2
z?&w66?4(Wln)-b<`GeBHQZZQ7lg%>rl&_AloBtL=v6|3Gddyr}yqex~iqp`Kv`_+7
z{hV;-jB5|*fcHs~EC$l>TAe;Q#aR!oPxtEfPF!7dx^&<n-5f=q&Bq5ev=p)Mdgh8b
z7!Lg(F%TSrot-kQjkzBAl;zmH4gv#2_5pZAA@FugWEFPu@){TkGICbquf5&&9fn^?
z&piwOh$*f(`~RFF1}SH+K07Q2Ea23~tz&VRM?kcXJCe|uxx=S1v_Oy+$`P&tLs@w!
z1h)AhgidOf`}<V9RI!22{C}PkzkVE8YR|^?NP9_mhuZi^G*y8DJWMY}b|fBpyh#z*
zO4Kbz8!k7n+((Z9#?xR}7YqX;OMsg_uxpS}Z+NAAXvY!E^GvB@%wtss4WIhc;;!_^
zgqANOe{{ex5B<LO2V7{t^q_3+^u%sFqTO)9np_5h;(s*yV~pX~A5oFyNEe~luV&2P
z1mGY;2;57BC_JiI!8?msK_F(skmf?_duqp^bSCn`g7k-nAo)dfHEx5R5#lm4r0waf
z{6Y*SXwtVDO;M%d5l|nK{31Cx$s71ELe^>nH=De%u{=ZbDUs;-bh(;~<3^T?_g@d1
zJIp|6r#9HOw-e~$6%|qqCDnvg;&uh>PH#?(Cx1E2Xx~G1D@7ZHO}6KvP)S5Uq2l*M
z4A@90#r!eA>BxP5%1ju!oE0b2>7x$4k+wM({kOlA-Ldeg*UUx_bd&BiT8e$g^r;I8
zV}M}5m<M`JB_lkN40s$Ka$T0JO2@AV3Z}W>`{BU^m`jOn51+b}<u}qRB`?riE?#dP
z3*vTLWA?g%t>dRH1XmY&O4(tp?(S~pgBQ8zNAPvejc1lP-|l3|VV?hqw@TZfW>OP6
z<pbuyP^Uh`mV>ICZXudbENq{a&Ljhb(!?p<`?I^3cUmqFnfE^xrpSIK@S|+C>Fix*
zYK2wwGvcst9_E8|4e@3_{<uausSrMOR;9IorP9pU(3>aR0BiEgkTbs@0`Q)SMM<oW
zt(L=eYs35rg_^R7g!u0BgAMPnlw-~2#L?a6*!+B}kB5oE0e76v!n7$^=f@0C``^T|
zi%VVZPH+phhKALK2OZlD=kjHDbM+<)shc~w>6*RawtTiwhdt_l^xOYn^XlN|;NI2c
zD9L%YgLGs5QZ~YxzpUwu#Vf3;!tb-iI-c|6);H*u$Cvt2qdYwqyjxn#A>CW1f2>_A
ziJElArD3$!7NPkf>(Mi+TP5Wbbj1}s0=F>b<y#tId87XM$)sP+x|M9AzZvzq1CL(K
zNXHv-My%aQ?Zffh*|0QE91*i7Z5pN6t8Gp@C+#Rlyz%Yr$2P^3xAMGG?bL~<W2bw5
z)uq2DG7xy_iu(H5w!G0TeFa*oQBT**{Kl|z59B7kJ0`+ya1m->2M*xaKWlF3$Q0ym
zqhu~10y#y}lWdSf`l=~$vLJslZh0}TAwoV}sb*9#5G81LS=cx<4RdBmS(VO$Kpw9=
zgrLUQgFomnjs28IuvD(i38!j&2BM-Diwt&Cz9+`aGW83=zkJ)fI6I;v8uh5OE!;rU
zCIPz}ibS6;4lmyQI;b()pE<LUcnSdScl4FaFpN?(m|~E2I}X3Bj6JE8J8M{_-uCM0
z2XYAJy@E5f`dE0>ZQOl9YMUS=uRh0$bQd$=5La&dXyTK;t>RCftHl(saH!v)b#fda
zHFIAd$e$EjDW92(M#K$k-Xr9Ocra$(Sz}3I2~&mc<iMmTf^gJgb}n#6PS>8;7i@lP
zxP{BWgCAf%@UM@_gX!n+lZ^6Mlq!`~@U}Y_9)4ae&*s-Zo>R_aKSOv!iCtc(u!_yy
zxJ3%TEoVc9jHIhux^!tWA9z<CVQ@Um_;=f~^-+}adIV7*(VvwVkxT}m&E^3?o3G8<
z0JfYu@utXJMg-)Q&{0<tpPnoiymN?{th`OV`~JikO;*{f6}PWPTvfqh(HT;?KNMu4
zmMm_+)L_`tsfjjPTyy7vZ|RfQ-=T0n!H}OcbXUOu#|qfsB%1@&B`#*M+qW<nwciA-
zW#en|QpvJd1tD>0wC}l)sHk;5og)q}bsKZ(mYv*xsv%GxeSejkJli7IOc`fzJV1wF
zm^1-o%}v))*u%TWF^Vg=%%o+EMxbPh@;1CSR)Z3gC-iC9&p#D@(8fkT{o#B!a?<#K
z231c_r!)vd9<0==Q-aL+{UFgt-x@@lzvh+yB;J&g%H}wrqXik%!PKsvRcC@(whcE(
zEAPBR{Ss}OjF1~`8X><Ox(PikXiFCJ<Ow*EaLT8HIIswICIlyxXc;Y9d7`-+TrM9v
zVwESS<Y%%8C`7QK5-Yr^fY?9GW!XhrWeUi5A^w%d;Vy&F$`V=(0#uGAB~zo3v4RhZ
zN%=L^!BlxP)>>RKG6k-6W)36JS?t@NkMhbd)p3ru2A0N#D7iEZz|kEv(->57A~dj_
zK+DL-8zR43V|-k|_A@I%Zo9|u+kinKH9IRKTm4{SI1nr;Zr%Dz)Wt22sYf&>j8oR#
z_zb>&#xE+x_Uz)+&0?WrO{uj7qM9I2L>fhwRawA4OdamFsR8yONNoO)ixDw#gjo4=
zQ;q7p3Gz56nDqbs@o|yxL%GWJj5KXI2*Jdp@<}g^*1}BPAzpJl<6#LR26Lmkhf4fh
zugMWR%rQF7@N+y1nZ|QU)O%0(d`=>u-Izn!@5tL#qDhtCheCXY`9>loRlXRQpPzg&
z`0Ze$Q#}v^JT3grdW&*(Gk180X=3nz{LIjkM2Z1ItV%`uP5<@LK0(}Et#p^)uhjua
zJY<X|MFK`Y<m*28y;A)fkpA|sPoRCWw)vKa&j`$GZLo(R>YmG>JA2(ds6b9eQUiBE
zoxrX27kKwjLoaqN8DA0zKfx%1HuCB-(|)t;kmvXq+YNv2LhOpaL*DV-Y&^QC0{GC{
z<=)^3A?PRcsZ)L)&8En3I*CxZKeGnzyJ~AVzncJx=4N^0Y$ll$8E41HSiKDx+eNM?
z<(g`T*%<b8c0Ivo$650N9noMOQqu{uqTqpcZ~~UY{Qk(Y{yRxv*YaqDgqbYjNtMC*
zll>L}IM<X7cUs%k;VI*fhi|ksfz$}oi$zFk){ZBwZi2u%?h(;jo@!HQnC+YBKk5k(
z;Ls2jH~tX0TFR3>Z=b;9y24gm_Bf!xasQE8nBeH>=_!=OHvK&}KHufc<KuWx4lsX9
zF){MZDqjqzf^uA>+hE=8nfu7xI0%oWz?WCP+r_3VtVYC6-hJv(6x46Zd9I$_Mi#QB
zZ2T}Z$!x~)vf#Em@u*92nIL&?#d2P==x;ycQjzUu*{YoEp6#Ylk(unS_2CbK)H(&?
zPWi^}(&;qH->Dm{?*UEAeiWinJML5tHXK2coTqM$UgAa42EPxp2FAUt2BnE$*m!nh
zBYYxWIA(2yISGGdX_LI%`+~qz4>)}bbuQ_vv4o^0pTQZAp^WPET#-5IjPP<Ia8}~m
zOyJ6hdQm_aOW_COa~LojM7KF{3G<;W@-Rjp1`XVNoI*YdNTVLw>onzTH9tKQcgbS2
zjyABvGtEu4nMo>Q<UKqO4(MCG`lc(_wl9x?0k&8hj_o|0Sz6)8b>cR4xDyJg%Z%O#
zX_zHG?|okXKCK9e5ag8|amwP;m!2b2B_faP;9(PNTH*kwwoZ~6oJaMJv}bWPFooAY
zGs7^g<PNE{rB5RSn?E3IDQ0`YGR59PI*p$-Z^UJ_P*}ax(f`R&I(+@vK%xz<kj)td
z;2726rkIwPYpf*%dZ4?bnWZcg_`+>Ix92k5f)2t%&c4FOYh%tme0#$4c%egx!_z!^
z+W6KnDYH@*$!AM3;pnTq&kFvF9kH8R<8*-|_VnR(`1=MfG=#$v<uCFTgBCup8=*8b
zL#UDhjKHBv-Pwom9&zdWEv&7#|KxXFop+S@Z;pJaKV9AB*1}t!JnlWYOE*R9x_Z&J
zs8UjS)v2W#ACc^6F&cKzuavd8CS!zrCbsOFGAM+s)MGN=dl5e!n6X}zkl;RL5$+eW
z@>ZDC^4G_A%gei(Da-MNlKs?v@QnxV%aX;MRK>*)I4pY?cYgS^&NouL;=db0gWgCa
zBB>j`91YF^FsW%S>tvrTUOuP|4<6`VGs6{mFA1lTP6OdQN$m7D0@pFTm&;wm3Z`0G
zpHXRF2{E~5mydjWcI)kHxs!9riRsfHjaMQZHE~>4<u57%OT|LHJLmhJQMulkS@R4$
zsJY7`w*Z70(gv@Me%3s+Osp)UcSn_ya!u3vja^vtPm$2L*LQKp4~q*80uQQQRt0?O
zf{9$NxOVIMpqwu&5e>My)F<+!?IBOrBk=<nsl4128@@y8j^kOWz*M~KZ|OXVxx7g-
zYCycf<?QzTO)If#d^r)YF%fD0hv&qh+wf51sb5zg&Dg(LIb!c#Xlq&jq$a?8FB=Cf
z%+u#`levvd(hF>kst^A93nbz1AD^L?DE*3~AP2uQnBE|LLz}ku$Z^yGXt4=}FW>N;
ziA<hv&#ypqcV2;h>^`-bLb?HAC{LcJR75GrE3ux9%x7JDh(rIizbC0aVgBGJ5d@qz
zrHrw!m<VZ(x2DQX+%T9GebSCz7yypHWQkdZS{+EufD<k920Skk>kiXheJofO{qMo=
zh(65&-e9Fk=#oc2$y9>ALIfOx<>AM0;TpqGqmedoqm?NsnS&xoaX%gi!azXzMnSMp
zz#w8`?RKN#JI}Yw;~xjLZ#^rH@lO3|XE};1x0YBRHJO^Y{KX&B8*J3d*nIaoXw*5E
zk;7;kB1Uch)ho$w(PhZqbXPT=HVycyjIpL;1c2d(B^bkZ(%%Ve?Wzhi$o=^^U|>Ge
z#GMGvvqEcg6WLJhE4V(_<C_~~9aOCxzQ!@jN=`6D%Qy21GaBjo;*;KYwY9}?D)vU9
zoi)OGpnKZL-pZK`>>F^Fv4qmA)~7_6;jnjCzYGq>%_w0v-I$7xS!X!%>)D_4$fIEb
znku;PUCPxn$lDXa@R0o00PwSo0stlpM*!AIT)!9=-s1|<;dv(G?eEyQC1iy9b#pUr
zG4g3q(7<>zWI37~ukN=t((vALp;F`YhN$bQAFI)&IlHAduTH6Z*+Yi{$3kuj6jYD8
zHgLod{C=ddOkG2U1Q~z{$e4HrgH=-UEAsFyh|-<ylo6``5r8r&Qml%%VG;gw<*^?V
z4%ig=C@v8@d@e`EIxXcQY)n`yps=tgZyvYKCbkqfeTRQJT8v9W$-CG^x@$6iq%n33
z%ZC&s)4(afbmFrdd{{D#kkQdB2jg__`qSq@F;dNjihUNQ0THUR@*P7wFOqEDeUW<s
zEY`!T=)3G!Tq@9h6_daNo6&4uUQ<uV?Qt9AaO>HJ*(bBXd1Y3OE`>k*d3=2O&O17r
zS<_r-!Y+1LfI~I@F$A1yyoY)!o=(kAA-s23D0woWMd{Xb@x}jk*zqM9GOOp(2+rki
zQhAn2Z?Y=wgNzHQ6%y$Z%w;`0iZD+vR2%l+F3l%>40AN*({HC?-LgXY!h`4tl}rXc
zq%hQo1cc=v@^d%OQE;D<6N1a@g#$PBv_>t_y2pnkAC>9X(X;qv$WS)le(Oa~uK%f8
zla3E*pr>ZakNr)*L(Ai4iQrOfnOa=6fy}DSA4?{EJy(}Yw{pn8qUgGv&?K037}x8|
z<!{KT#y>0?ESaz`%Bx&Ym^EI1-v|N+v>ME<uU!ODUXybIKwF)AHJJ)<`+Iy?lE&@R
z;nw9Yog?5REAsG~;zqx*THeHSzT3+2`*HvuWz1G1$%M!3C7F7`Z&W<NX^85Lvjhqg
z?{SM++-i1LtMVVTR#5hHaL&Oe<Yg#})rPZob|zzc*>I$Rxy>y%>9mtYar(`jvT_kA
z1_t<@41C4t#@K|Kqs5LaincN0E*z3~!##+UQMx5VAx<}eLM>O2IMK;&o*0(A!B8$A
zug+UwX>JSav&g;TcY>dY_nVC?xPUP3XwdQZy@3fc5!tx8w7MP3WN^CqGEoVCK|1V*
zjCV!)Avi}VwmcCwXEuJTYfAjBD|cH^XMkBA+^uw$Tf@N6rK;{yPsrfi)1*yhGV<5Z
znmAU(dYz=O-FE%bVn$irqdk4z(*a~XrW^oa*`_|TRxb>7aYIr7SgkCI7krgLf1d@o
z;Nkw(EXYhWc>;1`1N*}n@fTOy%??i}S2I`tsF}{y9^(px$1p035Hcu$zZUQ{Io%$b
znftt!yrx>biK2;@$nQ4kvFDQ1jDpz=o3B#7@pkSCF+ZO-FCl}Di3)wfYYeR45Lp?B
z6~-%y;^y6rU-vTuXx=prMVC8kVKL-cqQ>vQ-w3$d8D1U)<Q-oPO;h#^5l<~6!8JH6
z->MfK6xmcL(CI<^FlF^F0$dI7gW}Z>Jt?vV!aLsS`i!ni#H=V_UBo18zgH77x`Qek
zl}d}T+4V7;PoKoxVDBzlc6NpEM$##aa^mN&@07B7Re#P2f2|kgdH0z`eBz@ZYEZgJ
zJQd+odQT}SD8uHS`svOd_ui*+{LGmgm5j=VTt;nU8CQp*MmdK<^&OA4hCgs*XLruz
zXd1b)rgAGhP7kHy5XmbmDQ77v9@=3enN88cACb;{mMBB82s5JXUVQYHD=Juwi);Ut
zaN!O&&3wmG*8#zd-Ew@cfJpbGQ?>uq)mMjA^#$#s2qGY$(jkaQN;eWxDsgC}8>G7%
zNof&@LrTLz4&B||-QC>{cj525_xYat$N6WUv-eqR)~uO#X4al}Flw-pI0ffy+*Uot
zZZ6YW&;|Z(NsEPi`SCdA@Xf{u0%w-o2wr&3RuMeLB^UEDx>l=G*EKG11vR)ZcwUKK
zR$w2KidHkp@+oVavg*%wEMwkhY;)_(P*rE^>fjjI&7rFgcKT)_S<@!N#~qX>IvNK=
zbK~u9s&reNCLCu-MyT->a0%0rvV{Rfq-rNth6#)<S_av6dPmCer_bAB7FB*1oBgVM
zoEAv@;+}pHn~Ck)(AeX}g<s_)s(K#SH5<b=XV0ifrp2G(6AtINDGK6Z#$qRAm_}vF
zIom1H_nNS=+mBu^S1{PBn2zq_J$WHTO0~j+=WG8`iV#)dBqCSTwj}@!XZNecXZhNa
zUoonxbvAN}e%{MV6`BI|&P^VPn<b?y<3fE7v104cFKa0DUq+E2(p-=X`n&p9oD!m=
zhhBx`DUbeg39+)o8`#SKXe$UuQzVz_On_(@Ik)bgnM-Z(g*LLYUqim3sY8|3x(V<u
zrh;R>tnYqUO6bEeySvIV?CdfV7W5r{OwmDsvMnS((XPcm*WKp7P!zonKaVyId7rcd
zLyJT2(Z`SPjeZuJVT1ymi4~75GmFDDYtAw?f)zRGcbsN!^{3OLhN+#ovELtuc4B8)
zNa75CN2=Q9^XXYteo0Y`q&}`uxv7764P(&%$;dTPj`I$ZVPn^Jdto2C*|TKaFewQN
zhu!L9O2Jfi*mHF}m6@l?Wj+SgH)}%^B-T=f@8jJX*LonE%TzzCUao?%hQMjxF_#c?
zaJcv_=MA>*BHj46nej+BT0e{J&bNjsZv3U0Q><53rHLp#06BHIwLPz>o?o7K_h=wC
z(o@Yku1}5D!3<knMMH|qoy2N!Z$l`)QYH*J$16893@8wf=W(@l{%#FB!-v7NC(AY8
zS6GNz@fmW9JT7lzT^bi#`%4%R)B2Xd&%3!few$k$LHR4J2)ArmcM>D5*ioimK=g2j
z(#xzd8chRhF`c?RJ@SI<S<~kWgS3yl+J6|V7yaA`mu<O^%7jPq^WB6kE=x(6*wG;d
zE-zwsIu(+#=e0k_Q|;-Z4OyMOi8PIb4t06DdpzlO2x7gP!7;eUiNQJTmC%15hkIBU
zC%HBugZE^-RpSp#vy!Q+UkWptKE4YtTWbL_iq8Sf`_*KpezBITggeXFZXbBX$2yWT
zvNb!57ADc?0IMp#=Z__Aywtn8gud}On)6bxX_}=^cp`Xlt8GN<dAX?>7xQJbvH;d#
z^<JBaH}Xvs<A9r5ixG`A>#q(uJeKTS7I`U}J5FfNF!7m)q{PVnvy!;i8>>B;^i#_o
zO&&glIG++t#EJtI!v1V#yg|(TO6yh60bN=R)~m5G4mn(*myPVf?P{xg_mW!lE3S<#
zPwQzX+pEqk?+*J~`%^YQ863aKo+*)VH1+r|^Dowo`i|rRj?Z4a>Lhkw7OQkZdLiXa
z=5|6YgL|0mCq4c}A2t1$qjzvs`tp3!L?yY}`7wAGnN~9E{rmP-*e_KgiIpEnkTN!Z
z*_58N&Jjhh!8hH$)A#CNV?Xx)G-3IN`e(-&F2=FwX7S`>U}O9<e`yRjGDOfb{puJC
zsnDkn_?a_Id>B!fP_%SpHpg(u+-pjQcX62Wh){^qKQlToS-+3GqF`e-D>8Q!vETku
z+s;C^kaZtdpxWQV@zPtk&xXl9>R7nA+ar0`qr&~{2#KiMAy#TN_sZ}YdBWaNzKtN(
z9tF9_ZM5tPs$t_9B;90OhUQ@2bGp61A492zjU^_7K^q8ufIZ5bK4Qr9O>&<Rd9Xos
ziO4N)U2R}{83+C4uJt$V==-Cgr~2>XMcPu?x7sbU4LENs>j(Qi?@piISPBSe*^{tt
z-grl|uKHNp@@PcMwVcWsab~f&tu7!-bM;1jPO!)q%jKvUkFip5v=;k@R`LmP`v%G=
zuo}~Yb1{V$WAdqWtjlhysYrt*x;7IU;Eh5hh~<l!_%jD6MnvDVrnnhW1zb(Gnv2w$
zFrkx)KPK0um;{Lb!alBv|LzK!h?CmkFe|4qEMGl6If&ajqKHi$Oh&J-%@b<y0W4zR
zU&S~GSMtpDYNgH^3)0<i1U;KSBuopcyfg`S%a@u@$Y9V<g`sA`e|4yygi{%ZUK)o#
zb<+LJx_=@aRqMGpk=;cvtIUCG%dNgpOT-!+ly&O1Gc$m-!ig6nP~1n$m44*+>(Pdb
z&Ljp>yABJqy@;eJ8gZ|SWd5I0p^y@I2pcfyEpjomp=S2E8Z%{?(Eawxu{C%v#$sq~
z?F1LX?p;Oob<-rIGg43`>*>D>LPp1~A@K9xdKRIYli-*y9a&65DeuhU9REX_YKap+
zuJ<QBSIs16t55v)<OEjwyRG#h^PIv6k>~>H^Oiny+IN;8Br|N-{M=h_WjmEIjJp{w
zP5A0+{T3(9#MC7-ZS2Maa8aoIe&#5ND69?p+j0k+yh1LH)*xJlgr*enz@mQ9ajl?o
zxK?d_RiH?sck<4RHqxLkTVCP<2TW~Mg8v#67xOi366s2l0XzC+5u?pPqX;R!3MW>w
z%?DYS)tC^5FI3j%2K>C<7NJLS-;OttU7L-Jm-h2*AEyBd)5#u#yrYo`@(_xeY>1=A
z&tf0eB!s41G(yM<my3zlS!{B9Fulo?-gnXIOg4XaeKd#8D>5ysj%kZd;qF!>JpXSj
zBLNn>dag75Aa4-VcDJVBfAv$0$56@qF)v%B%GbOpag=L2vvG|fWSykKRQE=GliTeC
z>{ipy$2YEsC9O4L2%MQiHnOPXz(!TD20n%4Uwl(!xF3D>#lDs3c6mCUSs~*uk_3F=
z54AW%ufsXb7LOL4ds~=)4Tdj$cXq@Tsza(4*9@#l%zJ$7*5F?Wq~cc6pV9L!&cmt@
zxN{v^L=~~s&&PhQpkk<WXL4_SR;txK1@K3yt;@7#%bRi?*|3s1D%Asuv5eH7LbH>m
z{Q)~#9=|rz0)0-f)dv}=ofTID<9O_)E%daMIktBt@ac0lQ5}kiG=8(8VQfv=+bFUo
zF{~$xZw>0+Nqn9)2@2Pk(v8{=MnY#(%qZs$Df`3Ty`c{Oy?017s?lXX%1Xk-7S&+h
zJ%aT7*QCwjZ#ph@0*Rqvd|gwC*c2%GF^RTnng=}OUFY1I#cMRDCLNs?11Apo5Cs|>
zd$pW|b&)!zs?3F6<Y9J$?**)4`tOgBvM7ddg9?A<Fc*<2#uS>NmNl1+p+qk$n7@dm
z<JxGo)0WozdAPE*?P(#Eu?fR;O0Gof-u-f1{ar=-QpmKj$KbuLM;e>O2h}v+TV&Qv
zw;b~@vJJ=M#^6VpWHZhpH@VlUK~GJy2G8u<J5@P|wtWB;9H*4WkL$WWRu%YM#-2hg
zKB;g!Es9sLBXl|$(rP!QE3K3Oe6OKV#-|RnO>>MGkG+tmL7NRiqY7!;F5U+GijFSY
z{(KsTK@I((Ic(uL!=6b6tew*sKpjLZC0J=uFCd|Tw8+tgf5hq*e&zq-spf{Jro>{{
zN!+Ww6J>sRN;DBxy%U#l>9sP$gBQ@B&-pb(rmXGpM%+4v0JLiu`>Yd*_DhQxmhXqi
zvxzQpDHrCy@_F?d6C;;CB=ppxZ+`bpt0#VW$;Pf(&+-VMZaO&S1_x&V^vhh8vmqMB
z{rMsP>j~x0u07#!8eez97xl6=FI<<l3(X-oFaFFX78X4|RDdh{=6$rCSQq6D5?bDJ
z3hITF`zn3rZCJ1)L=vBVY!nm=Om=r#XOKdoH=cbF?vJ<K4p!<1+qBhFX*B{0+{G3=
z=6PzxCw>Hz*;-VUl5gzXSz6D=HUfr-l^b&_<$~K%1%;bc1plv&ogZbS!zs!CQcHaL
z>)<FtAhqIj*o3deKOolVFrbK2+=jf)%4Xka5<xb;ohG-^Qc7dB$a-Fa+nOL`<?)TU
zHCh^#w=gY3S0hxXxQF&l1pV*#m^Go!0?wKcW1d`R7MfM5wO(0)$r_D>OQjqK1;3)N
z^q1Z~yIfBop;tc}^^1x;Ov#~i1d%XQoX~*vO0FV1*(-n$HClh0>9weTy^g;8t-_m~
zfBm;AUf#HB8`0M->1}mN&J#DDGDOy#8uf&xS=s~&0V)(nmO(iaGaTaxgA-G0;+V!w
zbPtUTU_hlY9F9pbPDj-Fm&OIZm&N8+!Bqbcd7R#%Bu`JviuW1^H3`QPI=MI!A=E;2
zl4b_IwemxPb~i%~_f9vDe5~o}t1^e2(IJ(qgcxkTkhYg9*wmA>2yDJ?zyKi1Z^Aa4
z)_wr5PHRqSIyq4$@m_o2^)V|~VvbACv2JcPgN&kjXurXwWjmaSEmCu7xvCTnuR>_<
zHahzd)s>QqN^;mx^j4u;z_SLKlP~lQe{C()|EE}wWrrxu@54fvb(el|0{89f96Qu%
zZ9RPCSt5=C=ixQQF3K0O-d0(Qbhz5Nbm-jBxFA?GT7Zd}X`uF`Fq`k~#q&^-yg|{J
z<nv#sm$a6DYUyRA)Z>9U53Bp|QSE&L{OstIWJcL@d%`YMU}jX+{)0|(U?#0T8ivR0
zv4LtVKYAQtr`vahzBWUn3)~-!$#B^M4Q$ajZquP2rl!6UZ*2`|YKC~+3Cj!veT4?w
zvo8qD%z_TWspyDi`c`*_l&MWl4fnMYNRaU~c?G(X`PJ2aXC6!CcqXM%OK^B07ntEG
z${GbCcPMYi^T*Ezv=t<K)>YS@oI@aQ{J#y&48Mp(Q@C&v1uZIS8un(ZxouWZq<JqX
z0!Tmf_#LmW%47RVV+jfie}@$><G$5~b;D|N(C{b9Af_!^tbC4Y%IOM3iouJM@3Sm=
z4o+&#O|e2~2fd^`3s*ZOdBUZ~#ls!#1=A*ekJpyUqd68`?C*Y7AY}`R$nIKSacVdi
z(r7JTH_u1l(;xH5d>!8%9hAk%DJqT}yN}sxb+yLkJgsBIBW&B!ek~j*D)6gQzfQrm
zw2&DTdxuz0fKeP|^IMDcRAHBz=XI!TN<UdeYgRnpUbFLm{yfE=FL{15d8Ie^m8ojG
zBxQSkKW2A~4@y?lD^$OSo=~=d+(l4y(9?&Sg<T4Z-`dT?(@<2a)Sv||nndJ|GkI&V
zlCS#}_n}u~GJi=#AMF_ma9N!Ry_oYP3la38X6lFh^O=J)@4_%+yKx6_`YUTFkb{Lj
z$^H)dtu8!qg3c1tsKBBWgs=$<g45!dLuk(mFRrV{vo0Gu<7Z+enIj9GjBF93cdcxp
zYZ1MJ4Ml|KRbDAD5Q-UnG-NAuash8l6MMSem`u*k?ZhwL-((%X6Ua7sSz<O%eh2Gj
z2mk>}3`3ZLIYX_whm(!ha!h+J;V<BjaScrOzs9M>IdA8cJ6&?tFK$KU5_{|vD&|YG
z#gNTFd5ba%n^=u$z{xIA6*g6t{=PD!(J|W6iY&e$%~iCw`X*q@*-k!|-T{>Y=KgDu
zP(0Vx$*|!5?3<uIJmQh*A=P8}dVD->+B-G1nh_m##GbO)WPP~I%deX?8bOjdu<}s1
z#jgrf5v4NNurUdP_wo8=4FX9O&l<8EOO}QeEhI-R2onm;9=Ez<UfQvA{~&{898ts8
z&v6ckLg<$&_>+3Pw{5H`8iBOBG(oQAYe{8JMnV&RXb&ywpoX>;PqPzw{|>F+c%bPk
zbz-pvE{o1EA*{s`^3*}<aNaBGe%@R3aQ-W5JO=fj(y~~-FpejrNDK%44y7Y(1L|c5
z@;H(jdgRb%dp=ViKi11BU4A%T8mEQmebptANk+vn0il29*F>BNLbP8!xZ<+ui<9WH
zr<(e`BQ<M{U?ojvs@q?3bFlm2$9EqFw5s@FT9_<!DHoL$K)VOARTjcaJDPXmmD(>S
z3G&y(l@uf)?{JxDU3T4>;ubzwp2a*H`s_;NyxZlpX$j(|<hi*XAe_ULWKx1$1k{>Q
zN7HN0Ot~7+G@W3-)cLMf(1lRwhyKgO5?av6t7oGF|MvElSl~`=Ux+liCZ7yt4!uaA
zu0Hp-$yN5naR8^4mELV!RvfhxWs-5OjABZ5koR&Gg{>w`O6w=gH2kSGh_j+{ny!9!
z*1K4}4OQ$R0tYQZ`kj}Wtt&@gXF7>U5PWM#M0Q`coe1lm+vB;*qAEXHSo?(VdTOR4
zBVzTmYgp0mPYSc#yHk#Yjst-{^y8sRW`<CT37MTiyt7T5roA?WxVuB_NpA>`p`H=y
zQ&&QQ*s4;^c0a>2J2^ZiE@$_mlk@MJ=tOuIdQp9O1?v=bgf>V4@G3IPQ)gC6`u4>D
zD$9fI^B7zyAfL^e-bTpj#7k=+0YbS=>}f}{^(2jRt9|!#>yys{6fxcK!(4GJU+Kq!
z^2-hWRpD;c=Oy{`lDPoTQd%xAJ%Da;(Gn`tNh6q?&YzqG9Mk!*oE#j6Wi;d9O9Hx}
zs75F{5mBvOa;g)#iB5ghrAwBdrV3mxA#}ZB&aB2?hUPgT8ezJP14LGP3ubuq%m^ml
zFoG|k;KE&daYy}JCvwl9%zZgW4i&V@!Es;lH!R|U2XDLdEycmGFykiR!e!d5Oo`+?
z^LCS2w(d5YgnyMA{yGx}FIF(f=kQAJnNdbES{b;70hzrLMGKI4TP#sGk3=Z6RwQqa
z=3a_QULpcoK2=2ID#4woY}ndZ`+0*6RE}s+y$=Haa4#L3gsC$^hOyw1`R|Ih)yQV>
z&zNOe^E(F=&>=dMdJ%<Yclme>HpX1c`Sao;00C0TD~Czcq~-;$*YS}K9;JR(xeM1y
z(xNzBIXbsIJH8t*WA^^NG!kl@RGFW6@05;*Egl*|B*gEZ+3nYFL@r#mpk2*SOn@eQ
z+@iv(9ZjE$9qz9C9dk5*oK-#M>L(^d{<3=f`f_?4egvI0w;Ov(I}}nLSxijaZXm4Z
zTeGXsgYma*T&qgq>N2?xg9X~t<yYG0_SY?8sgUb%B9DqHd2_j2>ng}C2;>yVe_Wl)
zNmoJb>i*`5x9n$RVE@VO+NrH~OO1hqF5+0q%j2k%LFt&d5>vA9ONSJYAc~zU=EQiE
zk=!2k7EeSETZWp>)yl>(*3J;1s26ro&=5tK0b!gJh>wQ^G&N)MXp@xVC0Cu#r`V9D
z4U}9%B;GzNEBz!cx_5sZsWv&WKw>zMwY)G%-9zNvr~jVCxxd3!lZ=KMpp+{rIp13s
z8r`x=nx5R4wgTdqO@%P8l&|Ks#g*Je=^Z(uEQZ4xq)YQ8Q$0>Ytot?PtbG^iVe`*>
z4kpT?<Mv5nr3wp~eKV=wZ&b)f<|u^S1!AxJe*=cN?=t0yym7FAd-i7rcs2VH1&a59
z*tgH7zXwI4&jSoWgpI-&`v(7zs3$!yE+u002jxL3r&7CIx()6ldAd=lO_<W>sJ`im
zFbN*Z@sJLHW@~okJk>~|Qk^i?JCS6Iv=WcE<QPLK4mWA*E)^1&aMF@#tFn?SxrN(`
zoHuXqt3O!qG4%&idPV=hoVJECjB%@!$w$ipK?Q$HDYvi}W`+Lx;4VC&&tRAbbLeo0
z;c%|tjr&u&!Br20;luZwctR#TCr1PkdDu8N)gW|rMm2wzE5j}iPK%w;`c_Kbn9-bD
zF(ftP2+2Y+eWeL(ZjT=%-^oxM7dT%D@N&y6e&p|0A4`2#=7DtZ`~|5GD?z@)7o|a5
z?8I`BTM8Cj;b9uWxBN*{vlvdd=UwNqFmEZ@21_GJT-O1R)9gv7XUiBO6_>|(?B8~p
z!<K0OXiJy}oY0G{Sss{H*TD9_<$(+57|w1hwHc$tvx&Ol>}u8!IIYrjCYm$nXPO1d
zgu$pUp6C6IS#X@IXe*RR3$C-Ys&cp;Y4gW|x}~IU+>~~w-HhWgKDM%8YP=O)zdUpD
zO4)1`9gtyOy{i6B;BPpbG!T8A@Gl`F%`$r`cmyPvsrIGOx_c_`FOIxCh{<wlARQy!
za*CNLx*sq=CUVOgIsAm<Nb7!euBhomxnbeR>1DTrQT9vgBO=0LQERKMHa5ZJwz@s8
zm``gmiW!pGev%~W6Jvxa0E4fh>G?|LVZW(L{FAf1@QcnVBKADs<<8y@Sv)rU<ZhOz
zt|-+3ksIV?TjyWgM7E6==EfMdTUXdn-9dMHR%gg5xhleXz&Gv>OwH2sMCv8=b4KK!
zrS<Hoj=<E^8{b6H<*P`nn**)Sh|4^x^AFDbbKGqE1U)pzE@?bp((7><o#@g-LI8`u
z(f3xf>hHTP#%ou#9_f1#)Nv$5mHu64)*gm%N|ehJmUy4NDzahY;KFpO1lZ_30{gib
zlBCAFh{Kk~s6mR}7Xn0t!hd?Q5H0gqaoJ?BFVgrDc`GV&6i_s~(b;9p`iBJ~T981r
zZ5)d9<7uz%u};Q{7S0)gB&BUn`tcK7Cy!A-$xp8ceyN}d(}!0?ZR7M~G+AqSIDGrr
zJRJv80bxlsVsj3#ph%EXIP_AnpBK*5Jla<6sKFwF_JR;1C2KO5dmDynqxT)H8V$!{
z1-||AIlgjPvk`fKxA$&^0qa|ko-!5DjrAs*QDO7Dy_sw#%H@ftbK={9m3pesdk7=8
z28Wfj-zvsbGD9=nBj&#Q3`1c^+cG7}1*jDbBDC#`c@W|)bGT~ODE3(t;iiQN`m4ij
zx7Ehb;Jy1s$aBf8qnQM8P`Y?guPI)eQG^s1#QLhawdp!XzpiK7)Y6f!ogoyj^~-5T
z2#op(B&tv(I(OMN-G;-nAUEp%;^ON|mEiijO_jT2w5V@Ax63rDxCG_yL|aZ-`8DCQ
z%H&T`)rqARI7_yKFz_MimyGpw%4g-Kku(?<CfW>Y3!q%}VLEN1@95hyNQpiyYP8!~
zor6Qk*L=$>rk4y=TPhk-8AoQ7dC}BHCM)Ivz<80Ij>=0N>WWY)r;S2*jeeFAtJm<m
znp;bMVf*Z^xgCq-nDW^r{-QkG0=Wiz;r?#YHJ2xeF`au!kJ3D9E1ZMFfAcTefB>|u
zXjjgT#>hl7qFRv}yoA&6QC<-_bGTI@v1r5R3>wdRQsxT^J`f$!`zv|An`z;AfWf&~
znKV>gIP_Amk<{oh%>J(D-H8>UO-<?3#}`N&Qbe4dWmU}n$%)V$$lbfrkNb{fuimNt
zXxiF1=wfmRyUkidHI*M!9H>9yiEupwtI95!MbEP>v|UzLAkA3mJ3rSCe`ChNl}LkQ
z!T3pjLS2MwQk?*Tb-2yuTVo_$p)75XZCd@DuOuT7t@+ufZ>%S5E^G0-_HW?Q>9JCH
z;=QTD{E6PnzdEPcL@h-V+^T|L4IO3}^0_37vOy-MY!PqE{;a|1U#Dj=3$<8UnUiZM
znvLoT3;%FxSlGJAD`R6N2oB)nJlH#>S$dSFk2Qs#BrOnq+;sT{Vd(O)lq}h30OTXh
zrN*=En}RpOWGLQN>#x{qrl`6d#!5T1`4t1u>;>n|n~a^^R}iX*OhnQjQ*w8&h3398
zc#$_Z_GPrKlg{IcaSFfYs*&q{+tA^+!)PQ{IXQhJl05{?Pg<iRAQJ|PPD&Mp^Hcs*
zHJ@IpfFRvrj$>K&(V1Oq1(REDZO?={uIS+tX?nM+PXmSnoJsmhY9T35Hl1Wai4m>V
zVlf(1Mpm}vQK8U2mxwa;#zy;ERW9M&=B(V4GLgcaM>f%jk1GAg&;lRi?-rtL>YEAP
zJo4UO%MKqf=RjOs+V_Y3hH5gT8wbvL9sUs;I|_=@yHoRC&6(Z3DR!M26XbH1O}osj
zM=)W&<w`6&z<FzsB1c=u>+&rdwVEe2q<=MfW=wso7!&21*6{Lnk#F}zI=urs?}fi1
z>qDwUd-rfq(N9>-^7I&l9bjp9<|lr@bDrQ~Z##1OimlRVEYDXnFF(uD<rx`KbSP`R
zxT&*1hqT%^MyS}6Cyb#TT7z`%@~6C<OH&;^;h$|XyB$AZv<8&FMmJ!xh@<#t0FC8)
z)x0U$yXLoa9>C|2I2A*?%~nm*%zn3&&u6jA%viL>S^{HzTDxSGMgbS9+cNI%g)GVy
zp2sUuT?{quy*s~e@JB{A<W)K<6bbve*=u7Kc4ZVehVd$tqR7`BlzP)_Rf>>^9%@XM
z?qRa^f`UQKqW=^+6hE#C&hZ4TGV5(yBP5jFH=OE=iOP$f&o(Eb4h$e~bGVf_Mx>!Q
zF^0Pc<s->3(;YZ5iNeiX_tH(NRD1JfZ+>DSkO^i;5w+xV2A10J#f!4sw`~H6L^?(s
zy)EitVO0yit}RVyHS8bFRdO_fvV@kIc1<aFlY3%0@vwcbtqm#TSyC}cjB>-l72%q>
zf99}Rz#qY61cX-?4;%p|=t>E1?FA#Dl1UF3sh*gRp4^15kDK5zsA+^Z#*}7)Y;y0?
zDySur<gsfFD<ElwzP3PkB;RbAjUOzMuKUQR(kvx2Yfx3ji*Yai^<-u3pIRE-uRmxG
zh5}FD*mfTFt6)?x3=%dOyPE!vVGKFi9lv^Zc_KP&hc-Eby|z*9V`hSm6MVX8dtKEf
z@Kls$R2wIV-s;RU+tAA8XO6<_ZfkMB6|}%B`<Dc0(~m|=CHQWAN8i4s2JnnIbx-)q
zDE%j1qoEgCY6jMN6Y079GBD^>!QtkpVY~hNB~P5Q&5{<Aej!uT>!91K-*LEziS~kb
z!^>&}5-TP=^-6k%uIRbitXz>fR$9sJ1ANvz@eG!OLyDv;r*zBe_)-T8nO8yQrLBk|
zlq{e?fq&_%{&D*d%hr*!4x9U#_rYP)?T`Ahz3q_WFP8GX<7%lMNfU!YZZ8DOtKatB
z>GJHPHM}3=n&nS3H*`%1;7}5X4k%|U$n*F73jK+W&u%y$)uv~N_F*cudv3n1VEnHW
zM`FH;0DJN$aK^yZxzPb7j!=4bx({t1^VK22&fZ^asmx4Ji2pT7tonuu@8>_HQ@zUE
zAFn@;f8?G%ts%+QJAZPoJ-$@Dyti+B@#TCHv~g*bFlW$LlSp54eLYumVckdM$nl;z
zSVjpasWx9v^jZy%ZPPr^%GoY#Zf)y2n;NytyNnT(>?X2`i|O0?%9MNUO<~^Vl_8Wx
z7m>m?Z}Jd-!CrwvDgFZIh^`hVRJD#+RqyWIcJx<&&b{42%yxw%+iRW}Y#)C(OJbMa
z2L^&}hRQO=Plg&^i%`&=0`08;6;&ILmD1;gmavaN&xq>Y5!WDvyH+pR(d&Jk7N4Ab
zaV>LJPP4QQo@1<*UOu~9{$5^-=j_E9!AwG*Q;tIWuUEnk2S1u-TR$sm-&(H^(nGeZ
z>RO-lx`4yr;pV0}^s)9h@=D<RfJE_@YMR8pyk1MVP^DT`0mt5vLZa_Oq1Q=!i!>=b
zvVix-U)<94<h<f(?uSR}WVqOi6#wcJ3_|kQAOvs7DlhNQzInaz<LTwi9bD9K#C^id
z{5flM#{g&afeShVqOklS5n&ZfJp1GSjLFcUw3F=SHTT7fevF#L%z|+JzHO!Pf~KbP
zP*S04qKDEg3h;M@GvkWw1CstZ3LcZ}68@PB<BH^uG}iQjtW-a+<Vs<IF;-xm61DhH
zQe9=m@QIP>Z{4(}^b$`DkReh(3=xz}8FU4GQYO*q2Um$w97bJ@c>-}BDqLXuRtN35
z^v8?Ge5)l-@DTWM2hPf1@$=h_23Vz-yjsq%%IFe%7%mtGq~!(~b%0PUl&<Otnh00k
z3mOc(haXFowA}CxO^dkU_(y}D7a`~iJY;jX4`V-Iegno5j5wR?0?u1Z>m!Um5*r0B
z1U&7(e+RcOf)MKALepm2XIL|m;KOkL=UTzu%wh2ss^4S7zJ$fA)9vlLy<)@uRAX8g
ztP}ch4~io~rt#6quynAMDm@HV<hZZ7SG`|nujR-Pv%Hy=EI8g8_3wec(m$+JFZNx)
zy;bnwe-nSA<6I@;|MB7P{yQD?Fbb5cQ{e~?`~Po*C1Fm3l+VCM{x{Bl0{!2&gcFMm
z|9{^NyO4i<^6%$=pKP)hiNEh&T5Eu`efn&r5!2R2S;cAAV0fi(^)+z0N>WP7YWd2Z
z1&W>`^CKw<(z0w#kih;^JgZ`EzJF)ueH?sna8gu4g070PH&00-MMm4m=toM5AaQ#N
z93B?R$M@vs5kZ3D(qnn~$dmKa2=T0E$dApYN}yN%$TTqJ;fjZU7@D7#rc7)jMJaGT
z{<gJkpso}=!j<<6PxAt&(JPtH^{l{go*yt;2;3cpuY{-~E-x>KgoYK_u1oC+Sb3SN
zmD}v?5D>(&A8i#XZS3uRo3C@9slL`cswCb|{y>LVj6CPGY_9+@=uhGUga+Z!(YPLG
z!^vF@f4`eiHjm~iO_sTTc#1)+4d^=DtS%A%x@jUCKBrj!rC#k2x;tH7eBbb>%yPc1
z6%Efy<1?~h)Rz|HD8RSj<{|L?BbMlQH-709nV;WPx+5c&wF!~Ge?(A}wIWtaCEo@V
z4!+HA?3x?wH6A|_U}Y_4VKODNRJm~Hf86+ovke|>J`Jvfgn-l#6V^vrMdIn|JMgk-
zsm47#7;f5Xtq2$|Aj36S$-W69T<*W!?vAw<s01#Vp^}+)tXEt0SFgaV2d1XPwdNJ)
zT&~UCGQ~LJR|kay47A5OnlU|t_TBHVFC<B$4F<>N+9KX2M}JpAly<v%=5cqN%o-mn
z`bYt?e5`emirzzrg6#b#+iksizLgFA=#uvuleob!j)lvV3^!DFmA>wHc3*dc$GLlH
zq4BAVB9y31^>?n)^`5Gr=gEfb1OI`vd;a&7#Wyz|0;l$;HlOkk-QAVM-bDh1sjA}a
z@cR3f6Rwq3oAyd0#xS!K<m5N%$HMidOG-)xMn>NI6iF!~1%Lnk$c5vqL;LSrKOsT*
zttiS9MExP?yO>v<mEs-;KAuk&PB}I8thBv?@fZ*Gav~q38S&*{DVXtR|LPCl=AL&i
ziLZMlz~z;3#v4>Gz5E(UU5hZK@Y<$>gFj%76`;lKe$6pQrq06o_2%~W@~BdHCr|?g
zp5BHA1V9aN16WP;9UP*y?yrzge?8XV6k*H5Bv1J28j&T(6(;PdMS4DekANv~LNmvA
z2?tM%jgMbmKzuq`YD%S6X7+7A={h?<H}bp6+f7Nrc6(zpleHvQhvQAEBz^6VzQd{&
zE~}{FuL0SpC1Wj6+k-|Q_1$V(prPvHt+dLLl2Qks`|n=3?oA8tkL692jf9~(#v;_l
zV|vi@OYz0KJdh~-5J{curAdr7J~mA+%n2;l^bD1x3^k5hvE`onz@x7{_Jx9Yt#>?Z
zcxDQ<VP)mXqk1jmZ_jy84w^ydjeB31Q-6}5(04!+Acy>op7X6nR(^iCBu{f6$p;Bc
z`5^qgXO3|W74fDC9w%vOZfC{0`PSlwLz;qm5#Jjh@qFM}{i})?ZLqkcOQ<Pm!~&%t
z#^`Q%uyv%ga?2-gY!{oH!(QLMx!*6pkA_YXAb4E(kahqG>(h1$xb0WpTm%R#%)Z)3
zIB3iWW?=&NHIX!HXwsBjVI2UDkc-ItJDM^*+oKh+xVShg4QpZ*w7|(H$@fe3WW*S(
zOjePOb1ttmIKeDC81LGpH#c@f=reiM|74Vgj*W2uLvwUCZ5h#-9@x5UFi-A!wiDU-
z0vz5GVh1)&+vQtal@rNRQm&3pN9iEOHo|959!rLVzr+aG+}UXwVAu3G6g?y1BJ(Lm
z#^67pQE$)8sT4)lt8F`3@JZl0!~l<bg()al(OuCE#M5FZa6PFw%EcY#gJvXJxKaTh
z*P_S&U2GCn;pHCR|Air@c|G#Z^I?hsHwSe+gjv<94|o~~>#qoxn8kmFS_k@nU5zrO
z95KU#B$Gb^+7G}n!P#|prsR35v_0fvps(*cuKA%4a8kh0YHQUldJOL>A8-?J4`VTd
zgR*t!wfoi=2RayTKZ^bBHP#Onn-KstLqtTxpuNgIU<VPnXv8cqj}Il{#6tBmHZyy#
zwzjQx&!eHC!3&sI{srpbM8@$TTOLIZB_-t;gbQ@obXAUS1#CYOmX`Y(oN&42f4;D(
zh=72A5+8jgD)kw~Lxz!TMknEeV;E>g1vrbv_>-HNBnb%#ADU$<s8U9JvD%rSQ?K_x
z9Jk-x+FI#|Xzz~8qly&MblDVzYt;*qEvyfuG(S8*lCl?2T2N8EeD=_m3NW6$=D%1#
zw3}Us_{`th&3Nyx&D4QccYs75fwI7E!|!p!1=`$G#P<#k3^b$&hZpHIw+)@u9fn#K
zm`{K8ZY0Z+P8L+e0fxJAu{l6iG0JrO>T)pLLtXU_yz;yXKJ>SDtBjt|va+(F!`jQW
zcy#m`3aI2#6&A_`C}>)jS<GIggj{b9XK~waVF!wMTXD6_)m~53dwRCoOqxYoamgK?
z3#By7-}KJ^_Sgs3#8m5Ab@Lps+x&j|+UXyR0&9)8-k-egho{-Fe?B}o2<H0~`|<aW
z#suxwTrMZn#l^)ZXJ@I~gkUa9z_dT}JTvt4mosBLIZnP$XlxWLGoSv%gul|uIWHoc
z&BXVbG<l(u>fQ@*QYF>Z@xOLE5DX*v(6-tgGg;nmns$8D&1k(hb8TmDFRP^VSxbvl
z;IdQt;#lB*yhXQgs0#V%Q)cu?Fgh>rXsJXVQQp^v`*XD;<D6D29eO>zy?|n++HA52
z3WvjoT+;@r5#ZZ4CmWd1q@<LL48O864!hYuGSt7LRV?Q`@7*7Wr?$4YD?w!i9WxxP
zG_!Xj!Uu;PP+f`WUJZKO`DvK#WuVCBZHf#EWSGllg&D!)dU_f7guPlvM`LJMnAE?Z
z9i_>|X1VP-!revqeMRbmQxY)nEPQ;u^3r^*fScvf#_nv_alb%6@YNGV84n5%CQ`Xy
z=jyx^NEQ6pIAP!8`~w?1dsJS2US25Z2aZrxRkg9Pu`*Gp_b%n<7Ye`^<2G3Z_<SCy
zpB|PIv?Zs>qk~Ew;dvhJGeIJ6ug{NrE$=w5uCFJn9T^9We!E|vvGVYAbw=(F4GwnJ
zxktT7tgPhD%rXRQLaTu1>h??9ZC?3YdQMJYzS}CDQJnvF)lIX&4I-6Pf_{5gVgk3V
z|8jeHOzpbb{MFaZ^?Z-hTsBV5n6UNIaZM`RU`0Ou{~&aM6dM~G!a)~Zy$-kywAED-
zQ332?%;-LQdloEENwJ%3SkuUef*8dkv+1(hle)}IN}&{)R*v<z3a#_FH#_WO_(Vj2
z(#6HKJi08)m=FSi0KS;$yxhXVXTxldWE7*cnM7!#R0r{&_y_|3%xR$x#t>Gl)BI^_
zdRl4pwHY@+GoUe?GoW_qSU=9FceJ-ZoO8_zYAT7jx^h7Pd-WZ9Gj;C-Vppm~vfaNt
z7m6eL_4$6gwHate*Nw(lgYZ#8g7rbF87^`;6wwBUy_O!uOSh9|xL$TV&p%Qt%n6LJ
zDa~Dj2|cy~1U5nAs%qWwhD83d2G8lX%`&%nv;c(^aB!Rtgi4n6w;24%op4aF+Zaf(
z$CEj3KL>1UN8t0d#zF<x-rjy>bo7y?d367}UUxLT<0?1pzgVI|4@}qRDd`G;DbTpM
zm8nwGn%lE!-us*5n`_T|(gC*T$^GG})V$X_#W+F+{6~wUp9DGL)sFcuZ|AD`PjjTB
zg32vD%`@6j7PuUG4dUX}oc1c$3*J9~lZUASFNG4&{J>~LH@B9T9jA7DQD(8bI_J8d
z>r`8XuTBh+$NPUP+Xpt5jMjX^zyK52-+&|x3;3G7y*0=BYZ5>mMa*NDz2^D|u30PY
z>iS`!_ewlVo{phlZ)ZnD5eFE@2Y$1$of<v?c}puRqfUe8&40480+a!rPj?Iy3e_Ct
zSod3LX`#r=%QMLL6M6%-A^|@B%E4m$SOx;$%RnX$jxYn2+?SgF*>@cRVgZkYz`$o<
zlm201Wo-tS=k(Oxw^^qlx7^s$a=j(+)l7wTVi<<>L{INK=dGM98Eij5Wu&Sa$8Eht
z0=5spxs8n}(_7H!&s@ac?(Yy2xU9V`J+G%vPEHn%FTZ5T@63DtNu6+`0eS|U%gO2K
z;qAU>a$DP{_uabVa1HP~upKX289fBq2wR!Z-_!N?#&bT}*xJ$@t#EK~02EPT9%seL
zg*wg1EKoM!>j1RELkPemUAtUQUf;jl+n*>hKwGqmRcH7NdNu-MqkMP<H2*<04$eLA
z&+Qx>7OEIRLqlgg>nC13IMk*pz@SAyNC>P_8Z~O8wwL*I+0jN?Ik^N3cI3zC_8qV^
z1!MA8v;b3YO&lOY{e>f*UIh=R2<aifmPd_GV-Buwd2bGyYfi`1sJpsqlQBtnM0j7X
z1D>!1fqQ;G?@*wI+hgW*xKaqomp!CMU3B?Y$7={_X=$;UVvgm8ke}VFUow6HZZX|y
zjw%ZoC8c&BOn&7!(F!>Z^QjhLOum-M;`-$#KQv#VHxK)`_6~?B0uX`4*VW~&*f`s1
z&=r4Ies(_ZDF9p#CjW&OVBGp+JOvZkPW)kWYLZMB0|U_FK&;)G7<9GBAD*3UUFSb{
zCcsJw`N;H<GEv2b+qjba!CT`1XNx;sW^8I2(}?voG<^$z2w!Q6_Ha_FnC_)2rVk&w
zLy5)@uWvzP@LCqGe@BC#2r73xC|y$0C)pbBo!<EJ2ihnF_C{>qRdV+K$poCfFK;uR
ze39jh=KNn+TODP9mgRSA`Y(vZ;+K6dzt-`W=fCd^$p4G||5vE`AD#hl?H@=&`5#b%
k^`E)<UopAG^8HiZFLbQ;_K_)oIut=nR7wOcr0xBG0N%S|=>Px#

literal 0
HcmV?d00001

diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs
new file mode 100644
index 00000000000..b6f405f5654
--- /dev/null
+++ b/substrate/frame/sassafras/src/lib.rs
@@ -0,0 +1,1081 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Extension module for Sassafras consensus.
+//!
+//! [Sassafras](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS)
+//! is a constant-time block production protocol that aims to ensure that there is
+//! exactly one block produced with constant time intervals rather than multiple or none.
+//!
+//! We run a lottery to distribute block production slots in an epoch and to fix the
+//! order validators produce blocks in, by the beginning of an epoch.
+//!
+//! Each validator signs the same VRF input and publishes the output on-chain. This
+//! value is their lottery ticket that can be validated against their public key.
+//!
+//! We want to keep lottery winners secret, i.e. do not publish their public keys.
+//! At the beginning of the epoch all the validators tickets are published but not
+//! their public keys.
+//!
+//! A valid tickets is validated when an honest validator reclaims it on block
+//! production.
+//!
+//! To prevent submission of fake tickets, resulting in empty slots, the validator
+//! when submitting the ticket accompanies it with a SNARK of the statement: "Here's
+//! my VRF output that has been generated using the given VRF input and my secret
+//! key. I'm not telling you my keys, but my public key is among those of the
+//! nominated validators", that is validated before the lottery.
+//!
+//! To anonymously publish the ticket to the chain a validator sends their tickets
+//! to a random validator who later puts it on-chain as a transaction.
+
+#![deny(warnings)]
+#![warn(unused_must_use, unsafe_code, unused_variables, unused_imports, missing_docs)]
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use log::{debug, error, trace, warn};
+use scale_codec::{Decode, Encode, MaxEncodedLen};
+use scale_info::TypeInfo;
+
+use frame_support::{
+	dispatch::{DispatchResultWithPostInfo, Pays},
+	traits::{Defensive, Get},
+	weights::Weight,
+	BoundedVec, WeakBoundedVec,
+};
+use frame_system::{
+	offchain::{SendTransactionTypes, SubmitTransaction},
+	pallet_prelude::BlockNumberFor,
+};
+use sp_consensus_sassafras::{
+	digests::{ConsensusLog, NextEpochDescriptor, SlotClaim},
+	vrf, AuthorityId, Epoch, EpochConfiguration, Randomness, Slot, TicketBody, TicketEnvelope,
+	TicketId, RANDOMNESS_LENGTH, SASSAFRAS_ENGINE_ID,
+};
+use sp_io::hashing;
+use sp_runtime::{
+	generic::DigestItem,
+	traits::{One, Zero},
+	BoundToRuntimeAppPublic,
+};
+use sp_std::prelude::Vec;
+
+#[cfg(feature = "runtime-benchmarks")]
+mod benchmarking;
+#[cfg(all(feature = "std", test))]
+mod mock;
+#[cfg(all(feature = "std", test))]
+mod tests;
+
+pub mod weights;
+pub use weights::WeightInfo;
+
+pub use pallet::*;
+
+const LOG_TARGET: &str = "sassafras::runtime";
+
+// Contextual string used by the VRF to generate per-block randomness.
+const RANDOMNESS_VRF_CONTEXT: &[u8] = b"SassafrasOnChainRandomness";
+
+// Max length for segments holding unsorted tickets.
+const SEGMENT_MAX_SIZE: u32 = 128;
+
+/// Authorities bounded vector convenience type.
+pub type AuthoritiesVec<T> = WeakBoundedVec<AuthorityId, <T as Config>::MaxAuthorities>;
+
+/// Epoch length defined by the configuration.
+pub type EpochLengthFor<T> = <T as Config>::EpochLength;
+
+/// Tickets metadata.
+#[derive(Debug, Default, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen, Clone, Copy)]
+pub struct TicketsMetadata {
+	/// Number of outstanding next epoch tickets requiring to be sorted.
+	///
+	/// These tickets are held by the [`UnsortedSegments`] storage map in segments
+	/// containing at most `SEGMENT_MAX_SIZE` items.
+	pub unsorted_tickets_count: u32,
+
+	/// Number of tickets available for current and next epoch.
+	///
+	/// These tickets are held by the [`TicketsIds`] storage map.
+	///
+	/// The array entry to be used for the current epoch is computed as epoch index modulo 2.
+	pub tickets_count: [u32; 2],
+}
+
+#[frame_support::pallet]
+pub mod pallet {
+	use super::*;
+	use frame_support::pallet_prelude::*;
+	use frame_system::pallet_prelude::*;
+
+	/// The Sassafras pallet.
+	#[pallet::pallet]
+	pub struct Pallet<T>(_);
+
+	/// Configuration parameters.
+	#[pallet::config]
+	pub trait Config: frame_system::Config + SendTransactionTypes<Call<Self>> {
+		/// Amount of slots that each epoch should last.
+		#[pallet::constant]
+		type EpochLength: Get<u32>;
+
+		/// Max number of authorities allowed.
+		#[pallet::constant]
+		type MaxAuthorities: Get<u32>;
+
+		/// Epoch change trigger.
+		///
+		/// Logic to be triggered on every block to query for whether an epoch has ended
+		/// and to perform the transition to the next epoch.
+		type EpochChangeTrigger: EpochChangeTrigger;
+
+		/// Weight information for all calls of this pallet.
+		type WeightInfo: WeightInfo;
+	}
+
+	/// Sassafras runtime errors.
+	#[pallet::error]
+	pub enum Error<T> {
+		/// Submitted configuration is invalid.
+		InvalidConfiguration,
+	}
+
+	/// Current epoch index.
+	#[pallet::storage]
+	#[pallet::getter(fn epoch_index)]
+	pub type EpochIndex<T> = StorageValue<_, u64, ValueQuery>;
+
+	/// Current epoch authorities.
+	#[pallet::storage]
+	#[pallet::getter(fn authorities)]
+	pub type Authorities<T: Config> = StorageValue<_, AuthoritiesVec<T>, ValueQuery>;
+
+	/// Next epoch authorities.
+	#[pallet::storage]
+	#[pallet::getter(fn next_authorities)]
+	pub type NextAuthorities<T: Config> = StorageValue<_, AuthoritiesVec<T>, ValueQuery>;
+
+	/// First block slot number.
+	///
+	/// As the slots may not be zero-based, we record the slot value for the fist block.
+	/// This allows to always compute relative indices for epochs and slots.
+	#[pallet::storage]
+	#[pallet::getter(fn genesis_slot)]
+	pub type GenesisSlot<T> = StorageValue<_, Slot, ValueQuery>;
+
+	/// Current block slot number.
+	#[pallet::storage]
+	#[pallet::getter(fn current_slot)]
+	pub type CurrentSlot<T> = StorageValue<_, Slot, ValueQuery>;
+
+	/// Current epoch randomness.
+	#[pallet::storage]
+	#[pallet::getter(fn randomness)]
+	pub type CurrentRandomness<T> = StorageValue<_, Randomness, ValueQuery>;
+
+	/// Next epoch randomness.
+	#[pallet::storage]
+	#[pallet::getter(fn next_randomness)]
+	pub type NextRandomness<T> = StorageValue<_, Randomness, ValueQuery>;
+
+	/// Randomness accumulator.
+	///
+	/// Excluded the first imported block, its value is updated on block finalization.
+	#[pallet::storage]
+	#[pallet::getter(fn randomness_accumulator)]
+	pub(crate) type RandomnessAccumulator<T> = StorageValue<_, Randomness, ValueQuery>;
+
+	/// The configuration for the current epoch.
+	#[pallet::storage]
+	#[pallet::getter(fn config)]
+	pub type EpochConfig<T> = StorageValue<_, EpochConfiguration, ValueQuery>;
+
+	/// The configuration for the next epoch.
+	#[pallet::storage]
+	#[pallet::getter(fn next_config)]
+	pub type NextEpochConfig<T> = StorageValue<_, EpochConfiguration>;
+
+	/// Pending epoch configuration change that will be set as `NextEpochConfig` when the next
+	/// epoch is enacted.
+	///
+	/// In other words, a configuration change submitted during epoch N will be enacted on epoch
+	/// N+2. This is to maintain coherence for already submitted tickets for epoch N+1 that where
+	/// computed using configuration parameters stored for epoch N+1.
+	#[pallet::storage]
+	pub type PendingEpochConfigChange<T> = StorageValue<_, EpochConfiguration>;
+
+	/// Stored tickets metadata.
+	#[pallet::storage]
+	pub type TicketsMeta<T> = StorageValue<_, TicketsMetadata, ValueQuery>;
+
+	/// Tickets identifiers map.
+	///
+	/// The map holds tickets ids for the current and next epoch.
+	///
+	/// The key is a tuple composed by:
+	/// - `u8` equal to epoch's index modulo 2;
+	/// - `u32` equal to the ticket's index in a sorted list of epoch's tickets.
+	///
+	/// Epoch X first N-th ticket has key (X mod 2, N)
+	///
+	/// Note that the ticket's index doesn't directly correspond to the slot index within the epoch.
+	/// The assigment is computed dynamically using an *outside-in* strategy.
+	///
+	/// Be aware that entries within this map are never removed, only overwritten.
+	/// Last element index should be fetched from the [`TicketsMeta`] value.
+	#[pallet::storage]
+	pub type TicketsIds<T> = StorageMap<_, Identity, (u8, u32), TicketId>;
+
+	/// Tickets to be used for current and next epoch.
+	#[pallet::storage]
+	pub type TicketsData<T> = StorageMap<_, Identity, TicketId, TicketBody>;
+
+	/// Next epoch tickets unsorted segments.
+	///
+	/// Contains lists of tickets where each list represents a batch of tickets
+	/// received via the `submit_tickets` extrinsic.
+	///
+	/// Each segment has max length [`SEGMENT_MAX_SIZE`].
+	#[pallet::storage]
+	pub type UnsortedSegments<T: Config> =
+		StorageMap<_, Identity, u32, BoundedVec<TicketId, ConstU32<SEGMENT_MAX_SIZE>>, ValueQuery>;
+
+	/// The most recently set of tickets which are candidates to become the next
+	/// epoch tickets.
+	#[pallet::storage]
+	pub type SortedCandidates<T> =
+		StorageValue<_, BoundedVec<TicketId, EpochLengthFor<T>>, ValueQuery>;
+
+	/// Parameters used to construct the epoch's ring verifier.
+	///
+	/// In practice: Updatable Universal Reference String and the seed.
+	#[pallet::storage]
+	#[pallet::getter(fn ring_context)]
+	pub type RingContext<T: Config> = StorageValue<_, vrf::RingContext>;
+
+	/// Ring verifier data for the current epoch.
+	#[pallet::storage]
+	pub type RingVerifierData<T: Config> = StorageValue<_, vrf::RingVerifierData>;
+
+	/// Slot claim vrf-preoutput used to generate per-slot randomness.
+	///
+	/// The value is ephemeral and is cleared on block finalization.
+	#[pallet::storage]
+	pub(crate) type ClaimTemporaryData<T> = StorageValue<_, vrf::VrfOutput>;
+
+	/// Genesis configuration for Sassafras protocol.
+	#[pallet::genesis_config]
+	#[derive(frame_support::DefaultNoBound)]
+	pub struct GenesisConfig<T: Config> {
+		/// Genesis authorities.
+		pub authorities: Vec<AuthorityId>,
+		/// Genesis epoch configuration.
+		pub epoch_config: EpochConfiguration,
+		/// Phantom config
+		#[serde(skip)]
+		pub _phantom: sp_std::marker::PhantomData<T>,
+	}
+
+	#[pallet::genesis_build]
+	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
+		fn build(&self) {
+			EpochConfig::<T>::put(self.epoch_config);
+			Pallet::<T>::genesis_authorities_initialize(&self.authorities);
+
+			#[cfg(feature = "construct-dummy-ring-context")]
+			{
+				debug!(target: LOG_TARGET, "Constructing dummy ring context");
+				let ring_ctx = vrf::RingContext::new_testing();
+				RingContext::<T>::put(ring_ctx);
+				Pallet::<T>::update_ring_verifier(&self.authorities);
+			}
+		}
+	}
+
+	#[pallet::hooks]
+	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
+		fn on_initialize(block_num: BlockNumberFor<T>) -> Weight {
+			debug_assert_eq!(block_num, frame_system::Pallet::<T>::block_number());
+
+			let claim = <frame_system::Pallet<T>>::digest()
+				.logs
+				.iter()
+				.find_map(|item| item.pre_runtime_try_to::<SlotClaim>(&SASSAFRAS_ENGINE_ID))
+				.expect("Valid block must have a slot claim. qed");
+
+			CurrentSlot::<T>::put(claim.slot);
+
+			if block_num == One::one() {
+				Self::post_genesis_initialize(claim.slot);
+			}
+
+			let randomness_output = claim
+				.vrf_signature
+				.outputs
+				.get(0)
+				.expect("Valid claim must have vrf signature; qed");
+			ClaimTemporaryData::<T>::put(randomness_output);
+
+			let trigger_weight = T::EpochChangeTrigger::trigger::<T>(block_num);
+
+			T::WeightInfo::on_initialize() + trigger_weight
+		}
+
+		fn on_finalize(_: BlockNumberFor<T>) {
+			// At the end of the block, we can safely include the current slot randomness
+			// to the accumulator. If we've determined that this block was the first in
+			// a new epoch, the changeover logic has already occurred at this point
+			// (i.e. `enact_epoch_change` has already been called).
+			let randomness_input = vrf::slot_claim_input(
+				&Self::randomness(),
+				CurrentSlot::<T>::get(),
+				EpochIndex::<T>::get(),
+			);
+			let randomness_output = ClaimTemporaryData::<T>::take()
+				.expect("Unconditionally populated in `on_initialize`; `on_finalize` is always called after; qed");
+			let randomness = randomness_output
+				.make_bytes::<RANDOMNESS_LENGTH>(RANDOMNESS_VRF_CONTEXT, &randomness_input);
+			Self::deposit_slot_randomness(&randomness);
+
+			// Check if we are in the epoch's second half.
+			// If so, start sorting the next epoch tickets.
+			let epoch_length = T::EpochLength::get();
+			let current_slot_idx = Self::current_slot_index();
+			if current_slot_idx >= epoch_length / 2 {
+				let mut metadata = TicketsMeta::<T>::get();
+				if metadata.unsorted_tickets_count != 0 {
+					let next_epoch_idx = EpochIndex::<T>::get() + 1;
+					let next_epoch_tag = (next_epoch_idx & 1) as u8;
+					let slots_left = epoch_length.checked_sub(current_slot_idx).unwrap_or(1);
+					Self::sort_segments(
+						metadata
+							.unsorted_tickets_count
+							.div_ceil(SEGMENT_MAX_SIZE * slots_left as u32),
+						next_epoch_tag,
+						&mut metadata,
+					);
+					TicketsMeta::<T>::set(metadata);
+				}
+			}
+		}
+	}
+
+	#[pallet::call]
+	impl<T: Config> Pallet<T> {
+		/// Submit next epoch tickets candidates.
+		///
+		/// The number of tickets allowed to be submitted in one call is equal to the epoch length.
+		#[pallet::call_index(0)]
+		#[pallet::weight(T::WeightInfo::submit_tickets(tickets.len() as u32))]
+		pub fn submit_tickets(
+			origin: OriginFor<T>,
+			tickets: BoundedVec<TicketEnvelope, EpochLengthFor<T>>,
+		) -> DispatchResultWithPostInfo {
+			ensure_none(origin)?;
+
+			debug!(target: LOG_TARGET, "Received {} tickets", tickets.len());
+
+			let epoch_length = T::EpochLength::get();
+			let current_slot_idx = Self::current_slot_index();
+			if current_slot_idx > epoch_length / 2 {
+				warn!(target: LOG_TARGET, "Tickets shall be submitted in the first epoch half",);
+				return Err("Tickets shall be submitted in the first epoch half".into())
+			}
+
+			let Some(verifier) = RingVerifierData::<T>::get().map(|v| v.into()) else {
+				warn!(target: LOG_TARGET, "Ring verifier key not initialized");
+				return Err("Ring verifier key not initialized".into())
+			};
+
+			let next_authorities = Self::next_authorities();
+
+			// Compute tickets threshold
+			let next_config = Self::next_config().unwrap_or_else(|| Self::config());
+			let ticket_threshold = sp_consensus_sassafras::ticket_id_threshold(
+				next_config.redundancy_factor,
+				epoch_length as u32,
+				next_config.attempts_number,
+				next_authorities.len() as u32,
+			);
+
+			// Get next epoch params
+			let randomness = NextRandomness::<T>::get();
+			let epoch_idx = EpochIndex::<T>::get() + 1;
+
+			let mut valid_tickets = BoundedVec::with_bounded_capacity(tickets.len());
+
+			for ticket in tickets {
+				debug!(target: LOG_TARGET, "Checking ring proof");
+
+				let Some(ticket_id_output) = ticket.signature.outputs.get(0) else {
+					debug!(target: LOG_TARGET, "Missing ticket vrf output from ring signature");
+					continue
+				};
+				let ticket_id_input =
+					vrf::ticket_id_input(&randomness, ticket.body.attempt_idx, epoch_idx);
+
+				// Check threshold constraint
+				let ticket_id = vrf::make_ticket_id(&ticket_id_input, &ticket_id_output);
+				if ticket_id >= ticket_threshold {
+					debug!(target: LOG_TARGET, "Ignoring ticket over threshold ({:032x} >= {:032x})", ticket_id, ticket_threshold);
+					continue
+				}
+
+				// Check for duplicates
+				if TicketsData::<T>::contains_key(ticket_id) {
+					debug!(target: LOG_TARGET, "Ignoring duplicate ticket ({:032x})", ticket_id);
+					continue
+				}
+
+				// Check ring signature
+				let sign_data = vrf::ticket_body_sign_data(&ticket.body, ticket_id_input);
+				if !ticket.signature.ring_vrf_verify(&sign_data, &verifier) {
+					debug!(target: LOG_TARGET, "Proof verification failure for ticket ({:032x})", ticket_id);
+					continue
+				}
+
+				if let Ok(_) = valid_tickets.try_push(ticket_id).defensive_proof(
+					"Input segment has same length as bounded destination vector; qed",
+				) {
+					TicketsData::<T>::set(ticket_id, Some(ticket.body));
+				}
+			}
+
+			if !valid_tickets.is_empty() {
+				Self::append_tickets(valid_tickets);
+			}
+
+			Ok(Pays::No.into())
+		}
+
+		/// Plan an epoch configuration change.
+		///
+		/// The epoch configuration change is recorded and will be announced at the begining
+		/// of the next epoch together with next epoch authorities information.
+		/// In other words, the configuration will be enacted one epoch later.
+		///
+		/// Multiple calls to this method will replace any existing planned config change
+		/// that has not been enacted yet.
+		#[pallet::call_index(1)]
+		#[pallet::weight(T::WeightInfo::plan_config_change())]
+		pub fn plan_config_change(
+			origin: OriginFor<T>,
+			config: EpochConfiguration,
+		) -> DispatchResult {
+			ensure_root(origin)?;
+
+			ensure!(
+				config.redundancy_factor != 0 && config.attempts_number != 0,
+				Error::<T>::InvalidConfiguration
+			);
+			PendingEpochConfigChange::<T>::put(config);
+			Ok(())
+		}
+	}
+
+	#[pallet::validate_unsigned]
+	impl<T: Config> ValidateUnsigned for Pallet<T> {
+		type Call = Call<T>;
+
+		fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
+			let Call::submit_tickets { tickets } = call else {
+				return InvalidTransaction::Call.into()
+			};
+
+			// Discard tickets not coming from the local node or that are not included in a block
+			if source == TransactionSource::External {
+				warn!(
+					target: LOG_TARGET,
+					"Rejecting unsigned `submit_tickets` transaction from external source",
+				);
+				return InvalidTransaction::BadSigner.into()
+			}
+
+			// Current slot should be less than half of epoch length.
+			let epoch_length = T::EpochLength::get();
+			let current_slot_idx = Self::current_slot_index();
+			if current_slot_idx > epoch_length / 2 {
+				warn!(target: LOG_TARGET, "Tickets shall be proposed in the first epoch half",);
+				return InvalidTransaction::Stale.into()
+			}
+
+			// This should be set such that it is discarded after the first epoch half
+			let tickets_longevity = epoch_length / 2 - current_slot_idx;
+			let tickets_tag = tickets.using_encoded(|bytes| hashing::blake2_256(bytes));
+
+			ValidTransaction::with_tag_prefix("Sassafras")
+				.priority(TransactionPriority::max_value())
+				.longevity(tickets_longevity as u64)
+				.and_provides(tickets_tag)
+				.propagate(true)
+				.build()
+		}
+	}
+}
+
+// Inherent methods
+impl<T: Config> Pallet<T> {
+	/// Determine whether an epoch change should take place at this block.
+	///
+	/// Assumes that initialization has already taken place.
+	pub(crate) fn should_end_epoch(block_num: BlockNumberFor<T>) -> bool {
+		// The epoch has technically ended during the passage of time between this block and the
+		// last, but we have to "end" the epoch now, since there is no earlier possible block we
+		// could have done it.
+		//
+		// The exception is for block 1: the genesis has slot 0, so we treat epoch 0 as having
+		// started at the slot of block 1. We want to use the same randomness and validator set as
+		// signalled in the genesis, so we don't rotate the epoch.
+		block_num > One::one() && Self::current_slot_index() >= T::EpochLength::get()
+	}
+
+	/// Current slot index relative to the current epoch.
+	fn current_slot_index() -> u32 {
+		Self::slot_index(CurrentSlot::<T>::get())
+	}
+
+	/// Slot index relative to the current epoch.
+	fn slot_index(slot: Slot) -> u32 {
+		slot.checked_sub(*Self::current_epoch_start())
+			.and_then(|v| v.try_into().ok())
+			.unwrap_or(u32::MAX)
+	}
+
+	/// Finds the start slot of the current epoch.
+	///
+	/// Only guaranteed to give correct results after `initialize` of the first
+	/// block in the chain (as its result is based off of `GenesisSlot`).
+	fn current_epoch_start() -> Slot {
+		Self::epoch_start(EpochIndex::<T>::get())
+	}
+
+	/// Get the epoch's first slot.
+	fn epoch_start(epoch_index: u64) -> Slot {
+		const PROOF: &str = "slot number is u64; it should relate in some way to wall clock time; \
+							 if u64 is not enough we should crash for safety; qed.";
+
+		let epoch_start = epoch_index.checked_mul(T::EpochLength::get() as u64).expect(PROOF);
+		GenesisSlot::<T>::get().checked_add(epoch_start).expect(PROOF).into()
+	}
+
+	pub(crate) fn update_ring_verifier(authorities: &[AuthorityId]) {
+		debug!(target: LOG_TARGET, "Loading ring context");
+		let Some(ring_ctx) = RingContext::<T>::get() else {
+			debug!(target: LOG_TARGET, "Ring context not initialized");
+			return
+		};
+
+		let pks: Vec<_> = authorities.iter().map(|auth| *auth.as_ref()).collect();
+
+		debug!(target: LOG_TARGET, "Building ring verifier (ring size: {})", pks.len());
+		let verifier_data = ring_ctx
+			.verifier_data(&pks)
+			.expect("Failed to build ring verifier. This is a bug");
+
+		RingVerifierData::<T>::put(verifier_data);
+	}
+
+	/// Enact an epoch change.
+	///
+	/// WARNING: Should be called on every block once and if and only if [`should_end_epoch`]
+	/// has returned `true`.
+	///
+	/// If we detect one or more skipped epochs the policy is to use the authorities and values
+	/// from the first skipped epoch. The tickets data is invalidated.
+	pub(crate) fn enact_epoch_change(
+		authorities: WeakBoundedVec<AuthorityId, T::MaxAuthorities>,
+		next_authorities: WeakBoundedVec<AuthorityId, T::MaxAuthorities>,
+	) {
+		if next_authorities != authorities {
+			Self::update_ring_verifier(&next_authorities);
+		}
+
+		// Update authorities
+		Authorities::<T>::put(&authorities);
+		NextAuthorities::<T>::put(&next_authorities);
+
+		// Update epoch index
+		let mut epoch_idx = EpochIndex::<T>::get() + 1;
+
+		let slot_idx = CurrentSlot::<T>::get().saturating_sub(Self::epoch_start(epoch_idx));
+		if slot_idx >= T::EpochLength::get() {
+			// Detected one or more skipped epochs, clear tickets data and recompute epoch index.
+			Self::reset_tickets_data();
+			let skipped_epochs = *slot_idx / T::EpochLength::get() as u64;
+			epoch_idx += skipped_epochs;
+			warn!(
+				target: LOG_TARGET,
+				"Detected {} skipped epochs, resuming from epoch {}",
+				skipped_epochs,
+				epoch_idx
+			);
+		}
+
+		let mut metadata = TicketsMeta::<T>::get();
+		let mut metadata_dirty = false;
+
+		EpochIndex::<T>::put(epoch_idx);
+
+		let next_epoch_idx = epoch_idx + 1;
+
+		// Updates current epoch randomness and computes the *next* epoch randomness.
+		let next_randomness = Self::update_epoch_randomness(next_epoch_idx);
+
+		if let Some(config) = NextEpochConfig::<T>::take() {
+			EpochConfig::<T>::put(config);
+		}
+
+		let next_config = PendingEpochConfigChange::<T>::take();
+		if let Some(next_config) = next_config {
+			NextEpochConfig::<T>::put(next_config);
+		}
+
+		// After we update the current epoch, we signal the *next* epoch change
+		// so that nodes can track changes.
+		let next_epoch = NextEpochDescriptor {
+			randomness: next_randomness,
+			authorities: next_authorities.into_inner(),
+			config: next_config,
+		};
+		Self::deposit_next_epoch_descriptor_digest(next_epoch);
+
+		let epoch_tag = (epoch_idx & 1) as u8;
+
+		// Optionally finish sorting
+		if metadata.unsorted_tickets_count != 0 {
+			Self::sort_segments(u32::MAX, epoch_tag, &mut metadata);
+			metadata_dirty = true;
+		}
+
+		// Clear the "prev ≡ next (mod 2)" epoch tickets counter and bodies.
+		// Ids are left since are just cyclically overwritten on-the-go.
+		let prev_epoch_tag = epoch_tag ^ 1;
+		let prev_epoch_tickets_count = &mut metadata.tickets_count[prev_epoch_tag as usize];
+		if *prev_epoch_tickets_count != 0 {
+			for idx in 0..*prev_epoch_tickets_count {
+				if let Some(ticket_id) = TicketsIds::<T>::get((prev_epoch_tag, idx)) {
+					TicketsData::<T>::remove(ticket_id);
+				}
+			}
+			*prev_epoch_tickets_count = 0;
+			metadata_dirty = true;
+		}
+
+		if metadata_dirty {
+			TicketsMeta::<T>::set(metadata);
+		}
+	}
+
+	// Call this function on epoch change to enact current epoch randomness.
+	//
+	// Returns the next epoch randomness.
+	fn update_epoch_randomness(next_epoch_index: u64) -> Randomness {
+		let curr_epoch_randomness = NextRandomness::<T>::get();
+		CurrentRandomness::<T>::put(curr_epoch_randomness);
+
+		let accumulator = RandomnessAccumulator::<T>::get();
+
+		let mut buf = [0; RANDOMNESS_LENGTH + 8];
+		buf[..RANDOMNESS_LENGTH].copy_from_slice(&accumulator[..]);
+		buf[RANDOMNESS_LENGTH..].copy_from_slice(&next_epoch_index.to_le_bytes());
+
+		let next_randomness = hashing::blake2_256(&buf);
+		NextRandomness::<T>::put(&next_randomness);
+
+		next_randomness
+	}
+
+	// Deposit per-slot randomness.
+	fn deposit_slot_randomness(randomness: &Randomness) {
+		let accumulator = RandomnessAccumulator::<T>::get();
+
+		let mut buf = [0; 2 * RANDOMNESS_LENGTH];
+		buf[..RANDOMNESS_LENGTH].copy_from_slice(&accumulator[..]);
+		buf[RANDOMNESS_LENGTH..].copy_from_slice(&randomness[..]);
+
+		let accumulator = hashing::blake2_256(&buf);
+		RandomnessAccumulator::<T>::put(accumulator);
+	}
+
+	// Deposit next epoch descriptor in the block header digest.
+	fn deposit_next_epoch_descriptor_digest(desc: NextEpochDescriptor) {
+		let item = ConsensusLog::NextEpochData(desc);
+		let log = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, item.encode());
+		<frame_system::Pallet<T>>::deposit_log(log)
+	}
+
+	// Initialize authorities on genesis phase.
+	//
+	// Genesis authorities may have been initialized via other means (e.g. via session pallet).
+	//
+	// If this function has already been called with some authorities, then the new list
+	// should match the previously set one.
+	fn genesis_authorities_initialize(authorities: &[AuthorityId]) {
+		let prev_authorities = Authorities::<T>::get();
+
+		if !prev_authorities.is_empty() {
+			// This function has already been called.
+			if prev_authorities.as_slice() == authorities {
+				return
+			} else {
+				panic!("Authorities were already initialized");
+			}
+		}
+
+		let authorities = WeakBoundedVec::try_from(authorities.to_vec())
+			.expect("Initial number of authorities should be lower than T::MaxAuthorities");
+		Authorities::<T>::put(&authorities);
+		NextAuthorities::<T>::put(&authorities);
+	}
+
+	// Method to be called on first block `on_initialize` to properly populate some key parameters.
+	fn post_genesis_initialize(slot: Slot) {
+		// Keep track of the actual first slot used (may not be zero based).
+		GenesisSlot::<T>::put(slot);
+
+		// Properly initialize randomness using genesis hash and current slot.
+		// This is important to guarantee that a different set of tickets are produced for:
+		// - different chains which share the same ring parameters and
+		// - same chain started with a different slot base.
+		let genesis_hash = frame_system::Pallet::<T>::parent_hash();
+		let mut buf = genesis_hash.as_ref().to_vec();
+		buf.extend_from_slice(&slot.to_le_bytes());
+		let randomness = hashing::blake2_256(buf.as_slice());
+		RandomnessAccumulator::<T>::put(randomness);
+
+		let next_randoness = Self::update_epoch_randomness(1);
+
+		// Deposit a log as this is the first block in first epoch.
+		let next_epoch = NextEpochDescriptor {
+			randomness: next_randoness,
+			authorities: Self::next_authorities().into_inner(),
+			config: None,
+		};
+		Self::deposit_next_epoch_descriptor_digest(next_epoch);
+	}
+
+	/// Current epoch information.
+	pub fn current_epoch() -> Epoch {
+		let index = EpochIndex::<T>::get();
+		Epoch {
+			index,
+			start: Self::epoch_start(index),
+			length: T::EpochLength::get(),
+			authorities: Self::authorities().into_inner(),
+			randomness: Self::randomness(),
+			config: Self::config(),
+		}
+	}
+
+	/// Next epoch information.
+	pub fn next_epoch() -> Epoch {
+		let index = EpochIndex::<T>::get() + 1;
+		Epoch {
+			index,
+			start: Self::epoch_start(index),
+			length: T::EpochLength::get(),
+			authorities: Self::next_authorities().into_inner(),
+			randomness: Self::next_randomness(),
+			config: Self::next_config().unwrap_or_else(|| Self::config()),
+		}
+	}
+
+	/// Fetch expected ticket-id for the given slot according to an "outside-in" sorting strategy.
+	///
+	/// Given an ordered sequence of tickets [t0, t1, t2, ..., tk] to be assigned to n slots,
+	/// with n >= k, then the tickets are assigned to the slots according to the following
+	/// strategy:
+	///
+	/// slot-index  : [ 0,  1,  2, ............ , n ]
+	/// tickets     : [ t1, t3, t5, ... , t4, t2, t0 ].
+	///
+	/// With slot-index computed as `epoch_start() - slot`.
+	///
+	/// If `slot` value falls within the current epoch then we fetch tickets from the current epoch
+	/// tickets list.
+	///
+	/// If `slot` value falls within the next epoch then we fetch tickets from the next epoch
+	/// tickets ids list. Note that in this case we may have not finished receiving all the tickets
+	/// for that epoch yet. The next epoch tickets should be considered "stable" only after the
+	/// current epoch first half slots were elapsed (see `submit_tickets_unsigned_extrinsic`).
+	///
+	/// Returns `None` if, according to the sorting strategy, there is no ticket associated to the
+	/// specified slot-index (happens if a ticket falls in the middle of an epoch and n > k),
+	/// or if the slot falls beyond the next epoch.
+	///
+	/// Before importing the first block this returns `None`.
+	pub fn slot_ticket_id(slot: Slot) -> Option<TicketId> {
+		if frame_system::Pallet::<T>::block_number().is_zero() {
+			return None
+		}
+		let epoch_idx = EpochIndex::<T>::get();
+		let epoch_len = T::EpochLength::get();
+		let mut slot_idx = Self::slot_index(slot);
+		let mut metadata = TicketsMeta::<T>::get();
+
+		let get_ticket_idx = |slot_idx| {
+			let ticket_idx = if slot_idx < epoch_len / 2 {
+				2 * slot_idx + 1
+			} else {
+				2 * (epoch_len - (slot_idx + 1))
+			};
+			debug!(
+				target: LOG_TARGET,
+				"slot-idx {} <-> ticket-idx {}",
+				slot_idx,
+				ticket_idx
+			);
+			ticket_idx as u32
+		};
+
+		let mut epoch_tag = (epoch_idx & 1) as u8;
+
+		if epoch_len <= slot_idx && slot_idx < 2 * epoch_len {
+			// Try to get a ticket for the next epoch. Since its state values were not enacted yet,
+			// we may have to finish sorting the tickets.
+			epoch_tag ^= 1;
+			slot_idx -= epoch_len;
+			if metadata.unsorted_tickets_count != 0 {
+				Self::sort_segments(u32::MAX, epoch_tag, &mut metadata);
+				TicketsMeta::<T>::set(metadata);
+			}
+		} else if slot_idx >= 2 * epoch_len {
+			return None
+		}
+
+		let ticket_idx = get_ticket_idx(slot_idx);
+		if ticket_idx < metadata.tickets_count[epoch_tag as usize] {
+			TicketsIds::<T>::get((epoch_tag, ticket_idx))
+		} else {
+			None
+		}
+	}
+
+	/// Returns ticket id and data associated with the given `slot`.
+	///
+	/// Refer to the `slot_ticket_id` documentation for the slot-ticket association
+	/// criteria.
+	pub fn slot_ticket(slot: Slot) -> Option<(TicketId, TicketBody)> {
+		Self::slot_ticket_id(slot).and_then(|id| TicketsData::<T>::get(id).map(|body| (id, body)))
+	}
+
+	// Sort and truncate candidate tickets, cleanup storage.
+	fn sort_and_truncate(candidates: &mut Vec<u128>, max_tickets: usize) -> u128 {
+		candidates.sort_unstable();
+		candidates.drain(max_tickets..).for_each(TicketsData::<T>::remove);
+		candidates[max_tickets - 1]
+	}
+
+	/// Sort the tickets which belong to the epoch with the specified `epoch_tag`.
+	///
+	/// At most `max_segments` are taken from the `UnsortedSegments` structure.
+	///
+	/// The tickets of the removed segments are merged with the tickets on the `SortedCandidates`
+	/// which is then sorted an truncated to contain at most `MaxTickets` entries.
+	///
+	/// If all the entries in `UnsortedSegments` are consumed, then `SortedCandidates` is elected
+	/// as the next epoch tickets, else it is saved to be used by next calls of this function.
+	pub(crate) fn sort_segments(max_segments: u32, epoch_tag: u8, metadata: &mut TicketsMetadata) {
+		let unsorted_segments_count = metadata.unsorted_tickets_count.div_ceil(SEGMENT_MAX_SIZE);
+		let max_segments = max_segments.min(unsorted_segments_count);
+		let max_tickets = Self::epoch_length() as usize;
+
+		// Fetch the sorted candidates (if any).
+		let mut candidates = SortedCandidates::<T>::take().into_inner();
+
+		// There is an upper bound to check only if we already sorted the max number
+		// of allowed tickets.
+		let mut upper_bound = *candidates.get(max_tickets - 1).unwrap_or(&TicketId::MAX);
+
+		let mut require_sort = false;
+
+		// Consume at most `max_segments` segments.
+		// During the process remove every stale ticket from `TicketsData` storage.
+		for segment_idx in (0..unsorted_segments_count).rev().take(max_segments as usize) {
+			let segment = UnsortedSegments::<T>::take(segment_idx);
+			metadata.unsorted_tickets_count -= segment.len() as u32;
+
+			// Push only ids with a value less than the current `upper_bound`.
+			let prev_len = candidates.len();
+			for ticket_id in segment {
+				if ticket_id < upper_bound {
+					candidates.push(ticket_id);
+				} else {
+					TicketsData::<T>::remove(ticket_id);
+				}
+			}
+			require_sort = candidates.len() != prev_len;
+
+			// As we approach the tail of the segments buffer the `upper_bound` value is expected
+			// to decrease (fast). We thus expect the number of tickets pushed into the
+			// `candidates` vector to follow an exponential drop.
+			//
+			// Given this, sorting and truncating after processing each segment may be an overkill
+			// as we may find pushing few tickets more and more often. Is preferable to perform
+			// the sort and truncation operations only when we reach some bigger threshold
+			// (currently set as twice the capacity of `SortCandidate`).
+			//
+			// The more is the protocol's redundancy factor (i.e. the ratio between tickets allowed
+			// to be submitted and the epoch length) the more this check becomes relevant.
+			if candidates.len() > 2 * max_tickets {
+				upper_bound = Self::sort_and_truncate(&mut candidates, max_tickets);
+				require_sort = false;
+			}
+		}
+
+		if candidates.len() > max_tickets {
+			Self::sort_and_truncate(&mut candidates, max_tickets);
+		} else if require_sort {
+			candidates.sort_unstable();
+		}
+
+		if metadata.unsorted_tickets_count == 0 {
+			// Sorting is over, write to next epoch map.
+			candidates.iter().enumerate().for_each(|(i, id)| {
+				TicketsIds::<T>::insert((epoch_tag, i as u32), id);
+			});
+			metadata.tickets_count[epoch_tag as usize] = candidates.len() as u32;
+		} else {
+			// Keep the partial result for the next calls.
+			SortedCandidates::<T>::set(BoundedVec::truncate_from(candidates));
+		}
+	}
+
+	/// Append a set of tickets to the segments map.
+	pub(crate) fn append_tickets(mut tickets: BoundedVec<TicketId, EpochLengthFor<T>>) {
+		debug!(target: LOG_TARGET, "Appending batch with {} tickets", tickets.len());
+		tickets.iter().for_each(|t| trace!(target: LOG_TARGET, "  + {t:032x}"));
+
+		let mut metadata = TicketsMeta::<T>::get();
+		let mut segment_idx = metadata.unsorted_tickets_count / SEGMENT_MAX_SIZE;
+
+		while !tickets.is_empty() {
+			let rem = metadata.unsorted_tickets_count % SEGMENT_MAX_SIZE;
+			let to_be_added = tickets.len().min((SEGMENT_MAX_SIZE - rem) as usize);
+
+			let mut segment = UnsortedSegments::<T>::get(segment_idx);
+			let _ = segment
+				.try_extend(tickets.drain(..to_be_added))
+				.defensive_proof("We don't add more than `SEGMENT_MAX_SIZE` and this is the maximum bound for the vector.");
+			UnsortedSegments::<T>::insert(segment_idx, segment);
+
+			metadata.unsorted_tickets_count += to_be_added as u32;
+			segment_idx += 1;
+		}
+
+		TicketsMeta::<T>::set(metadata);
+	}
+
+	/// Remove all tickets related data.
+	///
+	/// May not be efficient as the calling places may repeat some of this operations
+	/// but is a very extraordinary operation (hopefully never happens in production)
+	/// and better safe than sorry.
+	fn reset_tickets_data() {
+		let metadata = TicketsMeta::<T>::get();
+
+		// Remove even/odd-epoch data.
+		for epoch_tag in 0..=1 {
+			for idx in 0..metadata.tickets_count[epoch_tag] {
+				if let Some(id) = TicketsIds::<T>::get((epoch_tag as u8, idx)) {
+					TicketsData::<T>::remove(id);
+				}
+			}
+		}
+
+		// Remove all unsorted tickets segments.
+		let segments_count = metadata.unsorted_tickets_count.div_ceil(SEGMENT_MAX_SIZE);
+		(0..segments_count).for_each(UnsortedSegments::<T>::remove);
+
+		// Reset sorted candidates
+		SortedCandidates::<T>::kill();
+
+		// Reset tickets metadata
+		TicketsMeta::<T>::kill();
+	}
+
+	/// Submit next epoch validator tickets via an unsigned extrinsic constructed with a call to
+	/// `submit_unsigned_transaction`.
+	///
+	/// The submitted tickets are added to the next epoch outstanding tickets as long as the
+	/// extrinsic is called within the first half of the epoch. Tickets received during the
+	/// second half are dropped.
+	pub fn submit_tickets_unsigned_extrinsic(tickets: Vec<TicketEnvelope>) -> bool {
+		let tickets = BoundedVec::truncate_from(tickets);
+		let call = Call::submit_tickets { tickets };
+		match SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call.into()) {
+			Ok(_) => true,
+			Err(e) => {
+				error!(target: LOG_TARGET, "Error submitting tickets {:?}", e);
+				false
+			},
+		}
+	}
+
+	/// Epoch length
+	pub fn epoch_length() -> u32 {
+		T::EpochLength::get()
+	}
+}
+
+/// Trigger an epoch change, if any should take place.
+pub trait EpochChangeTrigger {
+	/// May trigger an epoch change, if any should take place.
+	///
+	/// Returns an optional `Weight` if epoch change has been triggered.
+	///
+	/// This should be called during every block, after initialization is done.
+	fn trigger<T: Config>(_: BlockNumberFor<T>) -> Weight;
+}
+
+/// An `EpochChangeTrigger` which does nothing.
+///
+/// In practice this means that the epoch change logic is left to some external component
+/// (e.g. pallet-session).
+pub struct EpochChangeExternalTrigger;
+
+impl EpochChangeTrigger for EpochChangeExternalTrigger {
+	fn trigger<T: Config>(_: BlockNumberFor<T>) -> Weight {
+		// nothing - trigger is external.
+		Weight::zero()
+	}
+}
+
+/// An `EpochChangeTrigger` which recycle the same authorities set forever.
+///
+/// The internal trigger should only be used when no other module is responsible for
+/// changing authority set.
+pub struct EpochChangeInternalTrigger;
+
+impl EpochChangeTrigger for EpochChangeInternalTrigger {
+	fn trigger<T: Config>(block_num: BlockNumberFor<T>) -> Weight {
+		if Pallet::<T>::should_end_epoch(block_num) {
+			let authorities = Pallet::<T>::next_authorities();
+			let next_authorities = authorities.clone();
+			let len = next_authorities.len() as u32;
+			Pallet::<T>::enact_epoch_change(authorities, next_authorities);
+			T::WeightInfo::enact_epoch_change(len, T::EpochLength::get())
+		} else {
+			Weight::zero()
+		}
+	}
+}
+
+impl<T: Config> BoundToRuntimeAppPublic for Pallet<T> {
+	type Public = AuthorityId;
+}
diff --git a/substrate/frame/sassafras/src/mock.rs b/substrate/frame/sassafras/src/mock.rs
new file mode 100644
index 00000000000..b700207c499
--- /dev/null
+++ b/substrate/frame/sassafras/src/mock.rs
@@ -0,0 +1,343 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Test utilities for Sassafras pallet.
+
+use crate::{self as pallet_sassafras, EpochChangeInternalTrigger, *};
+
+use frame_support::{
+	derive_impl,
+	traits::{ConstU32, OnFinalize, OnInitialize},
+};
+use sp_consensus_sassafras::{
+	digests::SlotClaim,
+	vrf::{RingProver, VrfSignature},
+	AuthorityIndex, AuthorityPair, EpochConfiguration, Slot, TicketBody, TicketEnvelope, TicketId,
+};
+use sp_core::{
+	crypto::{ByteArray, Pair, UncheckedFrom, VrfSecret, Wraps},
+	ed25519::Public as EphemeralPublic,
+	H256, U256,
+};
+use sp_runtime::{
+	testing::{Digest, DigestItem, Header, TestXt},
+	BuildStorage,
+};
+
+const LOG_TARGET: &str = "sassafras::tests";
+
+const EPOCH_LENGTH: u32 = 10;
+const MAX_AUTHORITIES: u32 = 100;
+
+#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)]
+impl frame_system::Config for Test {
+	type Block = frame_system::mocking::MockBlock<Test>;
+}
+
+impl<C> frame_system::offchain::SendTransactionTypes<C> for Test
+where
+	RuntimeCall: From<C>,
+{
+	type OverarchingCall = RuntimeCall;
+	type Extrinsic = TestXt<RuntimeCall, ()>;
+}
+
+impl pallet_sassafras::Config for Test {
+	type EpochLength = ConstU32<EPOCH_LENGTH>;
+	type MaxAuthorities = ConstU32<MAX_AUTHORITIES>;
+	type EpochChangeTrigger = EpochChangeInternalTrigger;
+	type WeightInfo = ();
+}
+
+frame_support::construct_runtime!(
+	pub enum Test {
+		System: frame_system,
+		Sassafras: pallet_sassafras,
+	}
+);
+
+// Default used for most of the tests.
+//
+// The redundancy factor has been set to max value to accept all submitted
+// tickets without worrying about the threshold.
+pub const TEST_EPOCH_CONFIGURATION: EpochConfiguration =
+	EpochConfiguration { redundancy_factor: u32::MAX, attempts_number: 5 };
+
+/// Build and returns test storage externalities
+pub fn new_test_ext(authorities_len: usize) -> sp_io::TestExternalities {
+	new_test_ext_with_pairs(authorities_len, false).1
+}
+
+/// Build and returns test storage externalities and authority set pairs used
+/// by Sassafras genesis configuration.
+pub fn new_test_ext_with_pairs(
+	authorities_len: usize,
+	with_ring_context: bool,
+) -> (Vec<AuthorityPair>, sp_io::TestExternalities) {
+	let pairs = (0..authorities_len)
+		.map(|i| AuthorityPair::from_seed(&U256::from(i).into()))
+		.collect::<Vec<_>>();
+
+	let authorities: Vec<_> = pairs.iter().map(|p| p.public()).collect();
+
+	let mut storage = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
+
+	pallet_sassafras::GenesisConfig::<Test> {
+		authorities: authorities.clone(),
+		epoch_config: TEST_EPOCH_CONFIGURATION,
+		_phantom: sp_std::marker::PhantomData,
+	}
+	.assimilate_storage(&mut storage)
+	.unwrap();
+
+	let mut ext: sp_io::TestExternalities = storage.into();
+
+	if with_ring_context {
+		ext.execute_with(|| {
+			log::debug!(target: LOG_TARGET, "Building testing ring context");
+			let ring_ctx = vrf::RingContext::new_testing();
+			RingContext::<Test>::set(Some(ring_ctx.clone()));
+			Sassafras::update_ring_verifier(&authorities);
+		});
+	}
+
+	(pairs, ext)
+}
+
+fn make_ticket_with_prover(
+	attempt: u32,
+	pair: &AuthorityPair,
+	prover: &RingProver,
+) -> TicketEnvelope {
+	log::debug!("attempt: {}", attempt);
+
+	// Values are referring to the next epoch
+	let epoch = Sassafras::epoch_index() + 1;
+	let randomness = Sassafras::next_randomness();
+
+	// Make a dummy ephemeral public that hopefully is unique within one test instance.
+	// In the tests, the values within the erased public are just used to compare
+	// ticket bodies, so it is not important to be a valid key.
+	let mut raw: [u8; 32] = [0; 32];
+	raw.copy_from_slice(&pair.public().as_slice()[0..32]);
+	let erased_public = EphemeralPublic::unchecked_from(raw);
+	let revealed_public = erased_public;
+
+	let ticket_id_input = vrf::ticket_id_input(&randomness, attempt, epoch);
+
+	let body = TicketBody { attempt_idx: attempt, erased_public, revealed_public };
+	let sign_data = vrf::ticket_body_sign_data(&body, ticket_id_input);
+
+	let signature = pair.as_ref().ring_vrf_sign(&sign_data, &prover);
+
+	// Ticket-id can be generated via vrf-preout.
+	// We don't care that much about its value here.
+	TicketEnvelope { body, signature }
+}
+
+pub fn make_prover(pair: &AuthorityPair) -> RingProver {
+	let public = pair.public();
+	let mut prover_idx = None;
+
+	let ring_ctx = Sassafras::ring_context().unwrap();
+
+	let pks: Vec<sp_core::bandersnatch::Public> = Sassafras::authorities()
+		.iter()
+		.enumerate()
+		.map(|(idx, auth)| {
+			if public == *auth {
+				prover_idx = Some(idx);
+			}
+			*auth.as_ref()
+		})
+		.collect();
+
+	log::debug!("Building prover. Ring size: {}", pks.len());
+	let prover = ring_ctx.prover(&pks, prover_idx.unwrap()).unwrap();
+	log::debug!("Done");
+
+	prover
+}
+
+/// Construct `attempts` tickets envelopes for the next epoch.
+///
+/// E.g. by passing an optional threshold
+pub fn make_tickets(attempts: u32, pair: &AuthorityPair) -> Vec<TicketEnvelope> {
+	let prover = make_prover(pair);
+	(0..attempts)
+		.into_iter()
+		.map(|attempt| make_ticket_with_prover(attempt, pair, &prover))
+		.collect()
+}
+
+pub fn make_ticket_body(attempt_idx: u32, pair: &AuthorityPair) -> (TicketId, TicketBody) {
+	// Values are referring to the next epoch
+	let epoch = Sassafras::epoch_index() + 1;
+	let randomness = Sassafras::next_randomness();
+
+	let ticket_id_input = vrf::ticket_id_input(&randomness, attempt_idx, epoch);
+	let ticket_id_output = pair.as_inner_ref().vrf_output(&ticket_id_input);
+
+	let id = vrf::make_ticket_id(&ticket_id_input, &ticket_id_output);
+
+	// Make a dummy ephemeral public that hopefully is unique within one test instance.
+	// In the tests, the values within the erased public are just used to compare
+	// ticket bodies, so it is not important to be a valid key.
+	let mut raw: [u8; 32] = [0; 32];
+	raw[..16].copy_from_slice(&pair.public().as_slice()[0..16]);
+	raw[16..].copy_from_slice(&id.to_le_bytes());
+	let erased_public = EphemeralPublic::unchecked_from(raw);
+	let revealed_public = erased_public;
+
+	let body = TicketBody { attempt_idx, erased_public, revealed_public };
+
+	(id, body)
+}
+
+pub fn make_dummy_ticket_body(attempt_idx: u32) -> (TicketId, TicketBody) {
+	let hash = sp_core::hashing::blake2_256(&attempt_idx.to_le_bytes());
+
+	let erased_public = EphemeralPublic::unchecked_from(hash);
+	let revealed_public = erased_public;
+
+	let body = TicketBody { attempt_idx, erased_public, revealed_public };
+
+	let mut bytes = [0u8; 16];
+	bytes.copy_from_slice(&hash[..16]);
+	let id = TicketId::from_le_bytes(bytes);
+
+	(id, body)
+}
+
+pub fn make_ticket_bodies(
+	number: u32,
+	pair: Option<&AuthorityPair>,
+) -> Vec<(TicketId, TicketBody)> {
+	(0..number)
+		.into_iter()
+		.map(|i| match pair {
+			Some(pair) => make_ticket_body(i, pair),
+			None => make_dummy_ticket_body(i),
+		})
+		.collect()
+}
+
+/// Persist the given tickets in the unsorted segments buffer.
+///
+/// This function skips all the checks performed by the `submit_tickets` extrinsic and
+/// directly appends the tickets to the `UnsortedSegments` structure.
+pub fn persist_next_epoch_tickets_as_segments(tickets: &[(TicketId, TicketBody)]) {
+	let mut ids = Vec::with_capacity(tickets.len());
+	tickets.iter().for_each(|(id, body)| {
+		TicketsData::<Test>::set(id, Some(body.clone()));
+		ids.push(*id);
+	});
+	let max_chunk_size = Sassafras::epoch_length() as usize;
+	ids.chunks(max_chunk_size).for_each(|chunk| {
+		Sassafras::append_tickets(BoundedVec::truncate_from(chunk.to_vec()));
+	})
+}
+
+/// Calls the [`persist_next_epoch_tickets_as_segments`] and then proceeds to the
+/// sorting of the candidates.
+///
+/// Only "winning" tickets are left.
+pub fn persist_next_epoch_tickets(tickets: &[(TicketId, TicketBody)]) {
+	persist_next_epoch_tickets_as_segments(tickets);
+	// Force sorting of next epoch tickets (enactment) by explicitly querying the first of them.
+	let next_epoch = Sassafras::next_epoch();
+	assert_eq!(TicketsMeta::<Test>::get().unsorted_tickets_count, tickets.len() as u32);
+	Sassafras::slot_ticket(next_epoch.start).unwrap();
+	assert_eq!(TicketsMeta::<Test>::get().unsorted_tickets_count, 0);
+}
+
+fn slot_claim_vrf_signature(slot: Slot, pair: &AuthorityPair) -> VrfSignature {
+	let mut epoch = Sassafras::epoch_index();
+	let mut randomness = Sassafras::randomness();
+
+	// Check if epoch is going to change on initialization.
+	let epoch_start = Sassafras::current_epoch_start();
+	let epoch_length = EPOCH_LENGTH.into();
+	if epoch_start != 0_u64 && slot >= epoch_start + epoch_length {
+		epoch += slot.saturating_sub(epoch_start).saturating_div(epoch_length);
+		randomness = crate::NextRandomness::<Test>::get();
+	}
+
+	let data = vrf::slot_claim_sign_data(&randomness, slot, epoch);
+	pair.as_ref().vrf_sign(&data)
+}
+
+/// Construct a `PreDigest` instance for the given parameters.
+pub fn make_slot_claim(
+	authority_idx: AuthorityIndex,
+	slot: Slot,
+	pair: &AuthorityPair,
+) -> SlotClaim {
+	let vrf_signature = slot_claim_vrf_signature(slot, pair);
+	SlotClaim { authority_idx, slot, vrf_signature, ticket_claim: None }
+}
+
+/// Construct a `Digest` with a `SlotClaim` item.
+pub fn make_digest(authority_idx: AuthorityIndex, slot: Slot, pair: &AuthorityPair) -> Digest {
+	let claim = make_slot_claim(authority_idx, slot, pair);
+	Digest { logs: vec![DigestItem::from(&claim)] }
+}
+
+pub fn initialize_block(
+	number: u64,
+	slot: Slot,
+	parent_hash: H256,
+	pair: &AuthorityPair,
+) -> Digest {
+	let digest = make_digest(0, slot, pair);
+	System::reset_events();
+	System::initialize(&number, &parent_hash, &digest);
+	Sassafras::on_initialize(number);
+	digest
+}
+
+pub fn finalize_block(number: u64) -> Header {
+	Sassafras::on_finalize(number);
+	System::finalize()
+}
+
+/// Progress the pallet state up to the given block `number` and `slot`.
+pub fn go_to_block(number: u64, slot: Slot, pair: &AuthorityPair) -> Digest {
+	Sassafras::on_finalize(System::block_number());
+	let parent_hash = System::finalize().hash();
+
+	let digest = make_digest(0, slot, pair);
+
+	System::reset_events();
+	System::initialize(&number, &parent_hash, &digest);
+	Sassafras::on_initialize(number);
+
+	digest
+}
+
+/// Progress the pallet state up to the given block `number`.
+/// Slots will grow linearly accordingly to blocks.
+pub fn progress_to_block(number: u64, pair: &AuthorityPair) -> Option<Digest> {
+	let mut slot = Sassafras::current_slot() + 1;
+	let mut digest = None;
+	for i in System::block_number() + 1..=number {
+		let dig = go_to_block(i, slot, pair);
+		digest = Some(dig);
+		slot = slot + 1;
+	}
+	digest
+}
diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs
new file mode 100644
index 00000000000..ec3425cce7b
--- /dev/null
+++ b/substrate/frame/sassafras/src/tests.rs
@@ -0,0 +1,874 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Tests for Sassafras pallet.
+
+use crate::*;
+use mock::*;
+
+use sp_consensus_sassafras::Slot;
+
+fn h2b<const N: usize>(hex: &str) -> [u8; N] {
+	array_bytes::hex2array_unchecked(hex)
+}
+
+fn b2h<const N: usize>(bytes: [u8; N]) -> String {
+	array_bytes::bytes2hex("", &bytes)
+}
+
+#[test]
+fn genesis_values_assumptions_check() {
+	new_test_ext(3).execute_with(|| {
+		assert_eq!(Sassafras::authorities().len(), 3);
+		assert_eq!(Sassafras::config(), TEST_EPOCH_CONFIGURATION);
+	});
+}
+
+#[test]
+fn post_genesis_randomness_initialization() {
+	let (pairs, mut ext) = new_test_ext_with_pairs(1, false);
+	let pair = &pairs[0];
+
+	ext.execute_with(|| {
+		assert_eq!(Sassafras::randomness(), [0; 32]);
+		assert_eq!(Sassafras::next_randomness(), [0; 32]);
+		assert_eq!(Sassafras::randomness_accumulator(), [0; 32]);
+
+		// Test the values with a zero genesis block hash
+		let _ = initialize_block(1, 123.into(), [0x00; 32].into(), pair);
+
+		assert_eq!(Sassafras::randomness(), [0; 32]);
+		println!("[DEBUG] {}", b2h(Sassafras::next_randomness()));
+		assert_eq!(
+			Sassafras::next_randomness(),
+			h2b("b9497550deeeb4adc134555930de61968a0558f8947041eb515b2f5fa68ffaf7")
+		);
+		println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
+		assert_eq!(
+			Sassafras::randomness_accumulator(),
+			h2b("febcc7fe9539fe17ed29f525831394edfb30b301755dc9bd91584a1f065faf87")
+		);
+		let (id1, _) = make_ticket_bodies(1, Some(pair))[0];
+
+		// Reset what is relevant
+		NextRandomness::<Test>::set([0; 32]);
+		RandomnessAccumulator::<Test>::set([0; 32]);
+
+		// Test the values with a non-zero genesis block hash
+		let _ = initialize_block(1, 123.into(), [0xff; 32].into(), pair);
+
+		assert_eq!(Sassafras::randomness(), [0; 32]);
+		println!("[DEBUG] {}", b2h(Sassafras::next_randomness()));
+		assert_eq!(
+			Sassafras::next_randomness(),
+			h2b("51c1e3b3a73d2043b3cabae98ff27bdd4aad8967c21ecda7b9465afaa0e70f37")
+		);
+		println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
+		assert_eq!(
+			Sassafras::randomness_accumulator(),
+			h2b("466bf3007f2e17bffee0b3c42c90f33d654f5ff61eff28b0cc650825960abd52")
+		);
+		let (id2, _) = make_ticket_bodies(1, Some(pair))[0];
+
+		// Ticket ids should be different when next epoch randomness is different
+		assert_ne!(id1, id2);
+
+		// Reset what is relevant
+		NextRandomness::<Test>::set([0; 32]);
+		RandomnessAccumulator::<Test>::set([0; 32]);
+
+		// Test the values with a non-zero genesis block hash
+		let _ = initialize_block(1, 321.into(), [0x00; 32].into(), pair);
+
+		println!("[DEBUG] {}", b2h(Sassafras::next_randomness()));
+		assert_eq!(
+			Sassafras::next_randomness(),
+			h2b("d85d84a54f79453000eb62e8a17b30149bd728d3232bc2787a89d51dc9a36008")
+		);
+		println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
+		assert_eq!(
+			Sassafras::randomness_accumulator(),
+			h2b("8a035eed02b5b8642b1515ed19752df8df156627aea45c4ef6e3efa88be9a74d")
+		);
+		let (id2, _) = make_ticket_bodies(1, Some(pair))[0];
+
+		// Ticket ids should be different when next epoch randomness is different
+		assert_ne!(id1, id2);
+	});
+}
+
+// Tests if the sorted tickets are assigned to each slot outside-in.
+#[test]
+fn slot_ticket_id_outside_in_fetch() {
+	let genesis_slot = Slot::from(100);
+	let tickets_count = 6;
+
+	// Current epoch tickets
+	let curr_tickets: Vec<TicketId> = (0..tickets_count).map(|i| i as TicketId).collect();
+
+	// Next epoch tickets
+	let next_tickets: Vec<TicketId> =
+		(0..tickets_count - 1).map(|i| (i + tickets_count) as TicketId).collect();
+
+	new_test_ext(0).execute_with(|| {
+		// Some corner cases
+		TicketsIds::<Test>::insert((0, 0_u32), 1_u128);
+
+		// Cleanup
+		(0..3).for_each(|i| TicketsIds::<Test>::remove((0, i as u32)));
+
+		curr_tickets
+			.iter()
+			.enumerate()
+			.for_each(|(i, id)| TicketsIds::<Test>::insert((0, i as u32), id));
+
+		next_tickets
+			.iter()
+			.enumerate()
+			.for_each(|(i, id)| TicketsIds::<Test>::insert((1, i as u32), id));
+
+		TicketsMeta::<Test>::set(TicketsMetadata {
+			tickets_count: [curr_tickets.len() as u32, next_tickets.len() as u32],
+			unsorted_tickets_count: 0,
+		});
+
+		// Before importing the first block the pallet always return `None`
+		// This is a kind of special hardcoded case that should never happen in practice
+		// as the first thing the pallet does is to initialize the genesis slot.
+
+		assert_eq!(Sassafras::slot_ticket_id(0.into()), None);
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 0), None);
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 1), None);
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 100), None);
+
+		// Initialize genesis slot..
+		GenesisSlot::<Test>::set(genesis_slot);
+		frame_system::Pallet::<Test>::set_block_number(One::one());
+
+		// Try to fetch a ticket for a slot before current epoch.
+		assert_eq!(Sassafras::slot_ticket_id(0.into()), None);
+
+		// Current epoch tickets.
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 0), Some(curr_tickets[1]));
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 1), Some(curr_tickets[3]));
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 2), Some(curr_tickets[5]));
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 3), None);
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 4), None);
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 5), None);
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 6), None);
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 7), Some(curr_tickets[4]));
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 8), Some(curr_tickets[2]));
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 9), Some(curr_tickets[0]));
+
+		// Next epoch tickets (note that only 5 tickets are available)
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 10), Some(next_tickets[1]));
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 11), Some(next_tickets[3]));
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 12), None);
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 13), None);
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 14), None);
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 15), None);
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 16), None);
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 17), Some(next_tickets[4]));
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 18), Some(next_tickets[2]));
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 19), Some(next_tickets[0]));
+
+		// Try to fetch the tickets for slots beyond the next epoch.
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 20), None);
+		assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 42), None);
+	});
+}
+
+// Different test for outside-in test with more focus on corner case correctness.
+#[test]
+fn slot_ticket_id_outside_in_fetch_corner_cases() {
+	new_test_ext(0).execute_with(|| {
+		frame_system::Pallet::<Test>::set_block_number(One::one());
+
+		let mut meta = TicketsMetadata { tickets_count: [0, 0], unsorted_tickets_count: 0 };
+		let curr_epoch_idx = EpochIndex::<Test>::get();
+
+		let mut epoch_test = |epoch_idx| {
+			let tag = (epoch_idx & 1) as u8;
+			let epoch_start = Sassafras::epoch_start(epoch_idx);
+
+			// cleanup
+			meta.tickets_count = [0, 0];
+			TicketsMeta::<Test>::set(meta);
+			assert!((0..10).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none()));
+
+			meta.tickets_count[tag as usize] += 1;
+			TicketsMeta::<Test>::set(meta);
+			TicketsIds::<Test>::insert((tag, 0_u32), 1_u128);
+			assert_eq!(Sassafras::slot_ticket_id((epoch_start + 9).into()), Some(1_u128));
+			assert!((0..9).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none()));
+
+			meta.tickets_count[tag as usize] += 1;
+			TicketsMeta::<Test>::set(meta);
+			TicketsIds::<Test>::insert((tag, 1_u32), 2_u128);
+			assert_eq!(Sassafras::slot_ticket_id((epoch_start + 0).into()), Some(2_u128));
+			assert!((1..9).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none()));
+
+			meta.tickets_count[tag as usize] += 2;
+			TicketsMeta::<Test>::set(meta);
+			TicketsIds::<Test>::insert((tag, 2_u32), 3_u128);
+			assert_eq!(Sassafras::slot_ticket_id((epoch_start + 8).into()), Some(3_u128));
+			assert!((1..8).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none()));
+		};
+
+		// Even epoch
+		epoch_test(curr_epoch_idx);
+		epoch_test(curr_epoch_idx + 1);
+	});
+}
+
+#[test]
+fn on_first_block_after_genesis() {
+	let (pairs, mut ext) = new_test_ext_with_pairs(4, false);
+
+	ext.execute_with(|| {
+		let start_slot = Slot::from(100);
+		let start_block = 1;
+
+		let digest = initialize_block(start_block, start_slot, Default::default(), &pairs[0]);
+
+		let common_assertions = || {
+			assert_eq!(Sassafras::genesis_slot(), start_slot);
+			assert_eq!(Sassafras::current_slot(), start_slot);
+			assert_eq!(Sassafras::epoch_index(), 0);
+			assert_eq!(Sassafras::current_epoch_start(), start_slot);
+			assert_eq!(Sassafras::current_slot_index(), 0);
+			assert_eq!(Sassafras::randomness(), [0; 32]);
+			println!("[DEBUG] {}", b2h(Sassafras::next_randomness()));
+			assert_eq!(
+				Sassafras::next_randomness(),
+				h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e")
+			);
+		};
+
+		// Post-initialization status
+
+		assert!(ClaimTemporaryData::<Test>::exists());
+		common_assertions();
+		println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
+		assert_eq!(
+			Sassafras::randomness_accumulator(),
+			h2b("f0d42f6b7c0d157ecbd788be44847b80a96c290c04b5dfa5d1d40c98aa0c04ed")
+		);
+
+		let header = finalize_block(start_block);
+
+		// Post-finalization status
+
+		assert!(!ClaimTemporaryData::<Test>::exists());
+		common_assertions();
+		println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
+		assert_eq!(
+			Sassafras::randomness_accumulator(),
+			h2b("9f2b9fd19a772c34d437dcd8b84a927e73a5cb43d3d1cd00093223d60d2b4843"),
+		);
+
+		// Header data check
+
+		assert_eq!(header.digest.logs.len(), 2);
+		assert_eq!(header.digest.logs[0], digest.logs[0]);
+
+		// Genesis epoch start deposits consensus
+		let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData(
+			sp_consensus_sassafras::digests::NextEpochDescriptor {
+				authorities: Sassafras::next_authorities().into_inner(),
+				randomness: Sassafras::next_randomness(),
+				config: None,
+			},
+		);
+		let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode());
+		assert_eq!(header.digest.logs[1], consensus_digest)
+	})
+}
+
+#[test]
+fn on_normal_block() {
+	let (pairs, mut ext) = new_test_ext_with_pairs(4, false);
+	let start_slot = Slot::from(100);
+	let start_block = 1;
+	let end_block = start_block + 1;
+
+	ext.execute_with(|| {
+		initialize_block(start_block, start_slot, Default::default(), &pairs[0]);
+
+		// We don't want to trigger an epoch change in this test.
+		let epoch_length = Sassafras::epoch_length() as u64;
+		assert!(epoch_length > end_block);
+
+		// Progress to block 2
+		let digest = progress_to_block(end_block, &pairs[0]).unwrap();
+
+		let common_assertions = || {
+			assert_eq!(Sassafras::genesis_slot(), start_slot);
+			assert_eq!(Sassafras::current_slot(), start_slot + 1);
+			assert_eq!(Sassafras::epoch_index(), 0);
+			assert_eq!(Sassafras::current_epoch_start(), start_slot);
+			assert_eq!(Sassafras::current_slot_index(), 1);
+			assert_eq!(Sassafras::randomness(), [0; 32]);
+			println!("[DEBUG] {}", b2h(Sassafras::next_randomness()));
+			assert_eq!(
+				Sassafras::next_randomness(),
+				h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e")
+			);
+		};
+
+		// Post-initialization status
+
+		assert!(ClaimTemporaryData::<Test>::exists());
+		common_assertions();
+		println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
+		assert_eq!(
+			Sassafras::randomness_accumulator(),
+			h2b("9f2b9fd19a772c34d437dcd8b84a927e73a5cb43d3d1cd00093223d60d2b4843"),
+		);
+
+		let header = finalize_block(end_block);
+
+		// Post-finalization status
+
+		assert!(!ClaimTemporaryData::<Test>::exists());
+		common_assertions();
+		assert_eq!(
+			Sassafras::randomness_accumulator(),
+			h2b("be9261adb9686dfd3f23f8a276b7acc7f4beb3137070beb64c282ac22d84cbf0"),
+		);
+
+		// Header data check
+
+		assert_eq!(header.digest.logs.len(), 1);
+		assert_eq!(header.digest.logs[0], digest.logs[0]);
+	});
+}
+
+#[test]
+fn produce_epoch_change_digest_no_config() {
+	let (pairs, mut ext) = new_test_ext_with_pairs(4, false);
+
+	ext.execute_with(|| {
+		let start_slot = Slot::from(100);
+		let start_block = 1;
+
+		initialize_block(start_block, start_slot, Default::default(), &pairs[0]);
+
+		// We want to trigger an epoch change in this test.
+		let epoch_length = Sassafras::epoch_length() as u64;
+		let end_block = start_block + epoch_length;
+
+		let digest = progress_to_block(end_block, &pairs[0]).unwrap();
+
+		let common_assertions = || {
+			assert_eq!(Sassafras::genesis_slot(), start_slot);
+			assert_eq!(Sassafras::current_slot(), start_slot + epoch_length);
+			assert_eq!(Sassafras::epoch_index(), 1);
+			assert_eq!(Sassafras::current_epoch_start(), start_slot + epoch_length);
+			assert_eq!(Sassafras::current_slot_index(), 0);
+			println!("[DEBUG] {}", b2h(Sassafras::randomness()));
+			assert_eq!(
+				Sassafras::randomness(),
+				h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e")
+			);
+		};
+
+		// Post-initialization status
+
+		assert!(ClaimTemporaryData::<Test>::exists());
+		common_assertions();
+		println!("[DEBUG] {}", b2h(Sassafras::next_randomness()));
+		assert_eq!(
+			Sassafras::next_randomness(),
+			h2b("d3a18b857af6ecc7b52f047107e684fff0058b5722d540a296d727e37eaa55b3"),
+		);
+		println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
+		assert_eq!(
+			Sassafras::randomness_accumulator(),
+			h2b("bf0f1228f4ff953c8c1bda2cceb668bf86ea05d7ae93e26d021c9690995d5279"),
+		);
+
+		let header = finalize_block(end_block);
+
+		// Post-finalization status
+
+		assert!(!ClaimTemporaryData::<Test>::exists());
+		common_assertions();
+		println!("[DEBUG] {}", b2h(Sassafras::next_randomness()));
+		assert_eq!(
+			Sassafras::next_randomness(),
+			h2b("d3a18b857af6ecc7b52f047107e684fff0058b5722d540a296d727e37eaa55b3"),
+		);
+		println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
+		assert_eq!(
+			Sassafras::randomness_accumulator(),
+			h2b("8a1ceb346036c386d021264b10912c8b656799668004c4a487222462b394cd89"),
+		);
+
+		// Header data check
+
+		assert_eq!(header.digest.logs.len(), 2);
+		assert_eq!(header.digest.logs[0], digest.logs[0]);
+		// Deposits consensus log on epoch change
+		let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData(
+			sp_consensus_sassafras::digests::NextEpochDescriptor {
+				authorities: Sassafras::next_authorities().into_inner(),
+				randomness: Sassafras::next_randomness(),
+				config: None,
+			},
+		);
+		let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode());
+		assert_eq!(header.digest.logs[1], consensus_digest)
+	})
+}
+
+#[test]
+fn produce_epoch_change_digest_with_config() {
+	let (pairs, mut ext) = new_test_ext_with_pairs(4, false);
+
+	ext.execute_with(|| {
+		let start_slot = Slot::from(100);
+		let start_block = 1;
+
+		initialize_block(start_block, start_slot, Default::default(), &pairs[0]);
+
+		let config = EpochConfiguration { redundancy_factor: 1, attempts_number: 123 };
+		Sassafras::plan_config_change(RuntimeOrigin::root(), config).unwrap();
+
+		// We want to trigger an epoch change in this test.
+		let epoch_length = Sassafras::epoch_length() as u64;
+		let end_block = start_block + epoch_length;
+
+		let digest = progress_to_block(end_block, &pairs[0]).unwrap();
+
+		let header = finalize_block(end_block);
+
+		// Header data check.
+		// Skip pallet status checks that were already performed by other tests.
+
+		assert_eq!(header.digest.logs.len(), 2);
+		assert_eq!(header.digest.logs[0], digest.logs[0]);
+		// Deposits consensus log on epoch change
+		let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData(
+			sp_consensus_sassafras::digests::NextEpochDescriptor {
+				authorities: Sassafras::next_authorities().into_inner(),
+				randomness: Sassafras::next_randomness(),
+				config: Some(config),
+			},
+		);
+		let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode());
+		assert_eq!(header.digest.logs[1], consensus_digest)
+	})
+}
+
+#[test]
+fn segments_incremental_sort_works() {
+	let (pairs, mut ext) = new_test_ext_with_pairs(1, false);
+	let pair = &pairs[0];
+	let segments_count = 14;
+	let start_slot = Slot::from(100);
+	let start_block = 1;
+
+	ext.execute_with(|| {
+		let epoch_length = Sassafras::epoch_length() as u64;
+		// -3 just to have the last segment not full...
+		let submitted_tickets_count = segments_count * SEGMENT_MAX_SIZE - 3;
+
+		initialize_block(start_block, start_slot, Default::default(), pair);
+
+		// Manually populate the segments to skip the threshold check
+		let mut tickets = make_ticket_bodies(submitted_tickets_count, None);
+		persist_next_epoch_tickets_as_segments(&tickets);
+
+		// Proceed to half of the epoch (sortition should not have been started yet)
+		let half_epoch_block = start_block + epoch_length / 2;
+		progress_to_block(half_epoch_block, pair);
+
+		let mut unsorted_tickets_count = submitted_tickets_count;
+
+		// Check that next epoch tickets sortition is not started yet
+		let meta = TicketsMeta::<Test>::get();
+		assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count);
+		assert_eq!(meta.tickets_count, [0, 0]);
+
+		// Follow the incremental sortition block by block
+
+		progress_to_block(half_epoch_block + 1, pair);
+		unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE - 3;
+		let meta = TicketsMeta::<Test>::get();
+		assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count,);
+		assert_eq!(meta.tickets_count, [0, 0]);
+
+		progress_to_block(half_epoch_block + 2, pair);
+		unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE;
+		let meta = TicketsMeta::<Test>::get();
+		assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count);
+		assert_eq!(meta.tickets_count, [0, 0]);
+
+		progress_to_block(half_epoch_block + 3, pair);
+		unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE;
+		let meta = TicketsMeta::<Test>::get();
+		assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count);
+		assert_eq!(meta.tickets_count, [0, 0]);
+
+		progress_to_block(half_epoch_block + 4, pair);
+		unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE;
+		let meta = TicketsMeta::<Test>::get();
+		assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count);
+		assert_eq!(meta.tickets_count, [0, 0]);
+
+		let header = finalize_block(half_epoch_block + 4);
+
+		// Sort should be finished now.
+		// Check that next epoch tickets count have the correct value.
+		// Bigger ticket ids were discarded during sortition.
+		unsorted_tickets_count -= 2 * SEGMENT_MAX_SIZE;
+		assert_eq!(unsorted_tickets_count, 0);
+		let meta = TicketsMeta::<Test>::get();
+		assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count);
+		assert_eq!(meta.tickets_count, [0, epoch_length as u32]);
+		// Epoch change log should have been pushed as well
+		assert_eq!(header.digest.logs.len(), 1);
+		// No tickets for the current epoch
+		assert_eq!(TicketsIds::<Test>::get((0, 0)), None);
+
+		// Check persistence of "winning" tickets
+		tickets.sort_by_key(|t| t.0);
+		(0..epoch_length as usize).into_iter().for_each(|i| {
+			let id = TicketsIds::<Test>::get((1, i as u32)).unwrap();
+			let body = TicketsData::<Test>::get(id).unwrap();
+			assert_eq!((id, body), tickets[i]);
+		});
+		// Check removal of "loosing" tickets
+		(epoch_length as usize..tickets.len()).into_iter().for_each(|i| {
+			assert!(TicketsIds::<Test>::get((1, i as u32)).is_none());
+			assert!(TicketsData::<Test>::get(tickets[i].0).is_none());
+		});
+
+		// The next block will be the first produced on the new epoch.
+		// At this point the tickets are found already sorted and ready to be used.
+		let slot = Sassafras::current_slot() + 1;
+		let number = System::block_number() + 1;
+		initialize_block(number, slot, header.hash(), pair);
+		let header = finalize_block(number);
+		// Epoch changes digest is also produced
+		assert_eq!(header.digest.logs.len(), 2);
+	});
+}
+
+#[test]
+fn tickets_fetch_works_after_epoch_change() {
+	let (pairs, mut ext) = new_test_ext_with_pairs(4, false);
+	let pair = &pairs[0];
+	let start_slot = Slot::from(100);
+	let start_block = 1;
+	let submitted_tickets = 300;
+
+	ext.execute_with(|| {
+		initialize_block(start_block, start_slot, Default::default(), pair);
+
+		// We don't want to trigger an epoch change in this test.
+		let epoch_length = Sassafras::epoch_length() as u64;
+		assert!(epoch_length > 2);
+		progress_to_block(2, &pairs[0]).unwrap();
+
+		// Persist tickets as three different segments.
+		let tickets = make_ticket_bodies(submitted_tickets, None);
+		persist_next_epoch_tickets_as_segments(&tickets);
+
+		let meta = TicketsMeta::<Test>::get();
+		assert_eq!(meta.unsorted_tickets_count, submitted_tickets);
+		assert_eq!(meta.tickets_count, [0, 0]);
+
+		// Progress up to the last epoch slot (do not enact epoch change)
+		progress_to_block(epoch_length, &pairs[0]).unwrap();
+
+		// At this point next epoch tickets should have been sorted and ready to be used
+		let meta = TicketsMeta::<Test>::get();
+		assert_eq!(meta.unsorted_tickets_count, 0);
+		assert_eq!(meta.tickets_count, [0, epoch_length as u32]);
+
+		// Compute and sort the tickets ids (aka tickets scores)
+		let mut expected_ids: Vec<_> = tickets.into_iter().map(|(id, _)| id).collect();
+		expected_ids.sort();
+		expected_ids.truncate(epoch_length as usize);
+
+		// Check if we can fetch next epoch tickets ids (outside-in).
+		let slot = Sassafras::current_slot();
+		assert_eq!(Sassafras::slot_ticket_id(slot + 1).unwrap(), expected_ids[1]);
+		assert_eq!(Sassafras::slot_ticket_id(slot + 2).unwrap(), expected_ids[3]);
+		assert_eq!(Sassafras::slot_ticket_id(slot + 3).unwrap(), expected_ids[5]);
+		assert_eq!(Sassafras::slot_ticket_id(slot + 4).unwrap(), expected_ids[7]);
+		assert_eq!(Sassafras::slot_ticket_id(slot + 7).unwrap(), expected_ids[6]);
+		assert_eq!(Sassafras::slot_ticket_id(slot + 8).unwrap(), expected_ids[4]);
+		assert_eq!(Sassafras::slot_ticket_id(slot + 9).unwrap(), expected_ids[2]);
+		assert_eq!(Sassafras::slot_ticket_id(slot + 10).unwrap(), expected_ids[0]);
+		assert!(Sassafras::slot_ticket_id(slot + 11).is_none());
+
+		// Enact epoch change by progressing one more block
+
+		progress_to_block(epoch_length + 1, &pairs[0]).unwrap();
+
+		let meta = TicketsMeta::<Test>::get();
+		assert_eq!(meta.unsorted_tickets_count, 0);
+		assert_eq!(meta.tickets_count, [0, 10]);
+
+		// Check if we can fetch current epoch tickets ids (outside-in).
+		let slot = Sassafras::current_slot();
+		assert_eq!(Sassafras::slot_ticket_id(slot).unwrap(), expected_ids[1]);
+		assert_eq!(Sassafras::slot_ticket_id(slot + 1).unwrap(), expected_ids[3]);
+		assert_eq!(Sassafras::slot_ticket_id(slot + 2).unwrap(), expected_ids[5]);
+		assert_eq!(Sassafras::slot_ticket_id(slot + 3).unwrap(), expected_ids[7]);
+		assert_eq!(Sassafras::slot_ticket_id(slot + 6).unwrap(), expected_ids[6]);
+		assert_eq!(Sassafras::slot_ticket_id(slot + 7).unwrap(), expected_ids[4]);
+		assert_eq!(Sassafras::slot_ticket_id(slot + 8).unwrap(), expected_ids[2]);
+		assert_eq!(Sassafras::slot_ticket_id(slot + 9).unwrap(), expected_ids[0]);
+		assert!(Sassafras::slot_ticket_id(slot + 10).is_none());
+
+		// Enact another epoch change, for which we don't have any ticket
+		progress_to_block(2 * epoch_length + 1, &pairs[0]).unwrap();
+		let meta = TicketsMeta::<Test>::get();
+		assert_eq!(meta.unsorted_tickets_count, 0);
+		assert_eq!(meta.tickets_count, [0, 0]);
+	});
+}
+
+#[test]
+fn block_allowed_to_skip_epochs() {
+	let (pairs, mut ext) = new_test_ext_with_pairs(4, false);
+	let pair = &pairs[0];
+	let start_slot = Slot::from(100);
+	let start_block = 1;
+
+	ext.execute_with(|| {
+		let epoch_length = Sassafras::epoch_length() as u64;
+
+		initialize_block(start_block, start_slot, Default::default(), pair);
+
+		let tickets = make_ticket_bodies(3, Some(pair));
+		persist_next_epoch_tickets(&tickets);
+
+		let next_random = Sassafras::next_randomness();
+
+		// We want to skip 3 epochs in this test.
+		let offset = 4 * epoch_length;
+		go_to_block(start_block + offset, start_slot + offset, &pairs[0]);
+
+		// Post-initialization status
+
+		assert!(ClaimTemporaryData::<Test>::exists());
+		assert_eq!(Sassafras::genesis_slot(), start_slot);
+		assert_eq!(Sassafras::current_slot(), start_slot + offset);
+		assert_eq!(Sassafras::epoch_index(), 4);
+		assert_eq!(Sassafras::current_epoch_start(), start_slot + offset);
+		assert_eq!(Sassafras::current_slot_index(), 0);
+
+		// Tickets data has been discarded
+		assert_eq!(TicketsMeta::<Test>::get(), TicketsMetadata::default());
+		assert!(tickets.iter().all(|(id, _)| TicketsData::<Test>::get(id).is_none()));
+		assert_eq!(SortedCandidates::<Test>::get().len(), 0);
+
+		// We used the last known next epoch randomness as a fallback
+		assert_eq!(next_random, Sassafras::randomness());
+	});
+}
+
+#[test]
+fn obsolete_tickets_are_removed_on_epoch_change() {
+	let (pairs, mut ext) = new_test_ext_with_pairs(4, false);
+	let pair = &pairs[0];
+	let start_slot = Slot::from(100);
+	let start_block = 1;
+
+	ext.execute_with(|| {
+		let epoch_length = Sassafras::epoch_length() as u64;
+
+		initialize_block(start_block, start_slot, Default::default(), pair);
+
+		let tickets = make_ticket_bodies(10, Some(pair));
+		let mut epoch1_tickets = tickets[..4].to_vec();
+		let mut epoch2_tickets = tickets[4..].to_vec();
+
+		// Persist some tickets for next epoch (N)
+		persist_next_epoch_tickets(&epoch1_tickets);
+		assert_eq!(TicketsMeta::<Test>::get().tickets_count, [0, 4]);
+		// Check next epoch tickets presence
+		epoch1_tickets.sort_by_key(|t| t.0);
+		(0..epoch1_tickets.len()).into_iter().for_each(|i| {
+			let id = TicketsIds::<Test>::get((1, i as u32)).unwrap();
+			let body = TicketsData::<Test>::get(id).unwrap();
+			assert_eq!((id, body), epoch1_tickets[i]);
+		});
+
+		// Advance one epoch to enact the tickets
+		go_to_block(start_block + epoch_length, start_slot + epoch_length, pair);
+		assert_eq!(TicketsMeta::<Test>::get().tickets_count, [0, 4]);
+
+		// Persist some tickets for next epoch (N+1)
+		persist_next_epoch_tickets(&epoch2_tickets);
+		assert_eq!(TicketsMeta::<Test>::get().tickets_count, [6, 4]);
+		epoch2_tickets.sort_by_key(|t| t.0);
+		// Check for this epoch and next epoch tickets presence
+		(0..epoch1_tickets.len()).into_iter().for_each(|i| {
+			let id = TicketsIds::<Test>::get((1, i as u32)).unwrap();
+			let body = TicketsData::<Test>::get(id).unwrap();
+			assert_eq!((id, body), epoch1_tickets[i]);
+		});
+		(0..epoch2_tickets.len()).into_iter().for_each(|i| {
+			let id = TicketsIds::<Test>::get((0, i as u32)).unwrap();
+			let body = TicketsData::<Test>::get(id).unwrap();
+			assert_eq!((id, body), epoch2_tickets[i]);
+		});
+
+		// Advance to epoch 2 and check for cleanup
+
+		go_to_block(start_block + 2 * epoch_length, start_slot + 2 * epoch_length, pair);
+		assert_eq!(TicketsMeta::<Test>::get().tickets_count, [6, 0]);
+
+		(0..epoch1_tickets.len()).into_iter().for_each(|i| {
+			let id = TicketsIds::<Test>::get((1, i as u32)).unwrap();
+			assert!(TicketsData::<Test>::get(id).is_none());
+		});
+		(0..epoch2_tickets.len()).into_iter().for_each(|i| {
+			let id = TicketsIds::<Test>::get((0, i as u32)).unwrap();
+			let body = TicketsData::<Test>::get(id).unwrap();
+			assert_eq!((id, body), epoch2_tickets[i]);
+		});
+	})
+}
+
+const TICKETS_FILE: &str = "src/data/25_tickets_100_auths.bin";
+
+fn data_read<T: Decode>(filename: &str) -> T {
+	use std::{fs::File, io::Read};
+	let mut file = File::open(filename).unwrap();
+	let mut buf = Vec::new();
+	file.read_to_end(&mut buf).unwrap();
+	T::decode(&mut &buf[..]).unwrap()
+}
+
+fn data_write<T: Encode>(filename: &str, data: T) {
+	use std::{fs::File, io::Write};
+	let mut file = File::create(filename).unwrap();
+	let buf = data.encode();
+	file.write_all(&buf).unwrap();
+}
+
+// We don't want to implement anything secure here.
+// Just a trivial shuffle for the tests.
+fn trivial_fisher_yates_shuffle<T>(vector: &mut Vec<T>, random_seed: u64) {
+	let mut rng = random_seed as usize;
+	for i in (1..vector.len()).rev() {
+		let j = rng % (i + 1);
+		vector.swap(i, j);
+		rng = (rng.wrapping_mul(6364793005) + 1) as usize; // Some random number generation
+	}
+}
+
+// For this test we use a set of pre-constructed tickets from a file.
+// Creating a large set of tickets on the fly takes time, and may be annoying
+// for test execution.
+//
+// A valid ring-context is required for this test since we are passing through the
+// `submit_ticket` call which tests for ticket validity.
+#[test]
+fn submit_tickets_with_ring_proof_check_works() {
+	use sp_core::Pair as _;
+	// env_logger::init();
+
+	let (authorities, mut tickets): (Vec<AuthorityId>, Vec<TicketEnvelope>) =
+		data_read(TICKETS_FILE);
+
+	// Also checks that duplicates are discarded
+	tickets.extend(tickets.clone());
+	trivial_fisher_yates_shuffle(&mut tickets, 321);
+
+	let (pairs, mut ext) = new_test_ext_with_pairs(authorities.len(), true);
+	let pair = &pairs[0];
+	// Check if deserialized data has been generated for the correct set of authorities...
+	assert!(authorities.iter().zip(pairs.iter()).all(|(auth, pair)| auth == &pair.public()));
+
+	ext.execute_with(|| {
+		let start_slot = Slot::from(0);
+		let start_block = 1;
+
+		// Tweak the config to discard ~half of the tickets.
+		let mut config = EpochConfig::<Test>::get();
+		config.redundancy_factor = 25;
+		EpochConfig::<Test>::set(config);
+
+		initialize_block(start_block, start_slot, Default::default(), pair);
+		NextRandomness::<Test>::set([0; 32]);
+
+		// Check state before tickets submission
+		assert_eq!(
+			TicketsMeta::<Test>::get(),
+			TicketsMetadata { unsorted_tickets_count: 0, tickets_count: [0, 0] },
+		);
+
+		// Submit the tickets
+		let max_tickets_per_call = Sassafras::epoch_length() as usize;
+		tickets.chunks(max_tickets_per_call).for_each(|chunk| {
+			let chunk = BoundedVec::truncate_from(chunk.to_vec());
+			Sassafras::submit_tickets(RuntimeOrigin::none(), chunk).unwrap();
+		});
+
+		// Check state after submission
+		assert_eq!(
+			TicketsMeta::<Test>::get(),
+			TicketsMetadata { unsorted_tickets_count: 16, tickets_count: [0, 0] },
+		);
+		assert_eq!(UnsortedSegments::<Test>::get(0).len(), 16);
+		assert_eq!(UnsortedSegments::<Test>::get(1).len(), 0);
+
+		finalize_block(start_block);
+	})
+}
+
+#[test]
+#[ignore = "test tickets data generator"]
+fn make_tickets_data() {
+	use super::*;
+	use sp_core::crypto::Pair;
+
+	// Number of authorities who produces tickets (for the sake of this test)
+	let tickets_authors_count = 5;
+	// Total number of authorities (the ring)
+	let authorities_count = 100;
+	let (pairs, mut ext) = new_test_ext_with_pairs(authorities_count, true);
+
+	let authorities: Vec<_> = pairs.iter().map(|sk| sk.public()).collect();
+
+	ext.execute_with(|| {
+		let config = EpochConfig::<Test>::get();
+
+		let tickets_count = tickets_authors_count * config.attempts_number as usize;
+		let mut tickets = Vec::with_capacity(tickets_count);
+
+		// Construct pre-built tickets with a well known `NextRandomness` value.
+		NextRandomness::<Test>::set([0; 32]);
+
+		println!("Constructing {} tickets", tickets_count);
+		pairs.iter().take(tickets_authors_count).enumerate().for_each(|(i, pair)| {
+			let t = make_tickets(config.attempts_number, pair);
+			tickets.extend(t);
+			println!("{:.2}%", 100f32 * ((i + 1) as f32 / tickets_authors_count as f32));
+		});
+
+		data_write(TICKETS_FILE, (authorities, tickets));
+	});
+}
diff --git a/substrate/frame/sassafras/src/weights.rs b/substrate/frame/sassafras/src/weights.rs
new file mode 100644
index 00000000000..32ea2d29a18
--- /dev/null
+++ b/substrate/frame/sassafras/src/weights.rs
@@ -0,0 +1,425 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Autogenerated weights for `pallet_sassafras`
+//!
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
+//! DATE: 2023-11-16, STEPS: `20`, REPEAT: `3`, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! WORST CASE MAP SIZE: `1000000`
+//! HOSTNAME: `behemoth`, CPU: `AMD Ryzen Threadripper 3970X 32-Core Processor`
+//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024`
+
+// Executed Command:
+// ./target/release/node-template
+// benchmark
+// pallet
+// --chain
+// dev
+// --pallet
+// pallet_sassafras
+// --extrinsic
+// *
+// --steps
+// 20
+// --repeat
+// 3
+// --output
+// weights.rs
+// --template
+// substrate/.maintain/frame-weight-template.hbs
+
+#![cfg_attr(rustfmt, rustfmt_skip)]
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+#![allow(missing_docs)]
+
+use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
+use core::marker::PhantomData;
+
+/// Weight functions needed for `pallet_sassafras`.
+pub trait WeightInfo {
+	fn on_initialize() -> Weight;
+	fn enact_epoch_change(x: u32, y: u32, ) -> Weight;
+	fn submit_tickets(x: u32, ) -> Weight;
+	fn plan_config_change() -> Weight;
+	fn update_ring_verifier(x: u32, ) -> Weight;
+	fn load_ring_context() -> Weight;
+	fn sort_segments(x: u32, ) -> Weight;
+}
+
+/// Weights for `pallet_sassafras` using the Substrate node and recommended hardware.
+pub struct SubstrateWeight<T>(PhantomData<T>);
+impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
+	/// Storage: `System::Digest` (r:1 w:1)
+	/// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `Sassafras::NextRandomness` (r:1 w:0)
+	/// Proof: `Sassafras::NextRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::NextAuthorities` (r:1 w:0)
+	/// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::CurrentRandomness` (r:1 w:0)
+	/// Proof: `Sassafras::CurrentRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::EpochIndex` (r:1 w:0)
+	/// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::RandomnessAccumulator` (r:1 w:1)
+	/// Proof: `Sassafras::RandomnessAccumulator` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::CurrentSlot` (r:0 w:1)
+	/// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::ClaimTemporaryData` (r:0 w:1)
+	/// Proof: `Sassafras::ClaimTemporaryData` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::GenesisSlot` (r:0 w:1)
+	/// Proof: `Sassafras::GenesisSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	fn on_initialize() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `302`
+		//  Estimated: `4787`
+		// Minimum execution time: 438_039_000 picoseconds.
+		Weight::from_parts(439_302_000, 4787)
+			.saturating_add(T::DbWeight::get().reads(6_u64))
+			.saturating_add(T::DbWeight::get().writes(5_u64))
+	}
+	/// Storage: `Sassafras::CurrentSlot` (r:1 w:0)
+	/// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::EpochIndex` (r:1 w:1)
+	/// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::GenesisSlot` (r:1 w:0)
+	/// Proof: `Sassafras::GenesisSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::NextAuthorities` (r:1 w:1)
+	/// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::RingContext` (r:1 w:0)
+	/// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::TicketsMeta` (r:1 w:1)
+	/// Proof: `Sassafras::TicketsMeta` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::NextRandomness` (r:1 w:1)
+	/// Proof: `Sassafras::NextRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::RandomnessAccumulator` (r:1 w:0)
+	/// Proof: `Sassafras::RandomnessAccumulator` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::NextEpochConfig` (r:1 w:1)
+	/// Proof: `Sassafras::NextEpochConfig` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::PendingEpochConfigChange` (r:1 w:1)
+	/// Proof: `Sassafras::PendingEpochConfigChange` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `System::Digest` (r:1 w:1)
+	/// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `Sassafras::SortedCandidates` (r:1 w:0)
+	/// Proof: `Sassafras::SortedCandidates` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::UnsortedSegments` (r:79 w:79)
+	/// Proof: `Sassafras::UnsortedSegments` (`max_values`: None, `max_size`: Some(2054), added: 4529, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::TicketsIds` (r:5000 w:200)
+	/// Proof: `Sassafras::TicketsIds` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::Authorities` (r:0 w:1)
+	/// Proof: `Sassafras::Authorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::TicketsData` (r:0 w:9896)
+	/// Proof: `Sassafras::TicketsData` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::RingVerifierData` (r:0 w:1)
+	/// Proof: `Sassafras::RingVerifierData` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::EpochConfig` (r:0 w:1)
+	/// Proof: `Sassafras::EpochConfig` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::CurrentRandomness` (r:0 w:1)
+	/// Proof: `Sassafras::CurrentRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+	/// The range of component `x` is `[1, 100]`.
+	/// The range of component `y` is `[1000, 5000]`.
+	fn enact_epoch_change(x: u32, y: u32, ) -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `594909 + x * (33 ±0) + y * (53 ±0)`
+		//  Estimated: `593350 + x * (24 ±1) + y * (2496 ±0)`
+		// Minimum execution time: 121_279_846_000 picoseconds.
+		Weight::from_parts(94_454_851_972, 593350)
+			// Standard Error: 24_177_301
+			.saturating_add(Weight::from_parts(8_086_191, 0).saturating_mul(x.into()))
+			// Standard Error: 601_053
+			.saturating_add(Weight::from_parts(15_578_413, 0).saturating_mul(y.into()))
+			.saturating_add(T::DbWeight::get().reads(13_u64))
+			.saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(y.into())))
+			.saturating_add(T::DbWeight::get().writes(112_u64))
+			.saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(y.into())))
+			.saturating_add(Weight::from_parts(0, 24).saturating_mul(x.into()))
+			.saturating_add(Weight::from_parts(0, 2496).saturating_mul(y.into()))
+	}
+	/// Storage: `Sassafras::CurrentSlot` (r:1 w:0)
+	/// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::EpochIndex` (r:1 w:0)
+	/// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::GenesisSlot` (r:1 w:0)
+	/// Proof: `Sassafras::GenesisSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::RingVerifierData` (r:1 w:0)
+	/// Proof: `Sassafras::RingVerifierData` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::NextAuthorities` (r:1 w:0)
+	/// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::NextEpochConfig` (r:1 w:0)
+	/// Proof: `Sassafras::NextEpochConfig` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::NextRandomness` (r:1 w:0)
+	/// Proof: `Sassafras::NextRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::TicketsData` (r:25 w:25)
+	/// Proof: `Sassafras::TicketsData` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::TicketsMeta` (r:1 w:1)
+	/// Proof: `Sassafras::TicketsMeta` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::UnsortedSegments` (r:1 w:1)
+	/// Proof: `Sassafras::UnsortedSegments` (`max_values`: None, `max_size`: Some(2054), added: 4529, mode: `MaxEncodedLen`)
+	/// The range of component `x` is `[1, 25]`.
+	fn submit_tickets(x: u32, ) -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `3869`
+		//  Estimated: `5519 + x * (2559 ±0)`
+		// Minimum execution time: 36_904_934_000 picoseconds.
+		Weight::from_parts(25_822_957_295, 5519)
+			// Standard Error: 11_047_832
+			.saturating_add(Weight::from_parts(11_338_353_299, 0).saturating_mul(x.into()))
+			.saturating_add(T::DbWeight::get().reads(9_u64))
+			.saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into())))
+			.saturating_add(T::DbWeight::get().writes(2_u64))
+			.saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(x.into())))
+			.saturating_add(Weight::from_parts(0, 2559).saturating_mul(x.into()))
+	}
+	/// Storage: `Sassafras::PendingEpochConfigChange` (r:0 w:1)
+	/// Proof: `Sassafras::PendingEpochConfigChange` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	fn plan_config_change() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `0`
+		//  Estimated: `0`
+		// Minimum execution time: 4_038_000 picoseconds.
+		Weight::from_parts(4_499_000, 0)
+			.saturating_add(T::DbWeight::get().writes(1_u64))
+	}
+	/// Storage: `Sassafras::RingContext` (r:1 w:0)
+	/// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::RingVerifierData` (r:0 w:1)
+	/// Proof: `Sassafras::RingVerifierData` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`)
+	/// The range of component `x` is `[1, 100]`.
+	fn update_ring_verifier(x: u32, ) -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `590485`
+		//  Estimated: `591809`
+		// Minimum execution time: 105_121_424_000 picoseconds.
+		Weight::from_parts(105_527_334_385, 591809)
+			// Standard Error: 2_933_910
+			.saturating_add(Weight::from_parts(96_136_261, 0).saturating_mul(x.into()))
+			.saturating_add(T::DbWeight::get().reads(1_u64))
+			.saturating_add(T::DbWeight::get().writes(1_u64))
+	}
+	/// Storage: `Sassafras::RingContext` (r:1 w:0)
+	/// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`)
+	fn load_ring_context() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `590485`
+		//  Estimated: `591809`
+		// Minimum execution time: 44_005_681_000 picoseconds.
+		Weight::from_parts(44_312_079_000, 591809)
+			.saturating_add(T::DbWeight::get().reads(1_u64))
+	}
+	/// Storage: `Sassafras::SortedCandidates` (r:1 w:0)
+	/// Proof: `Sassafras::SortedCandidates` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::UnsortedSegments` (r:100 w:100)
+	/// Proof: `Sassafras::UnsortedSegments` (`max_values`: None, `max_size`: Some(2054), added: 4529, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::TicketsIds` (r:0 w:200)
+	/// Proof: `Sassafras::TicketsIds` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::TicketsData` (r:0 w:12600)
+	/// Proof: `Sassafras::TicketsData` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	/// The range of component `x` is `[1, 100]`.
+	fn sort_segments(x: u32, ) -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `222 + x * (2060 ±0)`
+		//  Estimated: `4687 + x * (4529 ±0)`
+		// Minimum execution time: 183_501_000 picoseconds.
+		Weight::from_parts(183_501_000, 4687)
+			// Standard Error: 1_426_363
+			.saturating_add(Weight::from_parts(169_156_241, 0).saturating_mul(x.into()))
+			.saturating_add(T::DbWeight::get().reads(1_u64))
+			.saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into())))
+			.saturating_add(T::DbWeight::get().writes((129_u64).saturating_mul(x.into())))
+			.saturating_add(Weight::from_parts(0, 4529).saturating_mul(x.into()))
+	}
+}
+
+// For backwards compatibility and tests.
+impl WeightInfo for () {
+	/// Storage: `System::Digest` (r:1 w:1)
+	/// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `Sassafras::NextRandomness` (r:1 w:0)
+	/// Proof: `Sassafras::NextRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::NextAuthorities` (r:1 w:0)
+	/// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::CurrentRandomness` (r:1 w:0)
+	/// Proof: `Sassafras::CurrentRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::EpochIndex` (r:1 w:0)
+	/// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::RandomnessAccumulator` (r:1 w:1)
+	/// Proof: `Sassafras::RandomnessAccumulator` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::CurrentSlot` (r:0 w:1)
+	/// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::ClaimTemporaryData` (r:0 w:1)
+	/// Proof: `Sassafras::ClaimTemporaryData` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::GenesisSlot` (r:0 w:1)
+	/// Proof: `Sassafras::GenesisSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	fn on_initialize() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `302`
+		//  Estimated: `4787`
+		// Minimum execution time: 438_039_000 picoseconds.
+		Weight::from_parts(439_302_000, 4787)
+			.saturating_add(RocksDbWeight::get().reads(6_u64))
+			.saturating_add(RocksDbWeight::get().writes(5_u64))
+	}
+	/// Storage: `Sassafras::CurrentSlot` (r:1 w:0)
+	/// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::EpochIndex` (r:1 w:1)
+	/// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::GenesisSlot` (r:1 w:0)
+	/// Proof: `Sassafras::GenesisSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::NextAuthorities` (r:1 w:1)
+	/// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::RingContext` (r:1 w:0)
+	/// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::TicketsMeta` (r:1 w:1)
+	/// Proof: `Sassafras::TicketsMeta` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::NextRandomness` (r:1 w:1)
+	/// Proof: `Sassafras::NextRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::RandomnessAccumulator` (r:1 w:0)
+	/// Proof: `Sassafras::RandomnessAccumulator` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::NextEpochConfig` (r:1 w:1)
+	/// Proof: `Sassafras::NextEpochConfig` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::PendingEpochConfigChange` (r:1 w:1)
+	/// Proof: `Sassafras::PendingEpochConfigChange` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `System::Digest` (r:1 w:1)
+	/// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
+	/// Storage: `Sassafras::SortedCandidates` (r:1 w:0)
+	/// Proof: `Sassafras::SortedCandidates` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::UnsortedSegments` (r:79 w:79)
+	/// Proof: `Sassafras::UnsortedSegments` (`max_values`: None, `max_size`: Some(2054), added: 4529, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::TicketsIds` (r:5000 w:200)
+	/// Proof: `Sassafras::TicketsIds` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::Authorities` (r:0 w:1)
+	/// Proof: `Sassafras::Authorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::TicketsData` (r:0 w:9896)
+	/// Proof: `Sassafras::TicketsData` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::RingVerifierData` (r:0 w:1)
+	/// Proof: `Sassafras::RingVerifierData` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::EpochConfig` (r:0 w:1)
+	/// Proof: `Sassafras::EpochConfig` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::CurrentRandomness` (r:0 w:1)
+	/// Proof: `Sassafras::CurrentRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+	/// The range of component `x` is `[1, 100]`.
+	/// The range of component `y` is `[1000, 5000]`.
+	fn enact_epoch_change(x: u32, y: u32, ) -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `594909 + x * (33 ±0) + y * (53 ±0)`
+		//  Estimated: `593350 + x * (24 ±1) + y * (2496 ±0)`
+		// Minimum execution time: 121_279_846_000 picoseconds.
+		Weight::from_parts(94_454_851_972, 593350)
+			// Standard Error: 24_177_301
+			.saturating_add(Weight::from_parts(8_086_191, 0).saturating_mul(x.into()))
+			// Standard Error: 601_053
+			.saturating_add(Weight::from_parts(15_578_413, 0).saturating_mul(y.into()))
+			.saturating_add(RocksDbWeight::get().reads(13_u64))
+			.saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(y.into())))
+			.saturating_add(RocksDbWeight::get().writes(112_u64))
+			.saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(y.into())))
+			.saturating_add(Weight::from_parts(0, 24).saturating_mul(x.into()))
+			.saturating_add(Weight::from_parts(0, 2496).saturating_mul(y.into()))
+	}
+	/// Storage: `Sassafras::CurrentSlot` (r:1 w:0)
+	/// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::EpochIndex` (r:1 w:0)
+	/// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::GenesisSlot` (r:1 w:0)
+	/// Proof: `Sassafras::GenesisSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::RingVerifierData` (r:1 w:0)
+	/// Proof: `Sassafras::RingVerifierData` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::NextAuthorities` (r:1 w:0)
+	/// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::NextEpochConfig` (r:1 w:0)
+	/// Proof: `Sassafras::NextEpochConfig` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::NextRandomness` (r:1 w:0)
+	/// Proof: `Sassafras::NextRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::TicketsData` (r:25 w:25)
+	/// Proof: `Sassafras::TicketsData` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::TicketsMeta` (r:1 w:1)
+	/// Proof: `Sassafras::TicketsMeta` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::UnsortedSegments` (r:1 w:1)
+	/// Proof: `Sassafras::UnsortedSegments` (`max_values`: None, `max_size`: Some(2054), added: 4529, mode: `MaxEncodedLen`)
+	/// The range of component `x` is `[1, 25]`.
+	fn submit_tickets(x: u32, ) -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `3869`
+		//  Estimated: `5519 + x * (2559 ±0)`
+		// Minimum execution time: 36_904_934_000 picoseconds.
+		Weight::from_parts(25_822_957_295, 5519)
+			// Standard Error: 11_047_832
+			.saturating_add(Weight::from_parts(11_338_353_299, 0).saturating_mul(x.into()))
+			.saturating_add(RocksDbWeight::get().reads(9_u64))
+			.saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into())))
+			.saturating_add(RocksDbWeight::get().writes(2_u64))
+			.saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(x.into())))
+			.saturating_add(Weight::from_parts(0, 2559).saturating_mul(x.into()))
+	}
+	/// Storage: `Sassafras::PendingEpochConfigChange` (r:0 w:1)
+	/// Proof: `Sassafras::PendingEpochConfigChange` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`)
+	fn plan_config_change() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `0`
+		//  Estimated: `0`
+		// Minimum execution time: 4_038_000 picoseconds.
+		Weight::from_parts(4_499_000, 0)
+			.saturating_add(RocksDbWeight::get().writes(1_u64))
+	}
+	/// Storage: `Sassafras::RingContext` (r:1 w:0)
+	/// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::RingVerifierData` (r:0 w:1)
+	/// Proof: `Sassafras::RingVerifierData` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`)
+	/// The range of component `x` is `[1, 100]`.
+	fn update_ring_verifier(x: u32, ) -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `590485`
+		//  Estimated: `591809`
+		// Minimum execution time: 105_121_424_000 picoseconds.
+		Weight::from_parts(105_527_334_385, 591809)
+			// Standard Error: 2_933_910
+			.saturating_add(Weight::from_parts(96_136_261, 0).saturating_mul(x.into()))
+			.saturating_add(RocksDbWeight::get().reads(1_u64))
+			.saturating_add(RocksDbWeight::get().writes(1_u64))
+	}
+	/// Storage: `Sassafras::RingContext` (r:1 w:0)
+	/// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`)
+	fn load_ring_context() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `590485`
+		//  Estimated: `591809`
+		// Minimum execution time: 44_005_681_000 picoseconds.
+		Weight::from_parts(44_312_079_000, 591809)
+			.saturating_add(RocksDbWeight::get().reads(1_u64))
+	}
+	/// Storage: `Sassafras::SortedCandidates` (r:1 w:0)
+	/// Proof: `Sassafras::SortedCandidates` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::UnsortedSegments` (r:100 w:100)
+	/// Proof: `Sassafras::UnsortedSegments` (`max_values`: None, `max_size`: Some(2054), added: 4529, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::TicketsIds` (r:0 w:200)
+	/// Proof: `Sassafras::TicketsIds` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
+	/// Storage: `Sassafras::TicketsData` (r:0 w:12600)
+	/// Proof: `Sassafras::TicketsData` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
+	/// The range of component `x` is `[1, 100]`.
+	fn sort_segments(x: u32, ) -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `222 + x * (2060 ±0)`
+		//  Estimated: `4687 + x * (4529 ±0)`
+		// Minimum execution time: 183_501_000 picoseconds.
+		Weight::from_parts(183_501_000, 4687)
+			// Standard Error: 1_426_363
+			.saturating_add(Weight::from_parts(169_156_241, 0).saturating_mul(x.into()))
+			.saturating_add(RocksDbWeight::get().reads(1_u64))
+			.saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into())))
+			.saturating_add(RocksDbWeight::get().writes((129_u64).saturating_mul(x.into())))
+			.saturating_add(Weight::from_parts(0, 4529).saturating_mul(x.into()))
+	}
+}
diff --git a/substrate/primitives/consensus/sassafras/Cargo.toml b/substrate/primitives/consensus/sassafras/Cargo.toml
index 67f09e2b904..e71f82b4382 100644
--- a/substrate/primitives/consensus/sassafras/Cargo.toml
+++ b/substrate/primitives/consensus/sassafras/Cargo.toml
@@ -18,12 +18,12 @@ targets = ["x86_64-unknown-linux-gnu"]
 scale-codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false }
 scale-info = { version = "2.10.0", default-features = false, features = ["derive"] }
 serde = { version = "1.0.193", default-features = false, features = ["derive"], optional = true }
-sp-api = { default-features = false, path = "../../api" }
-sp-application-crypto = { default-features = false, path = "../../application-crypto", features = ["bandersnatch-experimental"] }
-sp-consensus-slots = { default-features = false, path = "../slots" }
-sp-core = { default-features = false, path = "../../core", features = ["bandersnatch-experimental"] }
-sp-runtime = { default-features = false, path = "../../runtime" }
-sp-std = { default-features = false, path = "../../std" }
+sp-api = { path = "../../api", default-features = false }
+sp-application-crypto = { path = "../../application-crypto", default-features = false, features = ["bandersnatch-experimental"] }
+sp-consensus-slots = { path = "../slots", default-features = false }
+sp-core = { path = "../../core", default-features = false, features = ["bandersnatch-experimental"] }
+sp-runtime = { path = "../../runtime", default-features = false }
+sp-std = { path = "../../std", default-features = false }
 
 [features]
 default = ["std"]
diff --git a/substrate/primitives/consensus/sassafras/README.md b/substrate/primitives/consensus/sassafras/README.md
index b0f3685494e..d6251940a49 100644
--- a/substrate/primitives/consensus/sassafras/README.md
+++ b/substrate/primitives/consensus/sassafras/README.md
@@ -1,12 +1,6 @@
 Primitives for SASSAFRAS.
 
-# ⚠️ WARNING ⚠️
+- Tracking issue: https://github.com/paritytech/polkadot-sdk/issues/41
+- RFC proposal: https://github.com/polkadot-fellows/RFCs/pull/26
 
-The crate interfaces and structures are highly experimental and may be subject
-to significant changes.
-
-Depends on upstream experimental feature: `bandersnatch-experimental`.
-
-These structs were mostly extracted from the main SASSAFRAS protocol PR: https://github.com/paritytech/polkadot-sdk/pull/1336.
-
-Tracking issue: https://github.com/paritytech/polkadot-sdk/issues/41
+Depends on `sp-core` feature: `bandersnatch-experimental`.
diff --git a/substrate/primitives/consensus/sassafras/src/digests.rs b/substrate/primitives/consensus/sassafras/src/digests.rs
index 95a305099de..5274f1309d8 100644
--- a/substrate/primitives/consensus/sassafras/src/digests.rs
+++ b/substrate/primitives/consensus/sassafras/src/digests.rs
@@ -48,11 +48,11 @@ pub struct SlotClaim {
 /// This is mandatory in the first block of each epoch.
 #[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)]
 pub struct NextEpochDescriptor {
+	/// Randomness value.
+	pub randomness: Randomness,
 	/// Authorities list.
 	pub authorities: Vec<AuthorityId>,
-	/// Epoch randomness.
-	pub randomness: Randomness,
-	/// Epoch configurable parameters.
+	/// Epoch configuration.
 	///
 	/// If not present previous epoch parameters are used.
 	pub config: Option<EpochConfiguration>,
diff --git a/substrate/primitives/consensus/sassafras/src/lib.rs b/substrate/primitives/consensus/sassafras/src/lib.rs
index e421e771d40..1752f765886 100644
--- a/substrate/primitives/consensus/sassafras/src/lib.rs
+++ b/substrate/primitives/consensus/sassafras/src/lib.rs
@@ -80,33 +80,43 @@ pub type EquivocationProof<H> = sp_consensus_slots::EquivocationProof<H, Authori
 /// Randomness required by some protocol's operations.
 pub type Randomness = [u8; RANDOMNESS_LENGTH];
 
-/// Configuration data that can be modified on epoch change.
+/// Protocol configuration that can be modified on epoch change.
+///
+/// Mostly tweaks to the ticketing system parameters.
 #[derive(
 	Copy, Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, MaxEncodedLen, TypeInfo, Default,
 )]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 pub struct EpochConfiguration {
-	/// Tickets threshold redundancy factor.
+	/// Tickets redundancy factor.
+	///
+	/// Expected ratio between epoch's slots and the cumulative number of tickets which can
+	/// be submitted by the set of epoch validators.
 	pub redundancy_factor: u32,
-	/// Tickets attempts for each validator.
+	/// Tickets max attempts for each validator.
+	///
+	/// Influences the anonymity of block producers. As all published tickets have a public
+	/// attempt number less than `attempts_number` if two tickets share an attempt number
+	/// then they must belong to two different validators, which reduces anonymity late as
+	/// we approach the epoch tail.
+	///
+	/// This anonymity loss already becomes small when `attempts_number = 64` or `128`.
 	pub attempts_number: u32,
 }
 
 /// Sassafras epoch information
 #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TypeInfo)]
 pub struct Epoch {
-	/// The epoch index.
-	pub epoch_idx: u64,
-	/// The starting slot of the epoch.
-	pub start_slot: Slot,
-	/// Slot duration in milliseconds.
-	pub slot_duration: SlotDuration,
-	/// Duration of epoch in slots.
-	pub epoch_duration: u64,
-	/// Authorities for the epoch.
-	pub authorities: Vec<AuthorityId>,
-	/// Randomness for the epoch.
+	/// Epoch index.
+	pub index: u64,
+	/// Starting slot of the epoch.
+	pub start: Slot,
+	/// Number of slots in the epoch.
+	pub length: u32,
+	/// Randomness value.
 	pub randomness: Randomness,
+	/// Authorities list.
+	pub authorities: Vec<AuthorityId>,
 	/// Epoch configuration.
 	pub config: EpochConfiguration,
 }
diff --git a/substrate/primitives/consensus/sassafras/src/ticket.rs b/substrate/primitives/consensus/sassafras/src/ticket.rs
index d81770c96d9..dc0a61990d3 100644
--- a/substrate/primitives/consensus/sassafras/src/ticket.rs
+++ b/substrate/primitives/consensus/sassafras/src/ticket.rs
@@ -62,10 +62,10 @@ pub struct TicketClaim {
 	pub erased_signature: EphemeralSignature,
 }
 
-/// Computes ticket-id maximum allowed value for a given epoch.
+/// Computes a boundary for [`TicketId`] maximum allowed value for a given epoch.
 ///
-/// Only ticket identifiers below this threshold should be considered for slot
-/// assignment.
+/// Only ticket identifiers below this threshold should be considered as candidates
+/// for slot assignment.
 ///
 /// The value is computed as `TicketId::MAX*(redundancy*slots)/(attempts*validators)`
 ///
@@ -76,16 +76,51 @@ pub struct TicketClaim {
 /// - `validators`: number of validators in epoch.
 ///
 /// If `attempts * validators = 0` then we return 0.
+///
+/// For details about the formula and implications refer to
+/// [*probabilities an parameters*](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS#probabilities-and-parameters)
+/// paragraph of the w3f introduction to the protocol.
+// TODO: replace with [RFC-26](https://github.com/polkadot-fellows/RFCs/pull/26)
+// "Tickets Threshold" paragraph once is merged
 pub fn ticket_id_threshold(
 	redundancy: u32,
 	slots: u32,
 	attempts: u32,
 	validators: u32,
 ) -> TicketId {
-	let den = attempts as u64 * validators as u64;
 	let num = redundancy as u64 * slots as u64;
+	let den = attempts as u64 * validators as u64;
 	TicketId::max_value()
 		.checked_div(den.into())
 		.unwrap_or_default()
 		.saturating_mul(num.into())
 }
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+
+	// This is a trivial example/check which just better explain explains the rationale
+	// behind the threshold.
+	//
+	// After this reading the formula should become obvious.
+	#[test]
+	fn ticket_id_threshold_trivial_check() {
+		// For an epoch with `s` slots we want to accept a number of tickets equal to ~s·r
+		let redundancy = 2;
+		let slots = 1000;
+		let attempts = 100;
+		let validators = 500;
+
+		let threshold = ticket_id_threshold(redundancy, slots, attempts, validators);
+		let threshold = threshold as f64 / TicketId::MAX as f64;
+
+		// We expect that the total number of tickets allowed to be submited
+		// is slots*redundancy
+		let avt = ((attempts * validators) as f64 * threshold) as u32;
+		assert_eq!(avt, slots * redundancy);
+
+		println!("threshold: {}", threshold);
+		println!("avt = {}", avt);
+	}
+}
diff --git a/substrate/primitives/consensus/sassafras/src/vrf.rs b/substrate/primitives/consensus/sassafras/src/vrf.rs
index d25a656f950..bdbac0aae03 100644
--- a/substrate/primitives/consensus/sassafras/src/vrf.rs
+++ b/substrate/primitives/consensus/sassafras/src/vrf.rs
@@ -23,7 +23,7 @@ use sp_consensus_slots::Slot;
 use sp_std::vec::Vec;
 
 pub use sp_core::bandersnatch::{
-	ring_vrf::{RingContext, RingProver, RingVerifier, RingVrfSignature},
+	ring_vrf::{RingContext, RingProver, RingVerifier, RingVerifierData, RingVrfSignature},
 	vrf::{VrfInput, VrfOutput, VrfSignData, VrfSignature},
 };
 
diff --git a/substrate/primitives/core/Cargo.toml b/substrate/primitives/core/Cargo.toml
index 9c556c07736..331d762e0d7 100644
--- a/substrate/primitives/core/Cargo.toml
+++ b/substrate/primitives/core/Cargo.toml
@@ -56,7 +56,7 @@ sp-runtime-interface = { path = "../runtime-interface", default-features = false
 # bls crypto
 w3f-bls = { version = "0.1.3", default-features = false, optional = true }
 # bandersnatch crypto
-bandersnatch_vrfs = { git = "https://github.com/w3f/ring-vrf", rev = "3ddc205", default-features = false, optional = true }
+bandersnatch_vrfs = { git = "https://github.com/w3f/ring-vrf", rev = "2019248", default-features = false, features = ["substrate-curves"], optional = true }
 
 [dev-dependencies]
 criterion = "0.4.0"
diff --git a/substrate/primitives/core/src/bandersnatch.rs b/substrate/primitives/core/src/bandersnatch.rs
index 78b7f12f9ff..1d666f13b62 100644
--- a/substrate/primitives/core/src/bandersnatch.rs
+++ b/substrate/primitives/core/src/bandersnatch.rs
@@ -20,13 +20,17 @@
 //!
 //! The primitive can operate both as a regular VRF or as an anonymized Ring VRF.
 
-#[cfg(feature = "std")]
+#[cfg(feature = "serde")]
 use crate::crypto::Ss58Codec;
 use crate::crypto::{
 	ByteArray, CryptoType, CryptoTypeId, Derive, Public as TraitPublic, UncheckedFrom, VrfPublic,
 };
 #[cfg(feature = "full_crypto")]
 use crate::crypto::{DeriveError, DeriveJunction, Pair as TraitPair, SecretStringError, VrfSecret};
+#[cfg(feature = "serde")]
+use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
+#[cfg(all(not(feature = "std"), feature = "serde"))]
+use sp_std::alloc::{format, string::String};
 
 use bandersnatch_vrfs::CanonicalSerialize;
 #[cfg(feature = "full_crypto")]
@@ -44,23 +48,12 @@ pub const CRYPTO_ID: CryptoTypeId = CryptoTypeId(*b"band");
 #[cfg(feature = "full_crypto")]
 pub const SIGNING_CTX: &[u8] = b"BandersnatchSigningContext";
 
-// Max ring domain size.
-const RING_DOMAIN_SIZE: usize = 1024;
-
 #[cfg(feature = "full_crypto")]
-const SEED_SERIALIZED_LEN: usize = 32;
-
-// Short-Weierstrass form serialized sizes.
-const PUBLIC_SERIALIZED_LEN: usize = 33;
-const SIGNATURE_SERIALIZED_LEN: usize = 65;
-const RING_SIGNATURE_SERIALIZED_LEN: usize = 755;
-const PREOUT_SERIALIZED_LEN: usize = 33;
+const SEED_SERIALIZED_SIZE: usize = 32;
 
-// Max size of serialized ring-vrf context params.
-//
-// This size is dependent on the ring domain size and the actual value
-// is equal to the SCALE encoded size of the `KZG` backend.
-const RING_CONTEXT_SERIALIZED_LEN: usize = 147716;
+const PUBLIC_SERIALIZED_SIZE: usize = 33;
+const SIGNATURE_SERIALIZED_SIZE: usize = 65;
+const PREOUT_SERIALIZED_SIZE: usize = 33;
 
 /// Bandersnatch public key.
 #[cfg_attr(feature = "full_crypto", derive(Hash))]
@@ -77,16 +70,16 @@ const RING_CONTEXT_SERIALIZED_LEN: usize = 147716;
 	MaxEncodedLen,
 	TypeInfo,
 )]
-pub struct Public(pub [u8; PUBLIC_SERIALIZED_LEN]);
+pub struct Public(pub [u8; PUBLIC_SERIALIZED_SIZE]);
 
-impl UncheckedFrom<[u8; PUBLIC_SERIALIZED_LEN]> for Public {
-	fn unchecked_from(raw: [u8; PUBLIC_SERIALIZED_LEN]) -> Self {
+impl UncheckedFrom<[u8; PUBLIC_SERIALIZED_SIZE]> for Public {
+	fn unchecked_from(raw: [u8; PUBLIC_SERIALIZED_SIZE]) -> Self {
 		Public(raw)
 	}
 }
 
-impl AsRef<[u8; PUBLIC_SERIALIZED_LEN]> for Public {
-	fn as_ref(&self) -> &[u8; PUBLIC_SERIALIZED_LEN] {
+impl AsRef<[u8; PUBLIC_SERIALIZED_SIZE]> for Public {
+	fn as_ref(&self) -> &[u8; PUBLIC_SERIALIZED_SIZE] {
 		&self.0
 	}
 }
@@ -107,17 +100,17 @@ impl TryFrom<&[u8]> for Public {
 	type Error = ();
 
 	fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
-		if data.len() != PUBLIC_SERIALIZED_LEN {
+		if data.len() != PUBLIC_SERIALIZED_SIZE {
 			return Err(())
 		}
-		let mut r = [0u8; PUBLIC_SERIALIZED_LEN];
+		let mut r = [0u8; PUBLIC_SERIALIZED_SIZE];
 		r.copy_from_slice(data);
 		Ok(Self::unchecked_from(r))
 	}
 }
 
 impl ByteArray for Public {
-	const LEN: usize = PUBLIC_SERIALIZED_LEN;
+	const LEN: usize = PUBLIC_SERIALIZED_SIZE;
 }
 
 impl TraitPublic for Public {}
@@ -142,16 +135,31 @@ impl sp_std::fmt::Debug for Public {
 	}
 }
 
+#[cfg(feature = "serde")]
+impl Serialize for Public {
+	fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+		serializer.serialize_str(&self.to_ss58check())
+	}
+}
+
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for Public {
+	fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+		Public::from_ss58check(&String::deserialize(deserializer)?)
+			.map_err(|e| de::Error::custom(format!("{:?}", e)))
+	}
+}
+
 /// Bandersnatch signature.
 ///
 /// The signature is created via the [`VrfSecret::vrf_sign`] using [`SIGNING_CTX`] as transcript
 /// `label`.
 #[cfg_attr(feature = "full_crypto", derive(Hash))]
 #[derive(Clone, Copy, PartialEq, Eq, Encode, Decode, PassByInner, MaxEncodedLen, TypeInfo)]
-pub struct Signature([u8; SIGNATURE_SERIALIZED_LEN]);
+pub struct Signature([u8; SIGNATURE_SERIALIZED_SIZE]);
 
-impl UncheckedFrom<[u8; SIGNATURE_SERIALIZED_LEN]> for Signature {
-	fn unchecked_from(raw: [u8; SIGNATURE_SERIALIZED_LEN]) -> Self {
+impl UncheckedFrom<[u8; SIGNATURE_SERIALIZED_SIZE]> for Signature {
+	fn unchecked_from(raw: [u8; SIGNATURE_SERIALIZED_SIZE]) -> Self {
 		Signature(raw)
 	}
 }
@@ -172,17 +180,17 @@ impl TryFrom<&[u8]> for Signature {
 	type Error = ();
 
 	fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
-		if data.len() != SIGNATURE_SERIALIZED_LEN {
+		if data.len() != SIGNATURE_SERIALIZED_SIZE {
 			return Err(())
 		}
-		let mut r = [0u8; SIGNATURE_SERIALIZED_LEN];
+		let mut r = [0u8; SIGNATURE_SERIALIZED_SIZE];
 		r.copy_from_slice(data);
 		Ok(Self::unchecked_from(r))
 	}
 }
 
 impl ByteArray for Signature {
-	const LEN: usize = SIGNATURE_SERIALIZED_LEN;
+	const LEN: usize = SIGNATURE_SERIALIZED_SIZE;
 }
 
 impl CryptoType for Signature {
@@ -204,7 +212,7 @@ impl sp_std::fmt::Debug for Signature {
 
 /// The raw secret seed, which can be used to reconstruct the secret [`Pair`].
 #[cfg(feature = "full_crypto")]
-type Seed = [u8; SEED_SERIALIZED_LEN];
+type Seed = [u8; SEED_SERIALIZED_SIZE];
 
 /// Bandersnatch secret key.
 #[cfg(feature = "full_crypto")]
@@ -232,10 +240,10 @@ impl TraitPair for Pair {
 	///
 	/// The slice must be 32 bytes long or it will return an error.
 	fn from_seed_slice(seed_slice: &[u8]) -> Result<Pair, SecretStringError> {
-		if seed_slice.len() != SEED_SERIALIZED_LEN {
+		if seed_slice.len() != SEED_SERIALIZED_SIZE {
 			return Err(SecretStringError::InvalidSeedLength)
 		}
-		let mut seed = [0; SEED_SERIALIZED_LEN];
+		let mut seed = [0; SEED_SERIALIZED_SIZE];
 		seed.copy_from_slice(seed_slice);
 		let secret = SecretKey::from_seed(&seed);
 		Ok(Pair { secret, seed })
@@ -266,7 +274,7 @@ impl TraitPair for Pair {
 
 	fn public(&self) -> Public {
 		let public = self.secret.to_public();
-		let mut raw = [0; PUBLIC_SERIALIZED_LEN];
+		let mut raw = [0; PUBLIC_SERIALIZED_SIZE];
 		public
 			.serialize_compressed(raw.as_mut_slice())
 			.expect("serialization length is constant and checked by test; qed");
@@ -344,7 +352,7 @@ pub mod vrf {
 
 	impl Encode for VrfOutput {
 		fn encode(&self) -> Vec<u8> {
-			let mut bytes = [0; PREOUT_SERIALIZED_LEN];
+			let mut bytes = [0; PREOUT_SERIALIZED_SIZE];
 			self.0
 				.serialize_compressed(bytes.as_mut_slice())
 				.expect("serialization length is constant and checked by test; qed");
@@ -354,21 +362,24 @@ pub mod vrf {
 
 	impl Decode for VrfOutput {
 		fn decode<R: codec::Input>(i: &mut R) -> Result<Self, codec::Error> {
-			let buf = <[u8; PREOUT_SERIALIZED_LEN]>::decode(i)?;
-			let preout = bandersnatch_vrfs::VrfPreOut::deserialize_compressed(buf.as_slice())
-				.map_err(|_| "vrf-preout decode error: bad preout")?;
+			let buf = <[u8; PREOUT_SERIALIZED_SIZE]>::decode(i)?;
+			let preout =
+				bandersnatch_vrfs::VrfPreOut::deserialize_compressed_unchecked(buf.as_slice())
+					.map_err(|_| "vrf-preout decode error: bad preout")?;
 			Ok(VrfOutput(preout))
 		}
 	}
 
+	impl EncodeLike for VrfOutput {}
+
 	impl MaxEncodedLen for VrfOutput {
 		fn max_encoded_len() -> usize {
-			<[u8; PREOUT_SERIALIZED_LEN]>::max_encoded_len()
+			<[u8; PREOUT_SERIALIZED_SIZE]>::max_encoded_len()
 		}
 	}
 
 	impl TypeInfo for VrfOutput {
-		type Identity = [u8; PREOUT_SERIALIZED_LEN];
+		type Identity = [u8; PREOUT_SERIALIZED_SIZE];
 
 		fn type_info() -> scale_info::Type {
 			Self::Identity::type_info()
@@ -395,10 +406,10 @@ pub mod vrf {
 	///   will contribute to the signature as well.
 	#[derive(Clone)]
 	pub struct VrfSignData {
-		/// VRF inputs to be signed.
-		pub inputs: VrfIosVec<VrfInput>,
 		/// Associated protocol transcript.
 		pub transcript: Transcript,
+		/// VRF inputs to be signed.
+		pub inputs: VrfIosVec<VrfInput>,
 	}
 
 	impl VrfSignData {
@@ -468,10 +479,10 @@ pub mod vrf {
 	/// Refer to [`VrfSignData`] for more details.
 	#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)]
 	pub struct VrfSignature {
-		/// VRF (pre)outputs.
-		pub outputs: VrfIosVec<VrfOutput>,
 		/// Transcript signature.
 		pub signature: Signature,
+		/// VRF (pre)outputs.
+		pub outputs: VrfIosVec<VrfOutput>,
 	}
 
 	#[cfg(feature = "full_crypto")]
@@ -539,7 +550,7 @@ pub mod vrf {
 			let outputs = VrfIosVec::truncate_from(outputs);
 
 			let mut signature =
-				VrfSignature { signature: Signature([0; SIGNATURE_SERIALIZED_LEN]), outputs };
+				VrfSignature { signature: Signature([0; SIGNATURE_SERIALIZED_SIZE]), outputs };
 
 			thin_signature
 				.proof
@@ -567,7 +578,7 @@ pub mod vrf {
 			data: &VrfSignData,
 			signature: &VrfSignature,
 		) -> bool {
-			let Ok(public) = PublicKey::deserialize_compressed(self.as_slice()) else {
+			let Ok(public) = PublicKey::deserialize_compressed_unchecked(self.as_slice()) else {
 				return false
 			};
 
@@ -577,10 +588,10 @@ pub mod vrf {
 			// Deserialize only the proof, the rest has already been deserialized
 			// This is another hack used because backend signature type is generic over
 			// the number of ios.
-			let Ok(proof) =
-				ThinVrfSignature::<0>::deserialize_compressed(signature.signature.as_ref())
-					.map(|s| s.proof)
-			else {
+			let Ok(proof) = ThinVrfSignature::<0>::deserialize_compressed_unchecked(
+				signature.signature.as_ref(),
+			)
+			.map(|s| s.proof) else {
 				return false
 			};
 			let signature = ThinVrfSignature { proof, preouts };
@@ -609,16 +620,100 @@ pub mod vrf {
 pub mod ring_vrf {
 	use super::{vrf::*, *};
 	pub use bandersnatch_vrfs::ring::{RingProof, RingProver, RingVerifier, KZG};
-	use bandersnatch_vrfs::{CanonicalDeserialize, PublicKey};
+	use bandersnatch_vrfs::{ring::VerifierKey, CanonicalDeserialize, PublicKey};
+
+	/// Ring max size (keyset max size).
+	pub const RING_MAX_SIZE: u32 = RING_DOMAIN_MAX_SIZE - RING_DOMAIN_OVERHEAD;
+
+	/// Ring domain max size.
+	pub const RING_DOMAIN_MAX_SIZE: u32 = 2048;
+
+	/// Overhead in the domain size over the max ring size.
+	///
+	/// Some bits of the domain are reserved for the zk proof to work.
+	pub(crate) const RING_DOMAIN_OVERHEAD: u32 = 257;
+
+	// Max size of serialized ring-vrf context params.
+	//
+	// The actual size is dependent on the ring domain size and this value
+	// has been computed for `RING_DOMAIN_MAX_SIZE` with compression disabled
+	// for performance reasons.
+	//
+	// 1024 uncompressed
+	// pub(crate) const RING_CONTEXT_SERIALIZED_MAX_SIZE: usize = 295412;
+	// 1024 compressed
+	// pub(crate) const RING_CONTEXT_SERIALIZED_MAX_SIZE: usize = 147716;
+	// 2048 uncompressed
+	pub(crate) const RING_CONTEXT_SERIALIZED_MAX_SIZE: usize = 590324;
+	// 2048 compressed
+	// pub(crate) const RING_CONTEXT_SERIALIZED_MAX_SIZE: usize = 295172;
+
+	pub(crate) const RING_VERIFIER_DATA_SERIALIZED_SIZE: usize = 388;
+	pub(crate) const RING_SIGNATURE_SERIALIZED_SIZE: usize = 755;
+
+	/// remove as soon as soon as serialization is implemented by the backend
+	pub struct RingVerifierData {
+		/// Domain size.
+		pub domain_size: u32,
+		/// Verifier key.
+		pub verifier_key: VerifierKey,
+	}
+
+	impl From<RingVerifierData> for RingVerifier {
+		fn from(vd: RingVerifierData) -> RingVerifier {
+			bandersnatch_vrfs::ring::make_ring_verifier(vd.verifier_key, vd.domain_size as usize)
+		}
+	}
+
+	impl Encode for RingVerifierData {
+		fn encode(&self) -> Vec<u8> {
+			const ERR_STR: &str = "serialization length is constant and checked by test; qed";
+			let mut buf = [0; RING_VERIFIER_DATA_SERIALIZED_SIZE];
+			self.domain_size.serialize_compressed(&mut buf[..4]).expect(ERR_STR);
+			self.verifier_key.serialize_compressed(&mut buf[4..]).expect(ERR_STR);
+			buf.encode()
+		}
+	}
+
+	impl Decode for RingVerifierData {
+		fn decode<R: codec::Input>(i: &mut R) -> Result<Self, codec::Error> {
+			const ERR_STR: &str = "serialization length is constant and checked by test; qed";
+			let buf = <[u8; RING_VERIFIER_DATA_SERIALIZED_SIZE]>::decode(i)?;
+			let domain_size =
+				<u32 as CanonicalDeserialize>::deserialize_compressed_unchecked(&mut &buf[..4])
+					.expect(ERR_STR);
+			let verifier_key = <bandersnatch_vrfs::ring::VerifierKey as CanonicalDeserialize>::deserialize_compressed_unchecked(&mut &buf[4..]).expect(ERR_STR);
+
+			Ok(RingVerifierData { domain_size, verifier_key })
+		}
+	}
 
-	/// Context used to produce ring signatures.
+	impl EncodeLike for RingVerifierData {}
+
+	impl MaxEncodedLen for RingVerifierData {
+		fn max_encoded_len() -> usize {
+			<[u8; RING_VERIFIER_DATA_SERIALIZED_SIZE]>::max_encoded_len()
+		}
+	}
+
+	impl TypeInfo for RingVerifierData {
+		type Identity = [u8; RING_VERIFIER_DATA_SERIALIZED_SIZE];
+
+		fn type_info() -> scale_info::Type {
+			Self::Identity::type_info()
+		}
+	}
+
+	/// Context used to construct ring prover and verifier.
 	#[derive(Clone)]
 	pub struct RingContext(KZG);
 
 	impl RingContext {
-		/// Build an dummy instance used for testing purposes.
+		/// Build an dummy instance for testing purposes.
+		///
+		/// `domain_size` is set to `RING_DOMAIN_MAX_SIZE`.
 		pub fn new_testing() -> Self {
-			Self(KZG::testing_kzg_setup([0; 32], RING_DOMAIN_SIZE as u32))
+			Self(KZG::testing_kzg_setup([0; 32], RING_DOMAIN_MAX_SIZE))
 		}
 
 		/// Get the keyset max size.
@@ -630,7 +725,7 @@ pub mod ring_vrf {
 		pub fn prover(&self, public_keys: &[Public], public_idx: usize) -> Option<RingProver> {
 			let mut pks = Vec::with_capacity(public_keys.len());
 			for public_key in public_keys {
-				let pk = PublicKey::deserialize_compressed(public_key.as_slice()).ok()?;
+				let pk = PublicKey::deserialize_compressed_unchecked(public_key.as_slice()).ok()?;
 				pks.push(pk.0.into());
 			}
 
@@ -643,7 +738,7 @@ pub mod ring_vrf {
 		pub fn verifier(&self, public_keys: &[Public]) -> Option<RingVerifier> {
 			let mut pks = Vec::with_capacity(public_keys.len());
 			for public_key in public_keys {
-				let pk = PublicKey::deserialize_compressed(public_key.as_slice()).ok()?;
+				let pk = PublicKey::deserialize_compressed_unchecked(public_key.as_slice()).ok()?;
 				pks.push(pk.0.into());
 			}
 
@@ -651,13 +746,26 @@ pub mod ring_vrf {
 			let ring_verifier = self.0.init_ring_verifier(verifier_key);
 			Some(ring_verifier)
 		}
+
+		/// Information required for a lazy construction of a ring verifier.
+		pub fn verifier_data(&self, public_keys: &[Public]) -> Option<RingVerifierData> {
+			let mut pks = Vec::with_capacity(public_keys.len());
+			for public_key in public_keys {
+				let pk = PublicKey::deserialize_compressed_unchecked(public_key.as_slice()).ok()?;
+				pks.push(pk.0.into());
+			}
+			Some(RingVerifierData {
+				verifier_key: self.0.verifier_key(pks),
+				domain_size: self.0.domain_size,
+			})
+		}
 	}
 
 	impl Encode for RingContext {
 		fn encode(&self) -> Vec<u8> {
-			let mut buf = Box::new([0; RING_CONTEXT_SERIALIZED_LEN]);
+			let mut buf = Box::new([0; RING_CONTEXT_SERIALIZED_MAX_SIZE]);
 			self.0
-				.serialize_compressed(buf.as_mut_slice())
+				.serialize_uncompressed(buf.as_mut_slice())
 				.expect("serialization length is constant and checked by test; qed");
 			buf.encode()
 		}
@@ -665,9 +773,9 @@ pub mod ring_vrf {
 
 	impl Decode for RingContext {
 		fn decode<R: codec::Input>(i: &mut R) -> Result<Self, codec::Error> {
-			let buf = <Box<[u8; RING_CONTEXT_SERIALIZED_LEN]>>::decode(i)?;
-			let kzg =
-				KZG::deserialize_compressed(buf.as_slice()).map_err(|_| "KZG decode error")?;
+			let buf = <Box<[u8; RING_CONTEXT_SERIALIZED_MAX_SIZE]>>::decode(i)?;
+			let kzg = KZG::deserialize_uncompressed_unchecked(buf.as_slice())
+				.map_err(|_| "KZG decode error")?;
 			Ok(RingContext(kzg))
 		}
 	}
@@ -676,12 +784,12 @@ pub mod ring_vrf {
 
 	impl MaxEncodedLen for RingContext {
 		fn max_encoded_len() -> usize {
-			<[u8; RING_CONTEXT_SERIALIZED_LEN]>::max_encoded_len()
+			<[u8; RING_CONTEXT_SERIALIZED_MAX_SIZE]>::max_encoded_len()
 		}
 	}
 
 	impl TypeInfo for RingContext {
-		type Identity = [u8; RING_CONTEXT_SERIALIZED_LEN];
+		type Identity = [u8; RING_CONTEXT_SERIALIZED_MAX_SIZE];
 
 		fn type_info() -> scale_info::Type {
 			Self::Identity::type_info()
@@ -691,10 +799,10 @@ pub mod ring_vrf {
 	/// Ring VRF signature.
 	#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)]
 	pub struct RingVrfSignature {
+		/// Ring signature.
+		pub signature: [u8; RING_SIGNATURE_SERIALIZED_SIZE],
 		/// VRF (pre)outputs.
 		pub outputs: VrfIosVec<VrfOutput>,
-		/// Ring signature.
-		pub signature: [u8; RING_SIGNATURE_SERIALIZED_LEN],
 	}
 
 	#[cfg(feature = "full_crypto")]
@@ -731,7 +839,7 @@ pub mod ring_vrf {
 			let outputs = VrfIosVec::truncate_from(outputs);
 
 			let mut signature =
-				RingVrfSignature { outputs, signature: [0; RING_SIGNATURE_SERIALIZED_LEN] };
+				RingVrfSignature { outputs, signature: [0; RING_SIGNATURE_SERIALIZED_SIZE] };
 
 			ring_signature
 				.proof
@@ -769,7 +877,7 @@ pub mod ring_vrf {
 			verifier: &RingVerifier,
 		) -> bool {
 			let Ok(vrf_signature) =
-				bandersnatch_vrfs::RingVrfSignature::<0>::deserialize_compressed(
+				bandersnatch_vrfs::RingVrfSignature::<0>::deserialize_compressed_unchecked(
 					self.signature.as_slice(),
 				)
 			else {
@@ -795,7 +903,7 @@ pub mod ring_vrf {
 mod tests {
 	use super::{ring_vrf::*, vrf::*, *};
 	use crate::crypto::{VrfPublic, VrfSecret, DEV_PHRASE};
-	const DEV_SEED: &[u8; SEED_SERIALIZED_LEN] = &[0xcb; SEED_SERIALIZED_LEN];
+	const DEV_SEED: &[u8; SEED_SERIALIZED_SIZE] = &[0xcb; SEED_SERIALIZED_SIZE];
 
 	#[allow(unused)]
 	fn b2h(bytes: &[u8]) -> String {
@@ -808,9 +916,10 @@ mod tests {
 
 	#[test]
 	fn backend_assumptions_sanity_check() {
-		let kzg = KZG::testing_kzg_setup([0; 32], RING_DOMAIN_SIZE as u32);
-		assert_eq!(kzg.max_keyset_size(), RING_DOMAIN_SIZE - 257);
-		assert_eq!(kzg.compressed_size(), RING_CONTEXT_SERIALIZED_LEN);
+		let kzg = KZG::testing_kzg_setup([0; 32], RING_DOMAIN_MAX_SIZE);
+		assert_eq!(kzg.max_keyset_size() as u32, RING_MAX_SIZE);
+
+		assert_eq!(kzg.uncompressed_size(), RING_CONTEXT_SERIALIZED_MAX_SIZE);
 
 		let pks: Vec<_> = (0..16)
 			.map(|i| SecretKey::from_seed(&[i as u8; 32]).to_public().0.into())
@@ -819,11 +928,14 @@ mod tests {
 		let secret = SecretKey::from_seed(&[0u8; 32]);
 
 		let public = secret.to_public();
-		assert_eq!(public.compressed_size(), PUBLIC_SERIALIZED_LEN);
+		assert_eq!(public.compressed_size(), PUBLIC_SERIALIZED_SIZE);
 
 		let input = VrfInput::new(b"foo", &[]);
 		let preout = secret.vrf_preout(&input.0);
-		assert_eq!(preout.compressed_size(), PREOUT_SERIALIZED_LEN);
+		assert_eq!(preout.compressed_size(), PREOUT_SERIALIZED_SIZE);
+
+		let verifier_key = kzg.verifier_key(pks.clone());
+		assert_eq!(verifier_key.compressed_size() + 4, RING_VERIFIER_DATA_SERIALIZED_SIZE);
 
 		let prover_key = kzg.prover_key(pks);
 		let ring_prover = kzg.init_ring_prover(prover_key, 0);
@@ -832,12 +944,12 @@ mod tests {
 
 		let thin_signature: bandersnatch_vrfs::ThinVrfSignature<0> =
 			secret.sign_thin_vrf(data.transcript.clone(), &[]);
-		assert_eq!(thin_signature.compressed_size(), SIGNATURE_SERIALIZED_LEN);
+		assert_eq!(thin_signature.compressed_size(), SIGNATURE_SERIALIZED_SIZE);
 
 		let ring_signature: bandersnatch_vrfs::RingVrfSignature<0> =
 			bandersnatch_vrfs::RingProver { ring_prover: &ring_prover, secret: &secret }
 				.sign_ring_vrf(data.transcript.clone(), &[]);
-		assert_eq!(ring_signature.compressed_size(), RING_SIGNATURE_SERIALIZED_LEN);
+		assert_eq!(ring_signature.compressed_size(), RING_SIGNATURE_SERIALIZED_SIZE);
 	}
 
 	#[test]
@@ -941,7 +1053,8 @@ mod tests {
 
 		let bytes = expected.encode();
 
-		let expected_len = data.inputs.len() * PREOUT_SERIALIZED_LEN + SIGNATURE_SERIALIZED_LEN + 1;
+		let expected_len =
+			data.inputs.len() * PREOUT_SERIALIZED_SIZE + SIGNATURE_SERIALIZED_SIZE + 1;
 		assert_eq!(bytes.len(), expected_len);
 
 		let decoded = VrfSignature::decode(&mut bytes.as_slice()).unwrap();
@@ -1055,7 +1168,7 @@ mod tests {
 		let bytes = expected.encode();
 
 		let expected_len =
-			data.inputs.len() * PREOUT_SERIALIZED_LEN + RING_SIGNATURE_SERIALIZED_LEN + 1;
+			data.inputs.len() * PREOUT_SERIALIZED_SIZE + RING_SIGNATURE_SERIALIZED_SIZE + 1;
 		assert_eq!(bytes.len(), expected_len);
 
 		let decoded = RingVrfSignature::decode(&mut bytes.as_slice()).unwrap();
@@ -1067,11 +1180,31 @@ mod tests {
 		let ctx1 = RingContext::new_testing();
 		let enc1 = ctx1.encode();
 
-		assert_eq!(enc1.len(), RingContext::max_encoded_len());
+		assert_eq!(enc1.len(), RING_CONTEXT_SERIALIZED_MAX_SIZE);
+		assert_eq!(RingContext::max_encoded_len(), RING_CONTEXT_SERIALIZED_MAX_SIZE);
 
 		let ctx2 = RingContext::decode(&mut enc1.as_slice()).unwrap();
 		let enc2 = ctx2.encode();
 
 		assert_eq!(enc1, enc2);
 	}
+
+	#[test]
+	fn encode_decode_verifier_data() {
+		let ring_ctx = RingContext::new_testing();
+
+		let pks: Vec<_> = (0..16).map(|i| Pair::from_seed(&[i as u8; 32]).public()).collect();
+		assert!(pks.len() <= ring_ctx.max_keyset_size());
+
+		let verifier_data = ring_ctx.verifier_data(&pks).unwrap();
+		let enc1 = verifier_data.encode();
+
+		assert_eq!(enc1.len(), RING_VERIFIER_DATA_SERIALIZED_SIZE);
+		assert_eq!(RingVerifierData::max_encoded_len(), RING_VERIFIER_DATA_SERIALIZED_SIZE);
+
+		let vd2 = RingVerifierData::decode(&mut enc1.as_slice()).unwrap();
+		let enc2 = vd2.encode();
+
+		assert_eq!(enc1, enc2);
+	}
 }
-- 
GitLab