Sassafras Consensus Pallet (#1577)

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>
This commit is contained in:
Davide Galassi
2023-12-01 16:39:07 +01:00
committed by GitHub
parent 7f0beaf2a5
commit 095f4bd9ae
21 changed files with 3763 additions and 127 deletions
Generated
+39 -11
View File
@@ -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",
+1
View File
@@ -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",
+59
View File
@@ -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 = []
+8
View File
@@ -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.
@@ -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);
}
}
@@ -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
@@ -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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because it is too large Load Diff
+343
View File
@@ -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
}
+874
View File
@@ -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));
});
}
+425
View File
@@ -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()))
}
}
@@ -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"]
@@ -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`.
@@ -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>,
@@ -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,
}
@@ -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);
}
}
@@ -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},
};
+1 -1
View File
@@ -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"
+211 -78
View File
@@ -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;
const SEED_SERIALIZED_SIZE: 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;
// 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};
/// Context used to produce ring signatures.
/// 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 })
}
}
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);
}
}