feat: remove sessions (#2850)

This commit is contained in:
Antonio 2026-06-03 17:10:33 +02:00 committed by GitHub
parent 3a7b51a577
commit 49212e111e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
159 changed files with 5552 additions and 8832 deletions

View File

@ -48,7 +48,7 @@ Use log levels consistently so logs are easier to read and easier to filter by t
- `info`
- Use for important events that explain the node's current state or major transitions.
- Someone reading only `info` logs should still understand whether the node is starting, syncing, progressing, changing epoch/session, proposing blocks, or hitting notable conditions.
- Someone reading only `info` logs should still understand whether the node is starting, syncing, progressing, changing epoch, proposing blocks, or hitting notable conditions.
- `debug`
- Use for details that explain why the node behaved a certain way.

332
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -163,12 +163,12 @@ lb-zksign = { default-features = false, package = "logo
lb-zone-sdk = { default-features = false, package = "logos-blockchain-zone-sdk", path = "./zone-sdk" }
lb_config = { default-features = false, package = "lb_config", path = "./tools/config" }
lb_network = { default-features = false, package = "logos-blockchain-network-service", path = "./services/network" }
lbc-poc-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-poc-sys", rev = "e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" } # v0.5.0
lbc-pol-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-pol-sys", rev = "e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" } # v0.5.0
lbc-poq-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-poq-sys", rev = "e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" } # v0.5.0
lbc-rapidsnark-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-rapidsnark-sys", rev = "e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" } # v0.5.0
lbc-signature-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-signature-sys", rev = "e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" } # v0.5.0
lbc-types = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-types", rev = "e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" } # v0.5.0
lbc-poc-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-poc-sys", tag = "v0.5.1" }
lbc-pol-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-pol-sys", tag = "v0.5.1" }
lbc-poq-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-poq-sys", tag = "v0.5.1" }
lbc-rapidsnark-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-rapidsnark-sys", tag = "v0.5.1" }
lbc-signature-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-signature-sys", tag = "v0.5.1" }
lbc-types = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-types", tag = "v0.5.1" }
lbp-error = { default-features = false, package = "logos-blockchain-proofs-error", path = "./zk/proofs/error" }
testing-framework-core = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-testing.git", tag = "dev_snapshot_2026_06_03_4733e56a" }
testing-framework-runner-compose = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-testing.git", tag = "dev_snapshot_2026_06_03_4733e56a" }

View File

@ -20,12 +20,12 @@ itertools = { workspace = true }
lb-blend-crypto = { workspace = true }
lb-blend-proofs = { workspace = true }
lb-core = { workspace = true }
lb-cryptarchia-engine = { workspace = true }
lb-groth16 = { workspace = true }
lb-key-management-system-keys = { workspace = true }
lb-log-targets = { workspace = true }
lb-utils = { features = ["rng"], workspace = true }
serde = { workspace = true }
serde-big-array = { workspace = true }
serde_with = { features = ["alloc"], workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }

View File

@ -1,4 +1,3 @@
use core::mem::swap;
use std::time::Instant;
use lb_blend_proofs::{
@ -23,7 +22,6 @@ use crate::encap::ProofsVerifier;
/// verified.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct PoQVerificationInputsMinusSigningKey {
pub session: u64,
pub core: CoreInputs,
pub leader: LeaderInputs,
}
@ -35,7 +33,6 @@ impl Default for PoQVerificationInputsMinusSigningKey {
use lb_groth16::{Field as _, Fr};
Self {
session: 1,
core: CoreInputs {
zk_root: ZkHash::default(),
quota: 1,
@ -63,7 +60,6 @@ pub enum Error {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RealProofsVerifier {
current_inputs: PoQVerificationInputsMinusSigningKey,
previous_epoch_inputs: Option<LeaderInputs>,
}
impl ProofsVerifier for RealProofsVerifier {
@ -73,41 +69,20 @@ impl ProofsVerifier for RealProofsVerifier {
tracing::trace!("Generating new proof verifier with public inputs: {public_inputs:?}");
Self {
current_inputs: public_inputs,
previous_epoch_inputs: None,
}
}
fn start_epoch_transition(&mut self, new_pol_inputs: LeaderInputs) {
let old_epoch_inputs = {
let mut new_pol_inputs = new_pol_inputs;
swap(&mut self.current_inputs.leader, &mut new_pol_inputs);
new_pol_inputs
};
tracing::trace!(
"Transitioning epochs for proof verifier from: {old_epoch_inputs:?} to: {new_pol_inputs:?}"
);
self.previous_epoch_inputs = Some(old_epoch_inputs);
}
fn complete_epoch_transition(&mut self) {
self.previous_epoch_inputs = None;
}
fn verify_proof_of_quota(
&self,
proof: ProofOfQuota,
signing_key: &Ed25519PublicKey,
) -> Result<VerifiedProofOfQuota, Self::Error> {
let PoQVerificationInputsMinusSigningKey {
core,
leader,
session,
} = self.current_inputs;
let PoQVerificationInputsMinusSigningKey { core, leader } = self.current_inputs;
// Try with current input, and if it fails, try with the previous one, if any
// (i.e., within the epoch transition period).
tracing::trace!(
"Verifying proof of quota with key nullifier {:?}, signing key: {signing_key:?}, session {session:?}, public core inputs: {core:?} and leader inputs: {leader:?}.",
"Verifying proof of quota with key nullifier {:?}, signing key: {signing_key:?}, public core inputs: {core:?} and leader inputs: {leader:?}.",
hex::encode(fr_to_bytes(&proof.key_nullifier()))
);
let start = Instant::now();
@ -115,31 +90,9 @@ impl ProofsVerifier for RealProofsVerifier {
.verify(&PublicInputs {
core,
leader,
session,
signing_key: *signing_key.as_inner(),
})
.or_else(|_| {
let Some(previous_epoch_inputs) = self.previous_epoch_inputs else {
tracing::debug!("Input proof invalid and no previous epoch to try with");
return Err(Error::ProofOfQuota(quota::Error::InvalidProof));
};
tracing::trace!(
"Verifying same proof of quota with previous epoch leader inputs: {previous_epoch_inputs:?}."
);
proof
.verify(&PublicInputs {
core,
leader: previous_epoch_inputs,
session,
signing_key: *signing_key.as_inner(),
})
.map_err(Error::ProofOfQuota)
.inspect_err(|_| {
tracing::debug!(
"Input proof invalid with both current and previous epoch public inputs"
);
})
});
.map_err(Error::ProofOfQuota);
tracing::trace!(
"Proof verification time: {} ms.",
@ -157,130 +110,3 @@ impl ProofsVerifier for RealProofsVerifier {
proof.verify(inputs).map_err(Error::ProofOfSelection)
}
}
#[cfg(test)]
mod tests {
use lb_blend_proofs::quota::inputs::prove::public::LeaderInputs;
use lb_core::crypto::ZkHash;
use lb_groth16::{Field as _, Fr};
use crate::{
crypto::proofs::{PoQVerificationInputsMinusSigningKey, RealProofsVerifier},
encap::ProofsVerifier as _,
};
fn epoch_1_leader() -> LeaderInputs {
LeaderInputs {
pol_ledger_aged: ZkHash::ONE,
pol_epoch_nonce: ZkHash::ONE,
message_quota: 2,
lottery_0: Fr::ONE,
lottery_1: Fr::ONE,
}
}
#[test]
fn new_verifier_has_no_previous_epoch() {
let verifier = RealProofsVerifier::new(PoQVerificationInputsMinusSigningKey::default());
assert!(verifier.previous_epoch_inputs.is_none());
assert_eq!(
verifier.current_inputs.leader,
PoQVerificationInputsMinusSigningKey::default().leader
);
}
#[test]
fn start_epoch_transition_stores_previous_epoch() {
let initial = PoQVerificationInputsMinusSigningKey::default();
let mut verifier = RealProofsVerifier::new(initial);
let new_leader = epoch_1_leader();
verifier.start_epoch_transition(new_leader);
// Current should be updated to new epoch.
assert_eq!(verifier.current_inputs.leader, new_leader);
// Previous should hold the old epoch's leader inputs.
assert_eq!(verifier.previous_epoch_inputs, Some(initial.leader));
}
#[test]
fn complete_epoch_transition_clears_previous_epoch() {
let mut verifier = RealProofsVerifier::new(PoQVerificationInputsMinusSigningKey::default());
verifier.start_epoch_transition(epoch_1_leader());
assert!(verifier.previous_epoch_inputs.is_some());
verifier.complete_epoch_transition();
assert!(
verifier.previous_epoch_inputs.is_none(),
"Previous epoch inputs must be cleared after completing transition"
);
assert_eq!(verifier.current_inputs.leader, epoch_1_leader());
}
#[test]
fn consecutive_epoch_transitions_replace_previous() {
let initial = PoQVerificationInputsMinusSigningKey::default();
let mut verifier = RealProofsVerifier::new(initial);
let leader_1 = epoch_1_leader();
verifier.start_epoch_transition(leader_1);
assert_eq!(verifier.previous_epoch_inputs, Some(initial.leader));
// Start another transition without completing the first.
let leader_2 = LeaderInputs {
pol_ledger_aged: ZkHash::ZERO,
pol_epoch_nonce: ZkHash::ONE,
message_quota: 3,
lottery_0: Fr::ZERO,
lottery_1: Fr::ONE,
};
verifier.start_epoch_transition(leader_2);
// Previous should now be epoch 1 (not initial epoch 0).
assert_eq!(verifier.current_inputs.leader, leader_2);
assert_eq!(verifier.previous_epoch_inputs, Some(leader_1));
}
#[test]
fn complete_then_new_epoch_transition() {
let initial = PoQVerificationInputsMinusSigningKey::default();
let mut verifier = RealProofsVerifier::new(initial);
// Epoch 0 → 1
let leader_1 = epoch_1_leader();
verifier.start_epoch_transition(leader_1);
verifier.complete_epoch_transition();
assert!(verifier.previous_epoch_inputs.is_none());
assert_eq!(verifier.current_inputs.leader, leader_1);
// Epoch 1 → 2
let leader_2 = LeaderInputs {
pol_ledger_aged: ZkHash::ZERO,
pol_epoch_nonce: ZkHash::ONE,
message_quota: 3,
lottery_0: Fr::ZERO,
lottery_1: Fr::ONE,
};
verifier.start_epoch_transition(leader_2);
assert_eq!(verifier.current_inputs.leader, leader_2);
assert_eq!(
verifier.previous_epoch_inputs,
Some(leader_1),
"After new transition, previous must be the completed epoch 1"
);
}
#[test]
fn session_and_core_inputs_preserved_across_epoch_transitions() {
let initial = PoQVerificationInputsMinusSigningKey::default();
let mut verifier = RealProofsVerifier::new(initial);
verifier.start_epoch_transition(epoch_1_leader());
// Session and core inputs should not change during epoch transitions.
assert_eq!(verifier.current_inputs.session, initial.session);
assert_eq!(verifier.current_inputs.core, initial.core);
}
}

View File

@ -1,5 +1,5 @@
use lb_blend_proofs::{
quota::{ProofOfQuota, VerifiedProofOfQuota, inputs::prove::public::LeaderInputs},
quota::{ProofOfQuota, VerifiedProofOfQuota},
selection::{ProofOfSelection, VerifiedProofOfSelection, inputs::VerifyInputs},
};
use lb_key_management_system_keys::keys::Ed25519PublicKey;
@ -13,21 +13,14 @@ pub mod validated;
#[cfg(test)]
mod tests;
/// A session-bound `PoQ` verifier.
/// An epoch-bound `PoQ` verifier.
pub trait ProofsVerifier {
type Error;
/// Create a new proof verifier with the public inputs corresponding to the
/// current Blend session and cryptarchia epoch.
/// current epoch.
fn new(public_inputs: PoQVerificationInputsMinusSigningKey) -> Self;
/// Start a new epoch while still maintaining the old one around for
/// messages that are propagated around the bound between two epochs.
fn start_epoch_transition(&mut self, new_pol_inputs: LeaderInputs);
/// Complete the transition period and discard any messages generated in the
/// previous epoch.
fn complete_epoch_transition(&mut self);
/// Proof of Quota verification logic.
fn verify_proof_of_quota(
&self,

View File

@ -1,7 +1,7 @@
use core::convert::Infallible;
use lb_blend_proofs::{
quota::{ProofOfQuota, VerifiedProofOfQuota, inputs::prove::public::LeaderInputs},
quota::{ProofOfQuota, VerifiedProofOfQuota},
selection::{ProofOfSelection, VerifiedProofOfSelection, inputs::VerifyInputs},
};
use lb_core::codec::{DeserializeOp as _, SerializeOp as _};
@ -33,10 +33,6 @@ impl ProofsVerifier for NeverFailingProofsVerifier {
Self
}
fn start_epoch_transition(&mut self, _new_pol_inputs: LeaderInputs) {}
fn complete_epoch_transition(&mut self) {}
fn verify_proof_of_quota(
&self,
proof: ProofOfQuota,
@ -65,10 +61,6 @@ impl ProofsVerifier for AlwaysFailingProofOfQuotaVerifier {
Self
}
fn start_epoch_transition(&mut self, _new_pol_inputs: LeaderInputs) {}
fn complete_epoch_transition(&mut self) {}
fn verify_proof_of_quota(
&self,
_proof: ProofOfQuota,
@ -97,10 +89,6 @@ impl ProofsVerifier for AlwaysFailingProofOfSelectionVerifier {
Self
}
fn start_epoch_transition(&mut self, _new_pol_inputs: LeaderInputs) {}
fn complete_epoch_transition(&mut self) {}
fn verify_proof_of_quota(
&self,
proof: ProofOfQuota,

View File

@ -24,7 +24,7 @@ pub enum Error {
SignatureVerificationFailed,
#[error("Node is not a core node")]
NotCoreNodeReceiver,
#[error("Node has generated the maximum number of allowed Proof of Quota this session")]
#[error("Node has generated the maximum number of allowed Proof of Quota this epoch")]
NoMoreProofOfQuotas,
#[error("Attempted to generate a leadership proof without any secret PoL info provided.")]
NoLeadershipInfoProvided,

View File

@ -1,6 +1,6 @@
use derivative::Derivative;
use lb_blend_proofs::selection::inputs::VerifyInputs;
use lb_core::sdp::SessionNumber;
use lb_cryptarchia_engine::Epoch;
use serde::Serialize;
use tracing::debug;
@ -12,23 +12,20 @@ use crate::{
},
};
/// An activity proof for a session, made of the blending token
/// An activity proof for an epoch, made of the blending token
/// that has the smallest Hamming distance satisfying the activity threshold.
#[derive(Derivative, Serialize)]
#[derivative(Debug)]
pub struct ActivityProof {
session_number: SessionNumber,
epoch: Epoch,
#[derivative(Debug = "ignore")]
token: BlendingToken,
}
impl ActivityProof {
#[must_use]
pub const fn new(session_number: SessionNumber, token: BlendingToken) -> Self {
Self {
session_number,
token,
}
pub const fn new(epoch: Epoch, token: BlendingToken) -> Self {
Self { epoch, token }
}
#[must_use]
@ -57,14 +54,14 @@ impl ActivityProof {
)?;
Ok(Self::new(
proof.session,
proof.epoch,
BlendingToken::new(proof.signing_key, proof_of_quota, proof_of_selection),
))
}
}
/// Computes the activity threshold, which is the expected maximum Hamming
/// distance from any blending token in a session to the next session
/// distance from any blending token in an epoch to the next epoch
/// randomness.
pub fn activity_threshold(
token_count_bit_len: u64,
@ -86,7 +83,7 @@ pub fn activity_threshold(
impl From<&ActivityProof> for lb_core::sdp::blend::ActivityProof {
fn from(proof: &ActivityProof) -> Self {
Self {
session: proof.session_number,
epoch: proof.epoch,
signing_key: *proof.token.signing_key(),
proof_of_quota: (*proof.token.proof_of_quota()).into(),
proof_of_selection: (*proof.token.proof_of_selection()).into(),

View File

@ -1,30 +1,30 @@
use core::fmt::{self, Debug, Formatter};
use std::ops::{Add as _, Deref};
use std::ops::Add as _;
use lb_blend_crypto::blake2b512;
use lb_core::{crypto::ZkHash, sdp::SessionNumber};
use lb_groth16::fr_to_bytes;
use lb_core::crypto::ZkHash;
use lb_cryptarchia_engine::Epoch;
use lb_groth16::{Fr, FrBytes, fr_to_bytes};
use lb_utils::math::{F64Ge1, NonNegativeF64};
use serde::{Deserialize, Serialize};
use crate::reward::{BlendingToken, activity, token::HammingDistance};
/// Session-specific information to compute an activity proof.
pub struct SessionInfo {
pub(crate) session_number: SessionNumber,
pub(crate) session_randomness: SessionRandomness,
/// Epoch-specific information to compute an activity proof.
pub struct EpochInfo {
pub(crate) epoch: Epoch,
pub(crate) epoch_randomness: EpochRandomness,
pub(crate) token_evaluation: BlendingTokenEvaluation,
}
impl SessionInfo {
impl EpochInfo {
pub fn new(
session_number: SessionNumber,
epoch: Epoch,
pol_epoch_nonce: &ZkHash,
num_core_nodes: u64,
core_quota: u64,
activity_threshold_sensitivity: u64,
) -> Result<Self, Error> {
let session_randomness = SessionRandomness::new(session_number, pol_epoch_nonce);
let epoch_randomness = (*pol_epoch_nonce).into();
let token_evaluation = BlendingTokenEvaluation::new(
core_quota,
num_core_nodes,
@ -32,19 +32,19 @@ impl SessionInfo {
)?;
Ok(Self {
session_number,
session_randomness,
epoch,
epoch_randomness,
token_evaluation,
})
}
#[must_use]
pub const fn session_randomness(&self) -> SessionRandomness {
self.session_randomness
pub const fn epoch_randomness(&self) -> EpochRandomness {
self.epoch_randomness
}
}
/// Parameters to evaluate a blending token for a session.
/// Parameters to evaluate a blending token for an epoch.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlendingTokenEvaluation {
token_count_byte_len: u64,
@ -71,15 +71,15 @@ impl BlendingTokenEvaluation {
}
/// Calculate the Hamming distance of the given blending token to the next
/// session randomness, and return it only if it is not larger than the
/// epoch randomness, and return it only if it is not larger than the
/// activity threshold.
#[must_use]
pub fn evaluate(
&self,
token: &BlendingToken,
next_session_randomness: SessionRandomness,
next_epoch_randomness: EpochRandomness,
) -> Option<HammingDistance> {
let distance = token.hamming_distance(self.token_count_byte_len, next_session_randomness);
let distance = token.hamming_distance(self.token_count_byte_len, next_epoch_randomness);
tracing::trace!(
"Evaluated blending token {:?} for activity proof. Calculated Hamming distance = {distance:?}",
token.signing_key()
@ -93,42 +93,26 @@ impl BlendingTokenEvaluation {
}
}
/// Deterministic unbiased randomness for a session.
/// Deterministic unbiased randomness for an epoch.
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionRandomness(#[serde(with = "serde_big_array::BigArray")] [u8; 64]);
pub struct EpochRandomness(#[serde(with = "lb_groth16::serde::serde_fr")] Fr);
impl Debug for SessionRandomness {
impl Debug for EpochRandomness {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "SessionRandomness({})", hex::encode(self.0))
write!(f, "EpochRandomness({})", hex::encode(self.as_bytes()))
}
}
impl Deref for SessionRandomness {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.0
impl From<Fr> for EpochRandomness {
fn from(value: Fr) -> Self {
Self(value)
}
}
impl From<[u8; 64]> for SessionRandomness {
fn from(bytes: [u8; 64]) -> Self {
Self(bytes)
}
}
const SESSION_RANDOMNESS_TAG: [u8; 27] = *b"BLEND_SESSION_RANDOMNESS_V1";
impl SessionRandomness {
/// Derive the session randomness from the given session number and epoch
/// nonce.
impl EpochRandomness {
#[must_use]
pub fn new(session_number: SessionNumber, epoch_nonce: &ZkHash) -> Self {
Self(blake2b512(&[
&SESSION_RANDOMNESS_TAG,
&fr_to_bytes(epoch_nonce),
&session_number.to_le_bytes(),
]))
pub fn as_bytes(&self) -> FrBytes {
fr_to_bytes(&self.0)
}
}
@ -141,7 +125,7 @@ pub enum Error {
}
/// The number of bits that can represent the maximum number of blending
/// tokens generated during a single session.
/// tokens generated during a single epoch.
pub fn token_count_bit_len(core_quota: u64, num_core_nodes: u64) -> Result<u64, Error> {
let total_core_quota = core_quota
.checked_mul(num_core_nodes)
@ -179,6 +163,7 @@ pub fn activity_threshold(
#[cfg(test)]
mod tests {
use lb_blend_proofs::{quota::VerifiedProofOfQuota, selection::VerifiedProofOfSelection};
use lb_groth16::Field as _;
use lb_key_management_system_keys::keys::Ed25519Key;
use super::*;
@ -231,17 +216,17 @@ mod tests {
VerifiedProofOfQuota::from_bytes_unchecked([0; _]),
VerifiedProofOfSelection::from_bytes_unchecked([0; _]),
),
SessionRandomness::from([0; 64]),
EpochRandomness::from(Fr::ZERO),
);
assert_eq!(maybe_distance, Some(8.into()));
let maybe_distance = evaluation.evaluate(
&BlendingToken::new(
Ed25519Key::from_bytes(&[1; _]).public_key(),
VerifiedProofOfQuota::from_bytes_unchecked([1; _]),
VerifiedProofOfSelection::from_bytes_unchecked([1; _]),
Ed25519Key::from_bytes(&[0; _]).public_key(),
VerifiedProofOfQuota::from_bytes_unchecked([0; _]),
VerifiedProofOfSelection::from_bytes_unchecked([0; _]),
),
SessionRandomness::from([0; 64]),
EpochRandomness::from(ZkHash::from(100)),
);
assert_eq!(maybe_distance, None);
}

View File

@ -1,34 +1,34 @@
mod activity;
mod session;
mod epoch;
mod token;
use std::collections::HashSet;
pub use activity::ActivityProof;
use lb_core::sdp::SessionNumber;
pub use epoch::EpochInfo;
use lb_cryptarchia_engine::Epoch;
use lb_log_targets::blend;
use serde::{Deserialize, Serialize};
pub use session::SessionInfo;
pub use token::{BlendingToken, HammingDistance};
pub use crate::reward::session::{BlendingTokenEvaluation, Error, SessionRandomness};
pub use crate::reward::epoch::{BlendingTokenEvaluation, EpochRandomness, Error};
const LOG_TARGET: &str = blend::message::REWARD;
/// Holds blending tokens collected during a single session.
/// Holds blending tokens collected during a single epoch.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionBlendingTokenCollector {
session_number: SessionNumber,
pub struct EpochBlendingTokenCollector {
epoch: Epoch,
token_evaluation: BlendingTokenEvaluation,
tokens: HashSet<BlendingToken>,
}
impl SessionBlendingTokenCollector {
impl EpochBlendingTokenCollector {
#[must_use]
pub fn new(session_info: &SessionInfo) -> Self {
pub fn new(epoch_info: &EpochInfo) -> Self {
Self {
session_number: session_info.session_number,
token_evaluation: session_info.token_evaluation,
epoch: epoch_info.epoch,
token_evaluation: epoch_info.token_evaluation,
tokens: HashSet::new(),
}
}
@ -38,21 +38,21 @@ impl SessionBlendingTokenCollector {
}
#[must_use]
pub fn rotate_session(
pub fn rotate_epoch(
self,
new_session_info: &SessionInfo,
) -> (Self, OldSessionBlendingTokenCollector) {
let new_collector = Self::new(new_session_info);
let old_collector = OldSessionBlendingTokenCollector {
new_epoch_info: &EpochInfo,
) -> (Self, OldEpochBlendingTokenCollector) {
let new_collector = Self::new(new_epoch_info);
let old_collector = OldEpochBlendingTokenCollector {
collector: self,
next_session_randomness: new_session_info.session_randomness(),
next_epoch_randomness: new_epoch_info.epoch_randomness(),
};
(new_collector, old_collector)
}
#[must_use]
pub const fn session_number(&self) -> SessionNumber {
self.session_number
pub const fn epoch(&self) -> Epoch {
self.epoch
}
#[cfg(any(test, feature = "unsafe-test-functions"))]
@ -62,19 +62,19 @@ impl SessionBlendingTokenCollector {
}
}
/// Holds blending tokens collected during the old session.
/// Holds blending tokens collected during the old epoch.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OldSessionBlendingTokenCollector {
collector: SessionBlendingTokenCollector,
next_session_randomness: SessionRandomness,
pub struct OldEpochBlendingTokenCollector {
collector: EpochBlendingTokenCollector,
next_epoch_randomness: EpochRandomness,
}
impl OldSessionBlendingTokenCollector {
impl OldEpochBlendingTokenCollector {
pub fn collect(&mut self, token: BlendingToken) {
self.collector.collect(token);
}
/// Computes an activity proof for this session by consuming tokens.
/// Computes an activity proof for this epoch by consuming tokens.
///
/// It returns `None` if there was no blending token satisfying the
/// activity threshold calculated.
@ -84,10 +84,10 @@ impl OldSessionBlendingTokenCollector {
// which is <= activity threshold.
tracing::trace!(
LOG_TARGET,
"Computing activity proof for session {} with activity threshold {:?} and next session randomness {:?} for {} candidate tokens",
self.collector.session_number(),
"Computing activity proof for epoch {} with activity threshold {:?} and next epoch randomness {:?} for {} candidate tokens",
self.collector.epoch(),
self.collector.token_evaluation.activity_threshold(),
self.next_session_randomness,
self.next_epoch_randomness,
self.collector.tokens.len()
);
let (winning_activity_proof, distance) = self
@ -100,7 +100,7 @@ impl OldSessionBlendingTokenCollector {
let distance = self
.collector
.token_evaluation
.evaluate(&token, self.next_session_randomness)
.evaluate(&token, self.next_epoch_randomness)
.map(|distance| (token, distance));
tracing::trace!(
@ -111,25 +111,20 @@ impl OldSessionBlendingTokenCollector {
distance
})
.min_by_key(|(_, distance)| *distance)
.map(|(token, distance)| {
(
ActivityProof::new(self.collector.session_number, token),
distance,
)
})?;
.map(|(token, distance)| (ActivityProof::new(self.collector.epoch, token), distance))?;
tracing::trace!(
LOG_TARGET,
"Computed activity proof for session {}: {winning_activity_proof:?} with Hamming distance {distance:?}",
self.collector.session_number,
"Computed activity proof for epoch {}: {winning_activity_proof:?} with Hamming distance {distance:?}",
self.collector.epoch,
);
Some(winning_activity_proof)
}
#[must_use]
pub const fn session_number(&self) -> SessionNumber {
self.collector.session_number()
pub const fn epoch(&self) -> Epoch {
self.collector.epoch()
}
#[cfg(any(test, feature = "unsafe-test-functions"))]
@ -159,9 +154,9 @@ mod tests {
fn test_blending_token_collector() {
let num_core_nodes = 2;
let core_quota = 15;
let session_info =
SessionInfo::new(1, &ZkHash::from(1), num_core_nodes, core_quota, 1).unwrap();
let mut tokens = SessionBlendingTokenCollector::new(&session_info);
let epoch_info =
EpochInfo::new(1.into(), &ZkHash::from(1), num_core_nodes, core_quota, 1).unwrap();
let mut tokens = EpochBlendingTokenCollector::new(&epoch_info);
assert!(tokens.tokens().is_empty());
// Insert `total_core_quota-1` tokens.
@ -176,10 +171,10 @@ mod tests {
i += 1;
}
// Prepare a new session info.
let session_info =
SessionInfo::new(2, &ZkHash::from(2), num_core_nodes, core_quota, 1).unwrap();
let (_, mut tokens) = tokens.rotate_session(&session_info);
// Prepare a new epoch info.
let epoch_info =
EpochInfo::new(2.into(), &ZkHash::from(2), num_core_nodes, core_quota, 1).unwrap();
let (_, mut tokens) = tokens.rotate_epoch(&epoch_info);
// Insert one more tokens.
// Now,`total_core_quota` tokens have been collected.

View File

@ -7,7 +7,7 @@ use lb_core::codec::SerializeOp as _;
use lb_key_management_system_keys::keys::Ed25519PublicKey;
use serde::{Deserialize, Serialize};
use crate::reward::session::SessionRandomness;
use crate::reward::epoch::EpochRandomness;
/// A blending token consisting of a proof of quota and a proof of selection.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
@ -32,20 +32,23 @@ impl BlendingToken {
}
/// Computes the Hamming distance between this blending token and the next
/// session randomness.
/// epoch randomness.
#[must_use]
pub fn hamming_distance(
&self,
token_count_byte_len: u64,
next_session_randomness: SessionRandomness,
next_epoch_randomness: EpochRandomness,
) -> HammingDistance {
let token = self
.to_bytes()
.expect("BlendingToken should be serializable");
let token_hash = hash(&token, token_count_byte_len as usize);
let session_randomness_hash = hash(&next_session_randomness, token_count_byte_len as usize);
let epoch_randomness_hash = hash(
&next_epoch_randomness.as_bytes(),
token_count_byte_len as usize,
);
HammingDistance::new(&token_hash, &session_randomness_hash)
HammingDistance::new(&token_hash, &epoch_randomness_hash)
}
#[must_use]
@ -107,6 +110,7 @@ impl From<u64> for HammingDistance {
#[cfg(test)]
mod tests {
use lb_blend_proofs::{quota::PROOF_OF_QUOTA_SIZE, selection::PROOF_OF_SELECTION_SIZE};
use lb_groth16::{Field as _, Fr};
use lb_key_management_system_keys::keys::Ed25519Key;
use super::*;
@ -150,7 +154,7 @@ mod tests {
#[test]
fn test_blending_token_hamming_distance() {
let token = blending_token(1, 1, 2);
assert_eq!(token.hamming_distance(1, [3u8; 64].into()), 2.into());
assert_eq!(token.hamming_distance(1, Fr::ONE.into()), 6.into());
}
fn blending_token(

View File

@ -20,7 +20,7 @@ hex = { workspace = true }
lb-blend-message = { workspace = true }
lb-blend-proofs = { workspace = true }
lb-blend-scheduling = { workspace = true }
lb-core = { workspace = true }
lb-cryptarchia-engine = { workspace = true }
lb-key-management-system-keys = { workspace = true }
lb-libp2p = { workspace = true }
lb-log-targets = { workspace = true }

View File

@ -5,6 +5,7 @@ pub mod with_edge;
mod tests;
use lb_blend_scheduling::membership::Membership;
use lb_cryptarchia_engine::Epoch;
use libp2p::{PeerId, StreamProtocol};
use self::{
@ -33,7 +34,7 @@ impl<ObservationWindowClockProvider> NetworkBehaviour<ObservationWindowClockProv
pub fn new(
config: &Config,
observation_window_clock_provider: ObservationWindowClockProvider,
current_session_info: (Membership<PeerId>, u64),
current_epoch_info: (Membership<PeerId>, Epoch),
local_peer_id: PeerId,
protocol_name: StreamProtocol,
) -> Self {
@ -41,22 +42,21 @@ impl<ObservationWindowClockProvider> NetworkBehaviour<ObservationWindowClockProv
with_core: CoreToCoreBehaviour::new(
&config.with_core,
observation_window_clock_provider,
current_session_info.clone(),
current_epoch_info.clone(),
local_peer_id,
protocol_name.clone(),
),
with_edge: CoreToEdgeBehaviour::new(
&config.with_edge,
current_session_info.0,
current_epoch_info.0,
protocol_name,
),
}
}
pub fn start_new_session(&mut self, new_session_info: (Membership<PeerId>, u64)) {
self.with_core_mut()
.start_new_session(new_session_info.clone());
self.with_edge_mut().start_new_session(new_session_info.0);
pub fn start_new_epoch(&mut self, new_epoch_info: (Membership<PeerId>, Epoch)) {
self.with_core_mut().start_new_epoch(new_epoch_info.clone());
self.with_edge_mut().start_new_epoch(new_epoch_info.0);
}
pub const fn with_core(&self) -> &CoreToCoreBehaviour<ObservationWindowClockProvider> {
@ -77,7 +77,7 @@ impl<ObservationWindowClockProvider> NetworkBehaviour<ObservationWindowClockProv
&mut self.with_edge
}
pub fn finish_session_transition(&mut self) {
self.with_core_mut().finish_session_transition();
pub fn finish_epoch_transition(&mut self) {
self.with_core_mut().finish_epoch_transition();
}
}

View File

@ -11,7 +11,7 @@ use lb_blend_message::{
};
use lb_blend_proofs::{quota::VerifiedProofOfQuota, selection::VerifiedProofOfSelection};
use lb_blend_scheduling::message_blend::provers::BlendLayerProof;
use lb_core::sdp::SessionNumber;
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::{Ed25519Signature, UnsecuredEd25519Key};
use lb_libp2p::{NetworkBehaviour, ed25519, upgrade::Version};
use libp2p::{
@ -99,7 +99,7 @@ impl TestEncapsulatedMessage {
pub fn new(payload: &[u8]) -> Self {
Self(
EncapsulatedMessageWithVerifiedPublicHeader::try_new(
&generate_valid_inputs(0),
&generate_valid_inputs(0.into()),
PayloadType::Data,
payload.try_into().unwrap(),
)
@ -128,13 +128,13 @@ impl AsRef<EncapsulatedMessageWithVerifiedPublicHeader> for TestEncapsulatedMess
}
}
pub struct TestEncapsulatedMessageWithSession(EncapsulatedMessageWithVerifiedPublicHeader);
pub struct TestEncapsulatedMessageWithEpoch(EncapsulatedMessageWithVerifiedPublicHeader);
impl TestEncapsulatedMessageWithSession {
pub fn new(session: SessionNumber, payload: &[u8]) -> Self {
impl TestEncapsulatedMessageWithEpoch {
pub fn new(epoch: Epoch, payload: &[u8]) -> Self {
Self(
EncapsulatedMessageWithVerifiedPublicHeader::try_new(
&generate_valid_inputs(session),
&generate_valid_inputs(epoch),
PayloadType::Data,
payload.try_into().unwrap(),
)
@ -143,7 +143,7 @@ impl TestEncapsulatedMessageWithSession {
}
}
impl Deref for TestEncapsulatedMessageWithSession {
impl Deref for TestEncapsulatedMessageWithEpoch {
type Target = EncapsulatedMessageWithVerifiedPublicHeader;
fn deref(&self) -> &Self::Target {
@ -151,11 +151,11 @@ impl Deref for TestEncapsulatedMessageWithSession {
}
}
fn generate_valid_inputs(session: SessionNumber) -> Vec<EncapsulationInput> {
fn generate_valid_inputs(epoch: Epoch) -> Vec<EncapsulationInput> {
repeat_with(UnsecuredEd25519Key::generate_with_blake_rng)
.take(3)
.map(|recipient_signing_key| {
let proofs = session_based_mock_blend_proof(session);
let proofs = epoch_based_mock_blend_proof(epoch);
EncapsulationInput::try_new(
UnsecuredEd25519Key::generate_with_blake_rng(),
&recipient_signing_key.public_key(),
@ -167,17 +167,17 @@ fn generate_valid_inputs(session: SessionNumber) -> Vec<EncapsulationInput> {
.collect::<Vec<_>>()
}
fn session_based_mock_blend_proof(session: SessionNumber) -> BlendLayerProof {
let session_bytes = session.to_le_bytes();
fn epoch_based_mock_blend_proof(epoch: Epoch) -> BlendLayerProof {
let epoch_bytes = epoch.into_inner().to_le_bytes();
BlendLayerProof {
proof_of_quota: VerifiedProofOfQuota::from_bytes_unchecked({
let mut bytes = [0u8; _];
bytes[..session_bytes.len()].copy_from_slice(&session_bytes);
bytes[..epoch_bytes.len()].copy_from_slice(&epoch_bytes);
bytes
}),
proof_of_selection: VerifiedProofOfSelection::from_bytes_unchecked({
let mut bytes = [0u8; _];
bytes[..session_bytes.len()].copy_from_slice(&session_bytes);
bytes[..epoch_bytes.len()].copy_from_slice(&epoch_bytes);
bytes
}),
ephemeral_signing_key: UnsecuredEd25519Key::generate_with_blake_rng(),

View File

@ -15,6 +15,7 @@ use lb_blend_message::encap::validated::{
EncapsulatedMessageWithVerifiedPublicHeader, EncapsulatedMessageWithVerifiedSignature,
};
use lb_blend_scheduling::membership::Membership;
use lb_cryptarchia_engine::Epoch;
use lb_log_targets::blend;
use libp2p::{
Multiaddr, PeerId, StreamProtocol,
@ -32,7 +33,7 @@ use crate::core::with_core::{
ConnectionHandler, FromBehaviour, ToBehaviour, conn_maintenance::ConnectionMonitor,
},
message_cache::MessageCache,
old_session::OldSession,
old_epoch::OldEpoch,
utils::{
forward_validated_message_and_update_cache,
handle_received_serialized_encapsulated_message_and_update_cache,
@ -43,7 +44,7 @@ use crate::core::with_core::{
mod handler;
mod message_cache;
mod old_session;
mod old_epoch;
mod utils;
#[cfg(test)]
@ -112,16 +113,16 @@ pub struct Behaviour<ObservationWindowClockProvider> {
/// as malicious by our peers.
message_cache: MessageCache,
observation_window_clock_provider: ObservationWindowClockProvider,
current_session_info: (Membership<PeerId>, u64),
current_epoch_info: (Membership<PeerId>, Epoch),
/// The [minimum, maximum] peering degree of this node.
peering_degree: RangeInclusive<usize>,
local_peer_id: PeerId,
protocol_name: StreamProtocol,
/// The minimum Blend network size for messages to be relayed between peers.
minimum_network_size: NonZeroUsize,
/// States for processing messages from the old session
/// States for processing messages from the old epoch
/// before the transition period has passed.
old_session: Option<OldSession>,
old_epoch: Option<OldEpoch>,
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
@ -185,7 +186,7 @@ pub enum Event {
Message {
message: Box<EncapsulatedMessageWithVerifiedSignature>,
sender: PeerId,
session: u64,
epoch: Epoch,
},
/// A peer on a given connection has been detected as unhealthy.
UnhealthyPeer(PeerId),
@ -221,7 +222,7 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
pub fn new(
config: &Config,
observation_window_clock_provider: ObservationWindowClockProvider,
session_info: (Membership<PeerId>, u64),
epoch_info: (Membership<PeerId>, Epoch),
local_peer_id: PeerId,
protocol_name: StreamProtocol,
) -> Self {
@ -230,19 +231,19 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
events: VecDeque::new(),
waker: None,
observation_window_clock_provider,
message_cache: MessageCache::new_with_peer_capacity(session_info.0.size()),
current_session_info: session_info,
message_cache: MessageCache::new_with_peer_capacity(epoch_info.0.size()),
current_epoch_info: epoch_info,
peering_degree: config.peering_degree.clone(),
connections_waiting_upgrade: HashMap::new(),
local_peer_id,
protocol_name,
minimum_network_size: config.minimum_network_size,
old_session: None,
old_epoch: None,
}
}
pub(crate) fn start_new_session(&mut self, new_session_info: (Membership<PeerId>, u64)) {
let current_session_number = self.current_session_info.1;
pub(crate) fn start_new_epoch(&mut self, new_epoch_info: (Membership<PeerId>, Epoch)) {
let current_epoch_number = self.current_epoch_info.1;
// Close any connections that were still waiting to be upgraded. Without
// this, a late `FullyNegotiated` event from one of those handlers would
@ -253,29 +254,29 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
for (connection, _) in pending_upgrades {
self.close_connection(connection);
}
self.current_session_info = new_session_info;
self.current_epoch_info = new_epoch_info;
self.stop_old_session();
self.stop_old_epoch();
self.old_session = Some(OldSession::new(
self.old_epoch = Some(OldEpoch::new(
mem::take(&mut self.negotiated_peers)
.into_iter()
.map(|(peer_id, details)| (peer_id, details.connection_id))
.collect(),
mem::take(&mut self.message_cache),
current_session_number,
current_epoch_number,
));
tracing::debug!(target: LOG_TARGET, "Started a new session by passing negotiated peers and exchanged message IDs to the old session. Now, no negotiated peers in the current session.");
tracing::debug!(target: LOG_TARGET, "Started a new epoch by passing negotiated peers and exchanged message IDs to the old epoch. Now, no negotiated peers in the current epoch.");
}
pub(crate) fn finish_session_transition(&mut self) {
self.stop_old_session();
pub(crate) fn finish_epoch_transition(&mut self) {
self.stop_old_epoch();
}
fn stop_old_session(&mut self) {
if let Some(old_session) = self.old_session.take() {
let mut events = old_session.stop();
fn stop_old_epoch(&mut self) {
if let Some(old_epoch) = self.old_epoch.take() {
let mut events = old_epoch.stop();
let num_events = events.len();
self.events.append(&mut events);
if num_events > 0 {
@ -306,61 +307,61 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
/// Force send a message to a peer, as long as the peer is connected, no
/// matter the state the connection is in.
#[cfg(any(test, feature = "unsafe-test-functions"))]
pub fn force_send_message_to_current_session_peer(
pub fn force_send_message_to_current_epoch_peer(
&mut self,
message: &EncapsulatedMessageWithVerifiedPublicHeader,
peer_id: PeerId,
) -> Result<(), SendError> {
self.force_send_message_to_peer_at_session(message, peer_id, self.current_session_info.1)
self.force_send_message_to_peer_at_epoch(message, peer_id, self.current_epoch_info.1)
}
/// Force send a message to a peer, as long as the peer is connected, no
/// matter the state the connection is in.
#[cfg(any(test, feature = "unsafe-test-functions"))]
fn force_send_message_to_peer_at_session(
fn force_send_message_to_peer_at_epoch(
&mut self,
message: &EncapsulatedMessageWithVerifiedPublicHeader,
peer_id: PeerId,
session: u64,
epoch: Epoch,
) -> Result<(), SendError> {
let serialized_message =
lb_blend_scheduling::serialize_encapsulated_message_with_verified_public_header(
message,
);
self.force_send_serialized_message_to_peer_at_session(serialized_message, peer_id, session)
self.force_send_serialized_message_to_peer_at_epoch(serialized_message, peer_id, epoch)
}
/// Force send a serialized message to a peer (without trying to deserialize
/// nor validating it first), as long as the peer is connected, no
/// matter the state the connection is in.
#[cfg(test)]
fn force_send_serialized_message_to_current_session_peer(
fn force_send_serialized_message_to_current_epoch_peer(
&mut self,
serialized_message: Vec<u8>,
peer_id: PeerId,
) -> Result<(), SendError> {
self.force_send_serialized_message_to_peer_at_session(
self.force_send_serialized_message_to_peer_at_epoch(
serialized_message,
peer_id,
self.current_session_info.1,
self.current_epoch_info.1,
)
}
#[cfg(any(test, feature = "unsafe-test-functions"))]
pub fn force_send_serialized_message_to_peer_at_session(
pub fn force_send_serialized_message_to_peer_at_epoch(
&mut self,
serialized_message: Vec<u8>,
peer_id: PeerId,
session: u64,
epoch: Epoch,
) -> Result<(), SendError> {
if session != self.current_session_info.1 {
let Some(old_session) = &mut self.old_session else {
return Err(SendError::InvalidSession);
if epoch != self.current_epoch_info.1 {
let Some(old_epoch) = &mut self.old_epoch else {
return Err(SendError::InvalidEpoch);
};
return old_session.force_send_serialized_message_to_peer_at_session(
return old_epoch.force_send_serialized_message_to_peer_at_epoch(
serialized_message,
peer_id,
session,
epoch,
);
}
@ -372,7 +373,7 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
tracing::trace!(
target: LOG_TARGET,
"Notifying handler with peer {peer_id:?} on current session connection {connection_id:?} to deliver already-serialized message."
"Notifying handler with peer {peer_id:?} on current epoch connection {connection_id:?} to deliver already-serialized message."
);
self.events.push_back(ToSwarm::NotifyHandler {
peer_id,
@ -387,12 +388,10 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
&self.negotiated_peers
}
/// Returns the peer IDs of the old session's negotiated peers, if a
/// session transition is in progress.
pub fn old_session_peer_ids(&self) -> Option<impl Iterator<Item = &PeerId> + '_> {
self.old_session
.as_ref()
.map(OldSession::negotiated_peer_ids)
/// Returns the peer IDs of the old epoch's negotiated peers, if an
/// epoch transition is in progress.
pub fn old_epoch_peer_ids(&self) -> Option<impl Iterator<Item = &PeerId> + '_> {
self.old_epoch.as_ref().map(OldEpoch::negotiated_peer_ids)
}
fn try_wake(&mut self) {
@ -455,7 +454,7 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
}
fn is_network_large_enough(&self) -> bool {
self.current_session_info.0.size() >= self.minimum_network_size.get()
self.current_epoch_info.0.size() >= self.minimum_network_size.get()
}
/// Handle a new negotiated connection.
@ -473,7 +472,7 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
/// returned from the `Either::Left` branch of
/// [`Self::handle_established_inbound_connection`]
/// [`Self::handle_established_outbound_connection`]), so the entry must be
/// present. Pending entries are proactively closed on session transition to
/// present. Pending entries are proactively closed on epoch transition to
/// preserve this invariant.
///
/// # Panics
@ -513,7 +512,7 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
remote_peer_role: Endpoint,
) {
// We need to check if we still have available connection slots, as it is
// possible, especially upon session transition, that more than the maximum
// possible, especially upon epoch transition, that more than the maximum
// allowed number of peers are trying to connect to us. So once the stream is
// actually upgraded, we downgrade it again if we do not have space left for it.
// By not adding the new connection to the map of negotiated peers, the swarm
@ -703,7 +702,7 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
) -> Option<NegotiatedPeerState> {
let peer_details = self.negotiated_peers.get_mut(&peer_id)?;
// We double check we are dealing with the expected connection.
// This could be false if `connection_id` is from the old session.
// This could be false if `connection_id` is from the old epoch.
if peer_details.connection_id != connection_id {
tracing::trace!(
target: LOG_TARGET,
@ -715,7 +714,7 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
Some(mem::replace(&mut peer_details.negotiated_state, state))
}
/// Handle an unhealthy connection if it exists in the current session.
/// Handle an unhealthy connection if it exists in the current epoch.
/// If not, it is ignored.
#[expect(
dead_code,
@ -735,7 +734,7 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
}
}
/// Handle a unhealthy connection if it exists in the current session.
/// Handle a unhealthy connection if it exists in the current epoch.
/// If not, it is ignored.
fn handle_healthy_connection(&mut self, (peer_id, connection_id): (PeerId, ConnectionId)) {
// Notify swarm only on first transition into healthy state.
@ -806,24 +805,24 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
}
/// Publish an already-encapsulated and validated message to all connected
/// peers in the specified session.
/// peers in the specified epoch.
pub fn publish_message_with_validated_header(
&mut self,
message: EncapsulatedMessageWithVerifiedPublicHeader,
intended_session: u64,
intended_epoch: Epoch,
) -> Result<(), SendError> {
if self.current_session_info.1 != intended_session {
let Some(old_session) = &mut self.old_session else {
return Err(SendError::InvalidSession);
if self.current_epoch_info.1 != intended_epoch {
let Some(old_epoch) = &mut self.old_epoch else {
return Err(SendError::InvalidEpoch);
};
return old_session.publish_message_with_validated_header(message, intended_session);
return old_epoch.publish_message_with_validated_header(message, intended_epoch);
}
self.forward_maybe_excluding(&message.into(), None)
}
/// Publish an already-encapsulated message with a valid public header
/// signature to all connected peers in the current session.
pub fn publish_message_with_validated_signature_to_current_session(
/// signature to all connected peers in the current epoch.
pub fn publish_message_with_validated_signature_to_current_epoch(
&mut self,
message: &EncapsulatedMessageWithVerifiedSignature,
) -> Result<(), SendError> {
@ -831,30 +830,30 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
}
/// Forwards a message with a valid public header signature to all
/// non-spammy peers in the specified session, except the
/// non-spammy peers in the specified epoch, except the
/// [`except`] peer.
///
/// If the session is the previous session, the message is forwarded to the
/// peers in the old session. Otherwise, it is forwarded to the peers in
/// the current session.
/// If the epoch is the previous epoch, the message is forwarded to the
/// peers in the old epoch. Otherwise, it is forwarded to the peers in
/// the current epoch.
///
/// Returns [`Error::NoPeers`] if there are no connected peers that support
/// the blend protocol, and [`Error::InvalidSession`] if the provided
/// session does not match neither the current session nor the old session.
/// the blend protocol, and [`Error::InvalidEpoch`] if the provided
/// epoch does not match neither the current epoch nor the old epoch.
pub fn forward_message_with_validated_signature(
&mut self,
message: &EncapsulatedMessageWithVerifiedSignature,
except: PeerId,
intended_session: u64,
intended_epoch: Epoch,
) -> Result<(), SendError> {
if self.current_session_info.1 != intended_session {
let Some(old_session) = &mut self.old_session else {
return Err(SendError::InvalidSession);
if self.current_epoch_info.1 != intended_epoch {
let Some(old_epoch) = &mut self.old_epoch else {
return Err(SendError::InvalidEpoch);
};
return old_session.forward_message_with_validated_signature(
return old_epoch.forward_message_with_validated_signature(
message,
except,
intended_session,
intended_epoch,
);
}
@ -867,7 +866,7 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
excluded_peer: Option<PeerId>,
) -> Result<(), SendError> {
tracing::trace!(
"Forwarding message with id {:?} to current session peers. Negotiated peers: {:?}. Excluded peer: {excluded_peer:?}",
"Forwarding message with id {:?} to current epoch peers. Negotiated peers: {:?}. Excluded peer: {excluded_peer:?}",
hex::encode(message.id()),
self.negotiated_peers()
);
@ -897,10 +896,10 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
serialized_message: &[u8],
(from_peer_id, from_connection_id): (PeerId, ConnectionId),
) {
// First, try to handle the message in the context of the old session.
// If it is not part of the old session, try with the current session.
if let Some(old_session) = &mut self.old_session {
match old_session.handle_received_serialized_encapsulated_message(
// First, try to handle the message in the context of the old epoch.
// If it is not part of the old epoch, try with the current epoch.
if let Some(old_epoch) = &mut self.old_epoch {
match old_epoch.handle_received_serialized_encapsulated_message(
serialized_message,
(from_peer_id, from_connection_id),
) {
@ -921,9 +920,9 @@ impl<ObservationWindowClockProvider> Behaviour<ObservationWindowClockProvider> {
from_peer_id,
&mut self.events,
self.waker.take(),
self.current_session_info.1,
self.current_epoch_info.1,
) {
tracing::debug!(target: LOG_TARGET, "Failed to handle message from the current session: {receive_error:?}");
tracing::debug!(target: LOG_TARGET, "Failed to handle message from the current epoch: {receive_error:?}");
let spam_reason = match receive_error {
ReceiveError::DuplicateMessageFromPeer(_) => SpamReason::DuplicateMessage,
ReceiveError::InvalidHeaderSignature => SpamReason::InvalidHeaderSignature,
@ -986,7 +985,7 @@ where
Ok(if !self.is_network_large_enough() {
tracing::debug!(target: LOG_TARGET, "Denying inbound connection {connection_id:?} with peer {peer_id:?} because membership size is too small.");
Either::Right(DummyConnectionHandler)
} else if self.current_session_info.0.contains(&peer_id) {
} else if self.current_epoch_info.0.contains(&peer_id) {
tracing::trace!(
target: LOG_TARGET,
"Upgrading inbound connection {connection_id:?} with core peer {peer_id:?}."
@ -1035,7 +1034,7 @@ where
Ok(if !self.is_network_large_enough() {
tracing::debug!(target: LOG_TARGET, "Denying outbound connection {connection_id:?} with peer {peer_id:?} because membership size is too small.");
Either::Right(DummyConnectionHandler)
} else if self.current_session_info.0.contains(&peer_id) {
} else if self.current_epoch_info.0.contains(&peer_id) {
tracing::trace!(
target: LOG_TARGET,
"Upgrading outbound connection {connection_id:?} with core peer {peer_id:?}."
@ -1062,9 +1061,9 @@ where
..
}) = event
{
// Try to close the connection if it exists in the old session.
if let Some(old_session) = &mut self.old_session
&& old_session.handle_closed_connection(&(peer_id, connection_id))
// Try to close the connection if it exists in the old epoch.
if let Some(old_epoch) = &mut self.old_epoch
&& old_epoch.handle_closed_connection(&(peer_id, connection_id))
{
return;
}
@ -1173,8 +1172,8 @@ where
&mut self,
cx: &mut Context<'_>,
) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
if let Some(old_session) = &mut self.old_session
&& let Poll::Ready(event) = old_session.poll(cx)
if let Some(old_epoch) = &mut self.old_epoch
&& let Poll::Ready(event) = old_epoch.poll(cx)
{
return Poll::Ready(event);
}

View File

@ -8,6 +8,7 @@ use either::Either;
use lb_blend_message::encap::validated::{
EncapsulatedMessageWithVerifiedPublicHeader, EncapsulatedMessageWithVerifiedSignature,
};
use lb_cryptarchia_engine::Epoch;
use lb_log_targets::blend;
use libp2p::{
PeerId,
@ -29,44 +30,44 @@ use crate::core::with_core::{
const LOG_TARGET: &str = blend::network::core::core::behaviour::OLD;
/// Defines behaviours for processing messages from the old session
/// until the session transition period has passed.
pub struct OldSession {
/// Defines behaviours for processing messages from the old epoch
/// until the epoch transition period has passed.
pub struct OldEpoch {
negotiated_peers: HashMap<PeerId, ConnectionId>,
events: VecDeque<ToSwarm<Event, Either<FromBehaviour, Infallible>>>,
waker: Option<Waker>,
message_cache: MessageCache,
session_number: u64,
epoch: Epoch,
}
impl OldSession {
impl OldEpoch {
#[must_use]
pub const fn new(
negotiated_peers: HashMap<PeerId, ConnectionId>,
message_cache: MessageCache,
session_number: u64,
epoch: Epoch,
) -> Self {
Self {
negotiated_peers,
message_cache,
events: VecDeque::new(),
waker: None,
session_number,
epoch,
}
}
/// Publish an encapsulated message with a validated public header to all
/// negotiated peers.
///
/// If the specified session does not match the current session, it returns
/// If the specified epoch does not match the current epoch, it returns
/// an error without sending the message.
pub(super) fn publish_message_with_validated_header(
&mut self,
message: EncapsulatedMessageWithVerifiedPublicHeader,
intended_session: u64,
intended_epoch: Epoch,
) -> Result<(), SendError> {
if self.session_number != intended_session {
return Err(SendError::InvalidSession);
if self.epoch != intended_epoch {
return Err(SendError::InvalidEpoch);
}
forward_validated_message_and_update_cache(
&(message.into()),
@ -80,16 +81,16 @@ impl OldSession {
/// Forward an encapsulated message with a validated signature to all
/// negotiated peers, except the specified one.
///
/// If the specified session does not match the current session, it returns
/// If the specified epoch does not match the current epoch, it returns
/// an error without sending the message.
pub(super) fn forward_message_with_validated_signature(
&mut self,
message: &EncapsulatedMessageWithVerifiedSignature,
except: PeerId,
intended_session: u64,
intended_epoch: Epoch,
) -> Result<(), SendError> {
if self.session_number != intended_session {
return Err(SendError::InvalidSession);
if self.epoch != intended_epoch {
return Err(SendError::InvalidEpoch);
}
forward_validated_message_and_update_cache(
message,
@ -104,14 +105,14 @@ impl OldSession {
}
#[cfg(any(test, feature = "unsafe-test-functions"))]
pub(super) fn force_send_serialized_message_to_peer_at_session(
pub(super) fn force_send_serialized_message_to_peer_at_epoch(
&mut self,
serialized_message: Vec<u8>,
peer_id: PeerId,
session: u64,
epoch: Epoch,
) -> Result<(), SendError> {
if session != self.session_number {
return Err(SendError::InvalidSession);
if epoch != self.epoch {
return Err(SendError::InvalidEpoch);
}
let Some(connection_id) = self.negotiated_peers.get(&peer_id) else {
@ -119,7 +120,7 @@ impl OldSession {
};
tracing::trace!(
target: LOG_TARGET,
"Notifying handler with peer {peer_id:?} on old session connection {connection_id:?} to deliver already-serialized message."
"Notifying handler with peer {peer_id:?} on old epoch connection {connection_id:?} to deliver already-serialized message."
);
self.events.push_back(ToSwarm::NotifyHandler {
peer_id,
@ -133,7 +134,7 @@ impl OldSession {
/// Handles a message received from a peer.
///
/// # Returns
/// - [`Ok(false)`] if the connection is not part of the session.
/// - [`Ok(false)`] if the connection is not part of the epoch.
/// - [`Ok(true)`] if the message was successfully processed and forwarded.
/// - [`Err(Error)`] if the message is invalid or has already been
/// exchanged.
@ -152,9 +153,9 @@ impl OldSession {
from_peer_id,
&mut self.events,
self.waker.take(),
self.session_number,
self.epoch,
).inspect_err(|receive_error| {
tracing::debug!(target: LOG_TARGET, "Failed to handle message from the old session: {receive_error:?}. Closing connection with spammy peer.");
tracing::debug!(target: LOG_TARGET, "Failed to handle message from the old epoch: {receive_error:?}. Closing connection with spammy peer.");
self.events.push_back(ToSwarm::NotifyHandler {
peer_id: from_peer_id,
handler: NotifyHandler::One(from_connection_id),
@ -166,10 +167,10 @@ impl OldSession {
Ok(true)
}
/// Stops the old session by returning events to close all the substreams
/// in the old session.
/// Stops the old epoch by returning events to close all the substreams
/// in the old epoch.
///
/// It should be called once the session transition period has passed.
/// It should be called once the epoch transition period has passed.
pub fn stop(self) -> VecDeque<ToSwarm<Event, Either<FromBehaviour, Infallible>>> {
let mut events = VecDeque::with_capacity(self.negotiated_peers.len());
for (&peer_id, &connection_id) in &self.negotiated_peers {
@ -182,7 +183,7 @@ impl OldSession {
events
}
/// Checks if the connection is part of the old session.
/// Checks if the connection is part of the old epoch.
#[must_use]
pub fn is_negotiated(&self, (peer_id, connection_id): &(PeerId, ConnectionId)) -> bool {
self.negotiated_peers
@ -190,7 +191,7 @@ impl OldSession {
.is_some_and(|&id| id == *connection_id)
}
/// Returns the peer IDs of all negotiated peers in the old session.
/// Returns the peer IDs of all negotiated peers in the old epoch.
pub fn negotiated_peer_ids(&self) -> impl Iterator<Item = &PeerId> {
self.negotiated_peers.keys()
}
@ -198,7 +199,7 @@ impl OldSession {
/// Should be called when a connection is detected as closed.
///
/// It removes the connection from the states and returns [`true`]
/// if the connection was part of the old session.
/// if the connection was part of the old epoch.
pub fn handle_closed_connection(
&mut self,
(peer_id, connection_id): &(PeerId, ConnectionId),

View File

@ -16,7 +16,7 @@ use crate::core::{
},
};
#[ignore = "TODO: enable this logic after investigating session/epoch transition issues. Test disabled because we don't let connections turn spammy because of too many messages now until we have proper observation window values."]
#[ignore = "TODO: enable this logic after investigating epoch transition issues. Test disabled because we don't let connections turn spammy because of too many messages now until we have proper observation window values."]
#[test(tokio::test)]
async fn detect_spammy_peer() {
let (mut identities, nodes) = new_nodes_with_empty_address(2);
@ -41,13 +41,13 @@ async fn detect_spammy_peer() {
// Send two messages when only one was expected.
dialing_swarm
.behaviour_mut()
.publish_message_with_validated_signature_to_current_session(
.publish_message_with_validated_signature_to_current_epoch(
&TestEncapsulatedMessage::new(b"msg1").into_inner().into(),
)
.unwrap();
dialing_swarm
.behaviour_mut()
.publish_message_with_validated_signature_to_current_session(
.publish_message_with_validated_signature_to_current_epoch(
&TestEncapsulatedMessage::new(b"msg2").into_inner().into(),
)
.unwrap();
@ -80,7 +80,7 @@ async fn detect_spammy_peer() {
}
}
#[ignore = "TODO: enable this logic after investigating session/epoch transition issues. Test disabled because we don't let connections turn unhealthy now until we have proper observation window values."]
#[ignore = "TODO: enable this logic after investigating epoch transition issues. Test disabled because we don't let connections turn unhealthy now until we have proper observation window values."]
#[test(tokio::test)]
async fn detect_unhealthy_peer() {
let (mut identities, nodes) = new_nodes_with_empty_address(2);
@ -141,7 +141,7 @@ async fn detect_unhealthy_peer() {
);
}
#[ignore = "TODO: enable this logic after investigating session/epoch transition issues. Test disabled because we don't let connections turn unhealthy now until we have proper observation window values."]
#[ignore = "TODO: enable this logic after investigating epoch transition issues. Test disabled because we don't let connections turn unhealthy now until we have proper observation window values."]
#[test(tokio::test)]
async fn restore_healthy_peer() {
let (mut identities, nodes) = new_nodes_with_empty_address(2);
@ -166,7 +166,7 @@ async fn restore_healthy_peer() {
// Send a message to the listening swarm to revert from unhealthy to healthy.
dialing_swarm
.behaviour_mut()
.force_send_message_to_current_session_peer(
.force_send_message_to_current_epoch_peer(
&TestEncapsulatedMessage::new(b"msg").into_inner(),
*listening_swarm.local_peer_id(),
)

View File

@ -1,13 +1,14 @@
use core::time::Duration;
use futures::StreamExt as _;
use lb_cryptarchia_engine::Epoch;
use lb_libp2p::SwarmEvent;
use libp2p_swarm_test::SwarmExt as _;
use test_log::test;
use tokio::{select, time::sleep};
use crate::core::{
tests::utils::{TestEncapsulatedMessageWithSession, TestSwarm},
tests::utils::{TestEncapsulatedMessageWithEpoch, TestSwarm},
with_core::{
behaviour::{
Event,
@ -21,7 +22,7 @@ use crate::core::{
#[test(tokio::test)]
async fn publish_message() {
let mut session = 0;
let mut epoch = Epoch::new(0);
let (mut identities, nodes) = new_nodes_with_empty_address(2);
let mut dialer = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id).with_membership(&nodes).build()
@ -33,31 +34,31 @@ async fn publish_message() {
listener.listen().with_memory_addr_external().await;
dialer.connect_and_wait_for_upgrade(&mut listener).await;
// Start a new session before sending any message through the connection.
session += 1;
// Start a new epoch before sending any message through the connection.
epoch = Epoch::new(epoch.into_inner() + 1);
let memberships = build_memberships(&[&dialer, &listener]);
dialer
.behaviour_mut()
.start_new_session((memberships[0].clone(), session));
.start_new_epoch((memberships[0].clone(), epoch));
listener
.behaviour_mut()
.start_new_session((memberships[1].clone(), session));
.start_new_epoch((memberships[1].clone(), epoch));
// Send a message but expect [`Error::NoPeers`]
// because we haven't establish connections for the new session.
let test_message = TestEncapsulatedMessageWithSession::new(session, b"msg");
// because we haven't establish connections for the new epoch.
let test_message = TestEncapsulatedMessageWithEpoch::new(epoch, b"msg");
let result = dialer
.behaviour_mut()
.publish_message_with_validated_header(test_message.clone(), session);
.publish_message_with_validated_header(test_message.clone(), epoch);
assert_eq!(result, Err(SendError::NoPeers));
// Establish a connection for the new session.
// Establish a connection for the new epoch.
dialer.connect_and_wait_for_upgrade(&mut listener).await;
// Now we can send the message successfully.
dialer
.behaviour_mut()
.publish_message_with_validated_header(test_message.clone(), session)
.publish_message_with_validated_header(test_message.clone(), epoch)
.unwrap();
loop {
select! {
@ -75,14 +76,14 @@ async fn publish_message() {
assert_eq!(
dialer
.behaviour_mut()
.publish_message_with_validated_header(test_message.clone(), 1),
.publish_message_with_validated_header(test_message.clone(), 1.into()),
Err(SendError::DuplicateMessage)
);
}
#[test(tokio::test)]
async fn forward_message() {
let old_session = 0;
let old_epoch = Epoch::new(0);
let (mut identities, nodes) = new_nodes_with_empty_address(4);
let mut sender = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id).with_membership(&nodes).build()
@ -108,42 +109,42 @@ async fn forward_message() {
sender.connect_and_wait_for_upgrade(&mut forwarder).await;
forwarder.connect_and_wait_for_upgrade(&mut receiver1).await;
// Before sending any message, start a new session
// Before sending any message, start a new epoch
// only for the forwarder, receiver1, and receiver2.
// And, connect the forwarder to the receiver2 for the new session.
// And, connect the forwarder to the receiver2 for the new epoch.
// Then, the topology looks like:
// - Old session: sender -> forwarder -> receiver1
// - New session: forwarder -> receiver2
let new_session = old_session + 1;
// - Old epoch: sender -> forwarder -> receiver1
// - New epoch: forwarder -> receiver2
let new_epoch = Epoch::new(old_epoch.into_inner() + 1);
let memberships = build_memberships(&[&sender, &forwarder, &receiver1, &receiver2]);
forwarder
.behaviour_mut()
.start_new_session((memberships[1].clone(), new_session));
.start_new_epoch((memberships[1].clone(), new_epoch));
receiver1
.behaviour_mut()
.start_new_session((memberships[2].clone(), new_session));
.start_new_epoch((memberships[2].clone(), new_epoch));
receiver2
.behaviour_mut()
.start_new_session((memberships[3].clone(), new_session));
.start_new_epoch((memberships[3].clone(), new_epoch));
forwarder.connect_and_wait_for_upgrade(&mut receiver2).await;
// The sender publishes a message built with the old session to the forwarder.
let test_message = TestEncapsulatedMessageWithSession::new(old_session, b"msg");
// The sender publishes a message built with the old epoch to the forwarder.
let test_message = TestEncapsulatedMessageWithEpoch::new(old_epoch, b"msg");
sender
.behaviour_mut()
.publish_message_with_validated_header(test_message.clone(), old_session)
.publish_message_with_validated_header(test_message.clone(), old_epoch)
.unwrap();
// We expect that the message goes through the forwarder and receiver1
// even though the forwarder is connected to the receiver2 in the new session.
// even though the forwarder is connected to the receiver2 in the new epoch.
loop {
select! {
_ = sender.select_next_some() => {}
event = forwarder.select_next_some() => {
if let SwarmEvent::Behaviour(Event::Message { message, session, sender }) = event {
if let SwarmEvent::Behaviour(Event::Message { message, epoch, sender }) = event {
assert_eq!(message.id(), test_message.id());
forwarder.behaviour_mut()
.forward_message_with_validated_signature(&message, sender, session)
.forward_message_with_validated_signature(&message, sender, epoch)
.unwrap();
}
}
@ -157,19 +158,19 @@ async fn forward_message() {
}
}
// Now we start the new session for the sender as well.
// Also, connect the sender to the forwarder for the new session.
// Now we start the new epoch for the sender as well.
// Also, connect the sender to the forwarder for the new epoch.
sender
.behaviour_mut()
.start_new_session((memberships[0].clone(), new_session));
.start_new_epoch((memberships[0].clone(), new_epoch));
sender.connect_and_wait_for_upgrade(&mut forwarder).await;
// The sender publishes a new message built with the new session to the
// The sender publishes a new message built with the new epoch to the
// forwarder.
let test_message = TestEncapsulatedMessageWithSession::new(new_session, b"msg");
let test_message = TestEncapsulatedMessageWithEpoch::new(new_epoch, b"msg");
sender
.behaviour_mut()
.publish_message_with_validated_header(test_message.clone(), new_session)
.publish_message_with_validated_header(test_message.clone(), new_epoch)
.unwrap();
// We expect that the message goes through the forwarder and receiver2.
@ -177,10 +178,10 @@ async fn forward_message() {
select! {
_ = sender.select_next_some() => {}
event = forwarder.select_next_some() => {
if let SwarmEvent::Behaviour(Event::Message { message, session, sender }) = event {
if let SwarmEvent::Behaviour(Event::Message { message, epoch, sender }) = event {
assert_eq!(message.id(), test_message.id());
forwarder.behaviour_mut()
.forward_message_with_validated_signature(&message, sender, session)
.forward_message_with_validated_signature(&message, sender, epoch)
.unwrap();
}
}
@ -196,8 +197,8 @@ async fn forward_message() {
}
#[test(tokio::test)]
async fn finish_session_transition() {
let mut session = 0;
async fn finish_epoch_transition() {
let mut epoch = Epoch::new(0);
let (mut identities, nodes) = new_nodes_with_empty_address(2);
let mut dialer = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id).with_membership(&nodes).build()
@ -209,19 +210,19 @@ async fn finish_session_transition() {
listener.listen().with_memory_addr_external().await;
dialer.connect_and_wait_for_upgrade(&mut listener).await;
// Start a new session.
session += 1;
// Start a new epoch.
epoch = Epoch::new(epoch.into_inner() + 1);
let memberships = build_memberships(&[&dialer, &listener]);
dialer
.behaviour_mut()
.start_new_session((memberships[0].clone(), session));
.start_new_epoch((memberships[0].clone(), epoch));
listener
.behaviour_mut()
.start_new_session((memberships[1].clone(), session));
.start_new_epoch((memberships[1].clone(), epoch));
// Finish the transition period
dialer.behaviour_mut().finish_session_transition();
listener.behaviour_mut().finish_session_transition();
dialer.behaviour_mut().finish_epoch_transition();
listener.behaviour_mut().finish_epoch_transition();
// Expect that the connection is closed after 10s (default swarm timeout).
loop {
@ -237,8 +238,8 @@ async fn finish_session_transition() {
}
#[test(tokio::test)]
async fn old_session_message_not_forwarded_back_to_sender() {
let old_session = 0;
async fn old_epoch_message_not_forwarded_back_to_sender() {
let old_epoch = Epoch::new(0);
let (mut identities, nodes) = new_nodes_with_empty_address(3);
let mut sender = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id).with_membership(&nodes).build()
@ -255,35 +256,35 @@ async fn old_session_message_not_forwarded_back_to_sender() {
forwarder.listen().with_memory_addr_external().await;
receiver.listen().with_memory_addr_external().await;
// Topology: sender -> forwarder -> receiver (current session).
// Topology: sender -> forwarder -> receiver (current epoch).
sender.connect_and_wait_for_upgrade(&mut forwarder).await;
forwarder.connect_and_wait_for_upgrade(&mut receiver).await;
// Forwarder starts a new session. Both the sender and the receiver
// connections move into the forwarder's old session.
let new_session = old_session + 1;
// Forwarder starts a new epoch. Both the sender and the receiver
// connections move into the forwarder's old epoch.
let new_epoch = Epoch::new(old_epoch.into_inner() + 1);
let memberships = build_memberships(&[&sender, &forwarder, &receiver]);
forwarder
.behaviour_mut()
.start_new_session((memberships[1].clone(), new_session));
.start_new_epoch((memberships[1].clone(), new_epoch));
// Sender publishes a message for the old session.
let test_message = TestEncapsulatedMessageWithSession::new(old_session, b"msg");
// Sender publishes a message for the old epoch.
let test_message = TestEncapsulatedMessageWithEpoch::new(old_epoch, b"msg");
sender
.behaviour_mut()
.publish_message_with_validated_header(test_message.clone(), old_session)
.publish_message_with_validated_header(test_message.clone(), old_epoch)
.unwrap();
// Forwarder receives the message via the old session and forwards it,
// Forwarder receives the message via the old epoch and forwards it,
// excluding the original sender. Only receiver should receive the message.
loop {
select! {
_ = sender.select_next_some() => {}
forwarder_event = forwarder.select_next_some() => {
if let SwarmEvent::Behaviour(Event::Message { message, session, sender: msg_sender }) = forwarder_event {
if let SwarmEvent::Behaviour(Event::Message { message, epoch, sender: msg_sender }) = forwarder_event {
assert_eq!(message.id(), test_message.id());
forwarder.behaviour_mut()
.forward_message_with_validated_signature(&message, msg_sender, session)
.forward_message_with_validated_signature(&message, msg_sender, epoch)
.unwrap();
}
}
@ -314,13 +315,13 @@ async fn old_session_message_not_forwarded_back_to_sender() {
assert!(
!sender_received_message_back,
"Old session should not forward the message back to the original sender"
"Old epoch should not forward the message back to the original sender"
);
}
#[test(tokio::test)]
async fn publish_to_invalid_session_returns_error() {
let session = 1;
async fn publish_to_invalid_epoch_returns_error() {
let epoch = Epoch::new(1);
let (mut identities, nodes) = new_nodes_with_empty_address(2);
let mut dialer = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id).with_membership(&nodes).build()
@ -332,27 +333,27 @@ async fn publish_to_invalid_session_returns_error() {
listener.listen().with_memory_addr_external().await;
dialer.connect_and_wait_for_upgrade(&mut listener).await;
// Start the first session and connect.
// Start the first epoch and connect.
let memberships = build_memberships(&[&dialer, &listener]);
dialer
.behaviour_mut()
.start_new_session((memberships[0].clone(), session));
.start_new_epoch((memberships[0].clone(), epoch));
listener
.behaviour_mut()
.start_new_session((memberships[1].clone(), session));
.start_new_epoch((memberships[1].clone(), epoch));
dialer.connect_and_wait_for_upgrade(&mut listener).await;
// Attempt to publish to a session that neither matches the current nor old.
let test_message = TestEncapsulatedMessageWithSession::new(999, b"invalid-session");
// Attempt to publish to an epoch that neither matches the current nor old.
let test_message = TestEncapsulatedMessageWithEpoch::new(999.into(), b"invalid-epoch");
let result = dialer
.behaviour_mut()
.publish_message_with_validated_header(test_message.clone(), 999);
assert_eq!(result, Err(SendError::InvalidSession));
.publish_message_with_validated_header(test_message.clone(), 999.into());
assert_eq!(result, Err(SendError::InvalidEpoch));
}
#[test(tokio::test)]
async fn forward_to_invalid_session_returns_error() {
let session = 1;
async fn forward_to_invalid_epoch_returns_error() {
let epoch = Epoch::new(1);
let (mut identities, nodes) = new_nodes_with_empty_address(2);
let mut dialer = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id).with_membership(&nodes).build()
@ -364,30 +365,29 @@ async fn forward_to_invalid_session_returns_error() {
listener.listen().with_memory_addr_external().await;
dialer.connect_and_wait_for_upgrade(&mut listener).await;
// Start the first session and connect.
// Start the first epoch and connect.
let memberships = build_memberships(&[&dialer, &listener]);
dialer
.behaviour_mut()
.start_new_session((memberships[0].clone(), session));
.start_new_epoch((memberships[0].clone(), epoch));
listener
.behaviour_mut()
.start_new_session((memberships[1].clone(), session));
.start_new_epoch((memberships[1].clone(), epoch));
dialer.connect_and_wait_for_upgrade(&mut listener).await;
// Attempt to forward a message to an invalid session.
let test_message = TestEncapsulatedMessageWithSession::new(999, b"invalid-session");
// Attempt to forward a message to an invalid epoch.
let test_message = TestEncapsulatedMessageWithEpoch::new(999.into(), b"invalid-epoch");
let fake_sender = *listener.local_peer_id();
let sig_verified: lb_blend_message::encap::validated::EncapsulatedMessageWithVerifiedSignature =
(*test_message).clone().into();
let result = dialer
.behaviour_mut()
.forward_message_with_validated_signature(&sig_verified, fake_sender, 999);
assert_eq!(result, Err(SendError::InvalidSession));
.forward_message_with_validated_signature(&sig_verified, fake_sender, 999.into());
assert_eq!(result, Err(SendError::InvalidEpoch));
}
#[test(tokio::test)]
async fn event_message_carries_session_number() {
let mut session = 0;
async fn event_message_carries_epoch_number() {
let (mut identities, nodes) = new_nodes_with_empty_address(2);
let mut dialer = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id).with_membership(&nodes).build()
@ -399,31 +399,31 @@ async fn event_message_carries_session_number() {
listener.listen().with_memory_addr_external().await;
dialer.connect_and_wait_for_upgrade(&mut listener).await;
// Start session 1 and connect.
session += 1;
// Start epoch 1 and connect.
let epoch = Epoch::new(1);
let memberships = build_memberships(&[&dialer, &listener]);
dialer
.behaviour_mut()
.start_new_session((memberships[0].clone(), session));
.start_new_epoch((memberships[0].clone(), epoch));
listener
.behaviour_mut()
.start_new_session((memberships[1].clone(), session));
.start_new_epoch((memberships[1].clone(), epoch));
dialer.connect_and_wait_for_upgrade(&mut listener).await;
// Send a message for session 1.
let test_message = TestEncapsulatedMessageWithSession::new(session, b"session-check");
// Send a message for epoch 1.
let test_message = TestEncapsulatedMessageWithEpoch::new(epoch, b"epoch-check");
dialer
.behaviour_mut()
.publish_message_with_validated_header(test_message.clone(), session)
.publish_message_with_validated_header(test_message.clone(), epoch)
.unwrap();
loop {
select! {
_ = dialer.select_next_some() => {}
event = listener.select_next_some() => {
if let SwarmEvent::Behaviour(Event::Message { message, session: event_session, .. }) = event {
if let SwarmEvent::Behaviour(Event::Message { message, epoch: event_epoch, .. }) = event {
assert_eq!(message.id(), test_message.id());
assert_eq!(event_session, session, "Event::Message must carry the correct session number");
assert_eq!(event_epoch, epoch, "Event::Message must carry the correct epoch number");
break;
}
}
@ -431,10 +431,10 @@ async fn event_message_carries_session_number() {
}
}
/// After `start_new_session()`, current `negotiated_peers` must be empty and
/// old peers must live inside the `OldSession`.
/// After `start_new_epoch()`, current `negotiated_peers` must be empty and
/// old peers must live inside the `OldEpoch`.
#[test(tokio::test)]
async fn start_new_session_moves_peers_to_old_session() {
async fn start_new_epoch_moves_peers_to_old_epoch() {
let (mut identities, nodes) = new_nodes_with_empty_address(3);
let mut node_a = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id)
@ -454,33 +454,33 @@ async fn start_new_session_moves_peers_to_old_session() {
node_a.connect_and_wait_for_upgrade(&mut node_b).await;
node_a.connect_and_wait_for_upgrade(&mut node_c).await;
// Before session transition: node_a has 2 negotiated peers.
// Before epoch transition: node_a has 2 negotiated peers.
assert_eq!(node_a.behaviour().negotiated_peers.len(), 2);
assert!(node_a.behaviour().old_session.is_none());
assert!(node_a.behaviour().old_epoch.is_none());
// Start a new session.
// Start a new epoch.
let memberships = build_memberships(&[&node_a, &node_b, &node_c]);
node_a
.behaviour_mut()
.start_new_session((memberships[0].clone(), 1));
.start_new_epoch((memberships[0].clone(), 1.into()));
// After session transition: current negotiated_peers must be empty
// and old_session must exist.
// After epoch transition: current negotiated_peers must be empty
// and old_epoch must exist.
assert_eq!(
node_a.behaviour().negotiated_peers.len(),
0,
"Current session negotiated peers must be reset after start_new_session"
"Current epoch negotiated peers must be reset after start_new_epoch"
);
assert!(
node_a.behaviour().old_session.is_some(),
"Old session must be created after start_new_session"
node_a.behaviour().old_epoch.is_some(),
"Old epoch must be created after start_new_epoch"
);
}
/// `finish_session_transition()` emits close substream events for all peers
/// in the old session, generating `PeerDisconnected` events.
/// `finish_epoch_transition()` emits close substream events for all peers
/// in the old epoch, generating `PeerDisconnected` events.
#[test(tokio::test)]
async fn finish_session_transition_emits_peer_disconnected_for_old_session_peers() {
async fn finish_epoch_transition_emits_peer_disconnected_for_old_epoch_peers() {
let (mut identities, nodes) = new_nodes_with_empty_address(3);
let mut node_a = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id)
@ -500,22 +500,22 @@ async fn finish_session_transition_emits_peer_disconnected_for_old_session_peers
node_a.connect_and_wait_for_upgrade(&mut node_b).await;
node_a.connect_and_wait_for_upgrade(&mut node_c).await;
// Start a new session to move current peers into old session.
// Start a new epoch to move current peers into old epoch.
let memberships = build_memberships(&[&node_a, &node_b, &node_c]);
node_a
.behaviour_mut()
.start_new_session((memberships[0].clone(), 1));
.start_new_epoch((memberships[0].clone(), 1.into()));
// Finish the transition; this should close all old session connections.
node_a.behaviour_mut().finish_session_transition();
// Finish the transition; this should close all old epoch connections.
node_a.behaviour_mut().finish_epoch_transition();
// Old session should now be gone.
// Old epoch should now be gone.
assert!(
node_a.behaviour().old_session.is_none(),
"Old session must be cleared after finish_session_transition"
node_a.behaviour().old_epoch.is_none(),
"Old epoch must be cleared after finish_epoch_transition"
);
// Drive the swarms until both connections from the old session close.
// Drive the swarms until both connections from the old epoch close.
let mut closed_count = 0usize;
loop {
select! {
@ -530,16 +530,16 @@ async fn finish_session_transition_emits_peer_disconnected_for_old_session_peers
}
}
() = sleep(Duration::from_secs(15)) => {
panic!("Timed out waiting for old session connections to close");
panic!("Timed out waiting for old epoch connections to close");
}
}
}
}
/// Multiple consecutive `start_new_session` calls should discard the previous
/// old session, moving current peers into a new old session each time.
/// Multiple consecutive `start_new_epoch` calls should discard the previous
/// old epoch, moving current peers into a new old epoch each time.
#[test(tokio::test)]
async fn consecutive_session_transitions_replace_old_session() {
async fn consecutive_epoch_transitions_replace_old_epoch() {
let (mut identities, nodes) = new_nodes_with_empty_address(2);
let mut dialer = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id).with_membership(&nodes).build()
@ -551,40 +551,40 @@ async fn consecutive_session_transitions_replace_old_session() {
listener.listen().with_memory_addr_external().await;
dialer.connect_and_wait_for_upgrade(&mut listener).await;
// First session transition: move the current peer into old session.
// First epoch transition: move the current peer into old epoch.
let memberships = build_memberships(&[&dialer, &listener]);
dialer
.behaviour_mut()
.start_new_session((memberships[0].clone(), 1));
.start_new_epoch((memberships[0].clone(), 1.into()));
listener
.behaviour_mut()
.start_new_session((memberships[1].clone(), 1));
assert!(dialer.behaviour().old_session.is_some());
.start_new_epoch((memberships[1].clone(), 1.into()));
assert!(dialer.behaviour().old_epoch.is_some());
// Re-establish a connection for session 1 so there is something to move
// into old session again.
// Re-establish a connection for epoch 1 so there is something to move
// into old epoch again.
dialer.connect_and_wait_for_upgrade(&mut listener).await;
assert_eq!(dialer.behaviour().negotiated_peers.len(), 1);
// Second session transition: old session from session 0 gets stopped
// and current peers from session 1 move into old session.
// Second epoch transition: old epoch from epoch 0 gets stopped
// and current peers from epoch 1 move into old epoch.
let memberships = build_memberships(&[&dialer, &listener]);
dialer
.behaviour_mut()
.start_new_session((memberships[0].clone(), 2));
.start_new_epoch((memberships[0].clone(), 2.into()));
listener
.behaviour_mut()
.start_new_session((memberships[1].clone(), 2));
assert!(dialer.behaviour().old_session.is_some());
.start_new_epoch((memberships[1].clone(), 2.into()));
assert!(dialer.behaviour().old_epoch.is_some());
assert_eq!(
dialer.behaviour().negotiated_peers.len(),
0,
"Current negotiated peers must be empty after session transition"
"Current negotiated peers must be empty after epoch transition"
);
// Drive the swarm until the original (session 0) connection closes
// (due to stop_old_session called for session 0 peers inside the second
// start_new_session).
// Drive the swarm until the original (epoch 0) connection closes
// (due to stop_old_epoch called for epoch 0 peers inside the second
// start_new_epoch).
loop {
select! {
_ = listener.select_next_some() => {}
@ -594,16 +594,16 @@ async fn consecutive_session_transitions_replace_old_session() {
}
}
() = sleep(Duration::from_secs(15)) => {
panic!("Timed out waiting for old session 0 connections to close");
panic!("Timed out waiting for old epoch 0 connections to close");
}
}
}
}
/// Verify that after session transition, re-bootstrapping into the new session
/// Verify that after epoch transition, re-bootstrapping into the new epoch
/// respects peering degree limits.
#[test(tokio::test)]
async fn session_transition_reboots_peering_degree() {
async fn epoch_transition_reboots_peering_degree() {
let (mut identities, nodes) = new_nodes_with_empty_address(4);
let mut node_a = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id)
@ -632,29 +632,29 @@ async fn session_transition_reboots_peering_degree() {
assert_eq!(node_a.behaviour().negotiated_peers.len(), 2);
assert_eq!(node_a.behaviour().available_connection_slots(), 0);
// Start session transition - all current peers move to old session.
// Start epoch transition - all current peers move to old epoch.
let memberships = build_memberships(&[&node_a, &node_b, &node_c, &node_d]);
node_a
.behaviour_mut()
.start_new_session((memberships[0].clone(), 1));
.start_new_epoch((memberships[0].clone(), 1.into()));
node_b
.behaviour_mut()
.start_new_session((memberships[1].clone(), 1));
.start_new_epoch((memberships[1].clone(), 1.into()));
node_c
.behaviour_mut()
.start_new_session((memberships[2].clone(), 1));
.start_new_epoch((memberships[2].clone(), 1.into()));
node_d
.behaviour_mut()
.start_new_session((memberships[3].clone(), 1));
.start_new_epoch((memberships[3].clone(), 1.into()));
// After transition, new session has no peers, so all slots are available.
// After transition, new epoch has no peers, so all slots are available.
assert_eq!(
node_a.behaviour().available_connection_slots(),
2,
"All peering degree slots must be available after session transition"
"All peering degree slots must be available after epoch transition"
);
// Connect to new peers in the new session.
// Connect to new peers in the new epoch.
node_a.connect_and_wait_for_upgrade(&mut node_b).await;
node_a.connect_and_wait_for_upgrade(&mut node_d).await;
assert_eq!(node_a.behaviour().negotiated_peers.len(), 2);

View File

@ -8,7 +8,7 @@ use test_log::test;
use tokio::{select, time::sleep};
use crate::core::{
tests::utils::{TestEncapsulatedMessage, TestEncapsulatedMessageWithSession, TestSwarm},
tests::utils::{TestEncapsulatedMessage, TestEncapsulatedMessageWithEpoch, TestSwarm},
with_core::{
behaviour::{
Event, NegotiatedPeerState, SpamReason,
@ -41,7 +41,7 @@ async fn message_sending_and_reception() {
let test_message_id = test_message.id();
dialing_swarm
.behaviour_mut()
.publish_message_with_validated_signature_to_current_session(
.publish_message_with_validated_signature_to_current_epoch(
&test_message.as_ref().clone().into(),
)
.unwrap();
@ -89,7 +89,7 @@ async fn message_sending_and_reception() {
assert_eq!(
dialing_swarm
.behaviour_mut()
.publish_message_with_validated_signature_to_current_session(
.publish_message_with_validated_signature_to_current_epoch(
&test_message.as_ref().clone().into()
),
Err(SendError::DuplicateMessage)
@ -113,7 +113,7 @@ async fn undeserializable_message_received() {
dialing_swarm
.behaviour_mut()
.force_send_serialized_message_to_current_session_peer(
.force_send_serialized_message_to_current_epoch_peer(
b"msg".to_vec(),
*listening_swarm.local_peer_id(),
)
@ -163,7 +163,7 @@ async fn duplicate_message_received_from_same_peer() {
let test_message = TestEncapsulatedMessage::new(b"msg");
dialing_swarm
.behaviour_mut()
.publish_message_with_validated_signature_to_current_session(
.publish_message_with_validated_signature_to_current_epoch(
&test_message.as_ref().clone().into(),
)
.unwrap();
@ -190,7 +190,7 @@ async fn duplicate_message_received_from_same_peer() {
// This is a duplicate message, so the listener will mark the dialer as spammy.
dialing_swarm
.behaviour_mut()
.force_send_message_to_current_session_peer(
.force_send_message_to_current_epoch_peer(
&test_message.into_inner(),
*listening_swarm.local_peer_id(),
)
@ -249,13 +249,13 @@ async fn duplicate_message_received_from_different_peers() {
let test_message = TestEncapsulatedMessage::new(b"msg");
dialing_swarm_1
.behaviour_mut()
.publish_message_with_validated_signature_to_current_session(
.publish_message_with_validated_signature_to_current_epoch(
&test_message.as_ref().clone().into(),
)
.unwrap();
dialing_swarm_2
.behaviour_mut()
.publish_message_with_validated_signature_to_current_session(
.publish_message_with_validated_signature_to_current_epoch(
&test_message.as_ref().clone().into(),
)
.unwrap();
@ -297,7 +297,7 @@ async fn invalid_signature_message_received() {
let invalid_public_header_message = TestEncapsulatedMessage::new_with_invalid_signature(b"");
dialing_swarm
.behaviour_mut()
.force_send_message_to_current_session_peer(
.force_send_message_to_current_epoch_peer(
&invalid_public_header_message.as_ref().clone(),
*listening_swarm.local_peer_id(),
)
@ -348,7 +348,7 @@ async fn message_already_forwarded_silently_ignored_when_received_from_peer() {
// Node A forwards X to Node B. In Node A's cache X is now `Forwarded`.
node_a
.behaviour_mut()
.publish_message_with_validated_signature_to_current_session(
.publish_message_with_validated_signature_to_current_epoch(
&test_message.as_ref().clone().into(),
)
.unwrap();
@ -371,7 +371,7 @@ async fn message_already_forwarded_silently_ignored_when_received_from_peer() {
// silently dropped - no event, no spam marking.
node_b
.behaviour_mut()
.force_send_message_to_current_session_peer(
.force_send_message_to_current_epoch_peer(
&test_message.into_inner(),
*node_a.local_peer_id(),
)
@ -408,7 +408,7 @@ async fn message_already_forwarded_silently_ignored_when_received_from_peer() {
}
#[test(tokio::test)]
async fn duplicate_message_in_old_session_disconnects_peer_without_swarm_notification() {
async fn duplicate_message_in_old_epoch_disconnects_peer_without_swarm_notification() {
let (mut identities, nodes) = new_nodes_with_empty_address(2);
let mut sender = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id).with_membership(&nodes).build()
@ -425,7 +425,7 @@ async fn duplicate_message_in_old_session_disconnects_peer_without_swarm_notific
// Sender publishes X. Receiver marks it as `Processed` in its cache.
sender
.behaviour_mut()
.publish_message_with_validated_signature_to_current_session(
.publish_message_with_validated_signature_to_current_epoch(
&test_message.as_ref().clone().into(),
)
.unwrap();
@ -441,25 +441,25 @@ async fn duplicate_message_in_old_session_disconnects_peer_without_swarm_notific
}
}
// Receiver starts a new session. Sender's connection moves to the old
// session together with the existing message cache (which contains X as
// Receiver starts a new epoch. Sender's connection moves to the old
// epoch together with the existing message cache (which contains X as
// `Processed`).
let memberships = build_memberships(&[&sender, &receiver]);
receiver
.behaviour_mut()
.start_new_session((memberships[1].clone(), 1));
.start_new_epoch((memberships[1].clone(), 1.into()));
// Wait long enough so that the connection monitor does not fire
// `TooManyMessages` instead.
sleep(Duration::from_secs(3)).await;
// Sender sends X again, bypassing its own `Forwarded` guard. From
// receiver's point of view this arrives over the old-session connection.
// The old-session handler detects a duplicate from the same peer,
// receiver's point of view this arrives over the old-epoch connection.
// The old-epoch handler detects a duplicate from the same peer,
// closes the connection, but must NOT emit a `PeerDisconnected` event.
sender
.behaviour_mut()
.force_send_message_to_current_session_peer(
.force_send_message_to_current_epoch_peer(
&test_message.into_inner(),
*receiver.local_peer_id(),
)
@ -488,16 +488,16 @@ async fn duplicate_message_in_old_session_disconnects_peer_without_swarm_notific
assert!(
connection_closed,
"Connection with spammy old-session peer must be closed"
"Connection with spammy old-epoch peer must be closed"
);
assert!(
!peer_disconnected_event,
"No PeerDisconnected event must be emitted for a spammy old-session peer"
"No PeerDisconnected event must be emitted for a spammy old-epoch peer"
);
}
#[test(tokio::test)]
async fn undeserializable_message_in_old_session_closes_connection_without_swarm_notification() {
async fn undeserializable_message_in_old_epoch_closes_connection_without_swarm_notification() {
let (mut identities, nodes) = new_nodes_with_empty_address(2);
let mut sender = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id).with_membership(&nodes).build()
@ -509,17 +509,17 @@ async fn undeserializable_message_in_old_session_closes_connection_without_swarm
receiver.listen().with_memory_addr_external().await;
sender.connect_and_wait_for_upgrade(&mut receiver).await;
// Receiver starts a new session. Sender's connection moves to the old
// session.
// Receiver starts a new epoch. Sender's connection moves to the old
// epoch.
let memberships = build_memberships(&[&sender, &receiver]);
receiver
.behaviour_mut()
.start_new_session((memberships[1].clone(), 1));
.start_new_epoch((memberships[1].clone(), 1.into()));
// Sender sends garbage data over the old-session connection.
// Sender sends garbage data over the old-epoch connection.
sender
.behaviour_mut()
.force_send_serialized_message_to_current_session_peer(
.force_send_serialized_message_to_current_epoch_peer(
b"garbage".to_vec(),
*receiver.local_peer_id(),
)
@ -547,16 +547,16 @@ async fn undeserializable_message_in_old_session_closes_connection_without_swarm
assert!(
connection_closed,
"Connection with spammy old-session peer must be closed"
"Connection with spammy old-epoch peer must be closed"
);
assert!(
!peer_disconnected_event,
"No PeerDisconnected event must be emitted for a spammy old-session peer"
"No PeerDisconnected event must be emitted for a spammy old-epoch peer"
);
}
#[test(tokio::test)]
async fn spammy_old_session_peer_does_not_affect_current_session() {
async fn spammy_old_epoch_peer_does_not_affect_current_epoch() {
let (mut identities, nodes) = new_nodes_with_empty_address(2);
let mut sender = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id).with_membership(&nodes).build()
@ -568,36 +568,36 @@ async fn spammy_old_session_peer_does_not_affect_current_session() {
receiver.listen().with_memory_addr_external().await;
sender.connect_and_wait_for_upgrade(&mut receiver).await;
// Receiver starts a new session. Sender's connection moves to the old
// session.
// Receiver starts a new epoch. Sender's connection moves to the old
// epoch.
let memberships = build_memberships(&[&sender, &receiver]);
receiver
.behaviour_mut()
.start_new_session((memberships[1].clone(), 1));
.start_new_epoch((memberships[1].clone(), 1.into()));
// Re-connect for the new session.
// Re-connect for the new epoch.
sender
.behaviour_mut()
.start_new_session((memberships[0].clone(), 1));
.start_new_epoch((memberships[0].clone(), 1.into()));
sender.connect_and_wait_for_upgrade(&mut receiver).await;
// Sender sends garbage over the old-session connection. This should
// close the old-session connection but NOT mark the peer as spammy in
// the current session.
// Sender sends garbage over the old-epoch connection. This should
// close the old-epoch connection but NOT mark the peer as spammy in
// the current epoch.
sender
.behaviour_mut()
.force_send_serialized_message_to_peer_at_session(
.force_send_serialized_message_to_peer_at_epoch(
b"garbage".to_vec(),
*receiver.local_peer_id(),
0,
0.into(),
)
.unwrap();
// Wait for the old-session connection to close.
// Wait for the old-epoch connection to close.
loop {
select! {
() = sleep(Duration::from_secs(15)) => {
panic!("Timed out waiting for old-session connection to close");
panic!("Timed out waiting for old-epoch connection to close");
}
_ = sender.select_next_some() => {}
event = receiver.select_next_some() => {
@ -608,18 +608,18 @@ async fn spammy_old_session_peer_does_not_affect_current_session() {
}
}
// Now verify the current session connection is healthy by sending a
// Now verify the current epoch connection is healthy by sending a
// valid message through it.
let test_message = TestEncapsulatedMessageWithSession::new(1, b"after-spam");
let test_message = TestEncapsulatedMessageWithEpoch::new(1.into(), b"after-spam");
sender
.behaviour_mut()
.publish_message_with_validated_header(test_message.clone(), 1)
.publish_message_with_validated_header(test_message.clone(), 1.into())
.unwrap();
loop {
select! {
() = sleep(Duration::from_secs(15)) => {
panic!("Timed out waiting for message on current session - current session connection was incorrectly affected by old session spam");
panic!("Timed out waiting for message on current epoch - current epoch connection was incorrectly affected by old epoch spam");
}
_ = sender.select_next_some() => {}
event = receiver.select_next_some() => {
@ -633,7 +633,7 @@ async fn spammy_old_session_peer_does_not_affect_current_session() {
}
#[test(tokio::test)]
async fn duplicate_message_from_old_session_after_session_rotation_is_suppressed() {
async fn duplicate_message_from_old_epoch_after_epoch_rotation_is_suppressed() {
let (mut identities, nodes) = new_nodes_with_empty_address(3);
let mut sender_a = TestSwarm::new(&identities.next().unwrap(), |id| {
BehaviourBuilder::new(id).with_membership(&nodes).build()
@ -653,11 +653,11 @@ async fn duplicate_message_from_old_session_after_session_rotation_is_suppressed
sender_b.connect_and_wait_for_upgrade(&mut receiver).await;
// Sender A sends message X. Receiver processes it and stores X as
// `Processed` in its current-session message cache.
// `Processed` in its current-epoch message cache.
let test_message = TestEncapsulatedMessage::new(b"msg");
sender_a
.behaviour_mut()
.publish_message_with_validated_signature_to_current_session(
.publish_message_with_validated_signature_to_current_epoch(
&test_message.as_ref().clone().into(),
)
.unwrap();
@ -674,21 +674,21 @@ async fn duplicate_message_from_old_session_after_session_rotation_is_suppressed
}
}
// Receiver starts a new session. The message cache now containing X
// as `Processed` is transferred into the old session object,
// Receiver starts a new epoch. The message cache now containing X
// as `Processed` is transferred into the old epoch object,
// alongside the connections to both sender_a and sender_b.
let memberships = build_memberships(&[&sender_a, &sender_b, &receiver]);
receiver
.behaviour_mut()
.start_new_session((memberships[2].clone(), 1));
.start_new_epoch((memberships[2].clone(), 1.into()));
// Sender B sends the identical message X through its (still-open)
// connection to receiver. From receiver's point of view this connection
// now belongs to the old session. Because X is already in the transferred
// now belongs to the old epoch. Because X is already in the transferred
// cache, receiver must NOT emit a second `Message` event.
sender_b
.behaviour_mut()
.publish_message_with_validated_signature_to_current_session(
.publish_message_with_validated_signature_to_current_epoch(
&test_message.as_ref().clone().into(),
)
.unwrap();
@ -709,6 +709,6 @@ async fn duplicate_message_from_old_session_after_session_rotation_is_suppressed
assert!(
!duplicate_message_received,
"Receiver must not re-emit a message that was already processed in the previous session"
"Receiver must not re-emit a message that was already processed in the previous epoch"
);
}

View File

@ -1,5 +1,5 @@
mod bootstrapping;
mod connection_maintenance;
mod epoch;
mod message_handling;
mod session;
mod utils;

View File

@ -133,10 +133,10 @@ impl BehaviourBuilder {
observation_window_clock_provider: self
.provider
.unwrap_or_else(|| IntervalProviderBuilder::default().build()),
current_session_info: (
current_epoch_info: (
self.membership
.unwrap_or_else(|| Membership::new_without_local(&[])),
0,
0.into(),
),
peering_degree: self.peering_degree.unwrap_or(1..=1),
local_peer_id: PublicKey::from(self.local_public_key).into(),
@ -144,7 +144,7 @@ impl BehaviourBuilder {
minimum_network_size: self
.minimum_network_size
.unwrap_or_else(|| 1usize.try_into().unwrap()),
old_session: None,
old_epoch: None,
message_cache: MessageCache::new(),
}
}

View File

@ -6,6 +6,7 @@ use lb_blend_message::encap::validated::EncapsulatedMessageWithVerifiedSignature
use lb_blend_scheduling::{
deserialize_encapsulated_message, serialize_encapsulated_message_with_verified_signature,
};
use lb_cryptarchia_engine::Epoch;
use libp2p::{
PeerId,
swarm::{ConnectionId, NotifyHandler, ToSwarm},
@ -22,15 +23,15 @@ use crate::core::with_core::{
/// The message cache is also updated accordingly to mark the sent message as
/// processed if it was sent to at least one peer, or to ignore it if it has
/// already been forwarded before.
pub fn forward_validated_message_and_update_cache<'session, PeerConnections>(
pub fn forward_validated_message_and_update_cache<'epoch, PeerConnections>(
message: &EncapsulatedMessageWithVerifiedSignature,
peer_connections: PeerConnections,
events_queue: &'session mut VecDeque<ToSwarm<Event, Either<FromBehaviour, Infallible>>>,
message_cache: &'session mut MessageCache,
events_queue: &'epoch mut VecDeque<ToSwarm<Event, Either<FromBehaviour, Infallible>>>,
message_cache: &'epoch mut MessageCache,
waker: Option<Waker>,
) -> Result<(), SendError>
where
PeerConnections: Iterator<Item = (&'session PeerId, &'session ConnectionId)>,
PeerConnections: Iterator<Item = (&'epoch PeerId, &'epoch ConnectionId)>,
{
if message_cache.is_message_forwarded(&message.clone().into()) {
return Err(SendError::DuplicateMessage);
@ -74,7 +75,7 @@ pub fn handle_received_serialized_encapsulated_message_and_update_cache(
sender: PeerId,
events_queue: &mut VecDeque<ToSwarm<Event, Either<FromBehaviour, Infallible>>>,
waker: Option<Waker>,
session_number: u64,
epoch: Epoch,
) -> Result<(), ReceiveError> {
// Deserialize the message.
let deserialized_encapsulated_message = deserialize_encapsulated_message(serialized_message)
@ -103,7 +104,7 @@ pub fn handle_received_serialized_encapsulated_message_and_update_cache(
events_queue.push_back(ToSwarm::GenerateEvent(Event::Message {
message: Box::new(validated_message),
sender,
session: session_number,
epoch,
}));
if let Some(waker) = waker {
waker.wake();

View File

@ -6,8 +6,8 @@ pub enum SendError {
NoPeers,
/// The message being sent is a duplicate of a previous sent message.
DuplicateMessage,
/// The session associated with the message being sent is invalid.
InvalidSession,
/// The epoch associated with the message being sent is invalid.
InvalidEpoch,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]

View File

@ -73,13 +73,13 @@ impl Behaviour {
#[must_use]
pub fn new(
config: &Config,
current_session_info: Membership<PeerId>,
current_epoch_info: Membership<PeerId>,
protocol_name: StreamProtocol,
) -> Self {
Self {
events: VecDeque::new(),
waker: None,
current_membership: current_session_info,
current_membership: current_epoch_info,
connection_timeout: config.connection_timeout,
upgraded_edge_peers: HashSet::with_capacity(config.max_incoming_connections),
max_incoming_connections: config.max_incoming_connections,
@ -88,8 +88,8 @@ impl Behaviour {
}
}
pub(crate) fn start_new_session(&mut self, new_session_info: Membership<PeerId>) {
self.current_membership = new_session_info;
pub(crate) fn start_new_epoch(&mut self, new_epoch_info: Membership<PeerId>) {
self.current_membership = new_epoch_info;
// Close all the connections without waiting for the transition period,
// so that edge nodes can retry with the new membership.
let peers = mem::take(&mut self.upgraded_edge_peers);
@ -112,7 +112,7 @@ impl Behaviour {
fn handle_negotiated_connection(&mut self, connection: (PeerId, ConnectionId)) {
// We need to check if we still have available connection slots, as it
// is possible, especially upon session transition, that more
// is possible, especially upon epoch transition, that more
// than the maximum allowed number of peers are trying to
// connect to us. So once we stream is actually upgraded, we
// downgrade it again if we do not have space left for it. This will

View File

@ -15,10 +15,10 @@ use crate::core::{
with_edge::behaviour::tests::utils::{BehaviourBuilder, StreamBehaviourExt as _},
};
/// After `start_new_session()`, the edge behaviour must immediately close all
/// existing upgraded edge peer connections (no dual-session for edge).
/// After `start_new_epoch()`, the edge behaviour must immediately close all
/// existing upgraded edge peer connections (no dual-epoch for edge).
#[test(tokio::test)]
async fn start_new_session_closes_all_edge_connections() {
async fn start_new_epoch_closes_all_edge_connections() {
let core_membership_peer = PeerId::random();
let mut edge_swarm = TestSwarm::new_ephemeral(|_| StreamBehaviour::new());
let mut blend_swarm = TestSwarm::new_ephemeral(|_| {
@ -36,20 +36,18 @@ async fn start_new_session_closes_all_edge_connections() {
assert_eq!(blend_swarm.behaviour().upgraded_edge_peers.len(), 1);
// Start a new session: all edge connections should be closed immediately.
// Start a new epoch: all edge connections should be closed immediately.
let new_membership = Membership::new_without_local(&[Node {
address: Multiaddr::empty(),
id: core_membership_peer,
public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(),
}]);
blend_swarm
.behaviour_mut()
.start_new_session(new_membership);
blend_swarm.behaviour_mut().start_new_epoch(new_membership);
assert_eq!(
blend_swarm.behaviour().upgraded_edge_peers.len(),
0,
"All edge peers must be removed immediately after start_new_session"
"All edge peers must be removed immediately after start_new_epoch"
);
// Drive the swarms until the connection actually closes.
@ -63,18 +61,18 @@ async fn start_new_session_closes_all_edge_connections() {
}
}
() = sleep(Duration::from_secs(15)) => {
panic!("Timed out waiting for edge connection to close after session transition");
panic!("Timed out waiting for edge connection to close after epoch transition");
}
}
}
}
/// After a session transition with an updated membership, the edge behaviour
/// After an epoch transition with an updated membership, the edge behaviour
/// should accept new edge connections based on the new membership.
#[test(tokio::test)]
async fn session_transition_updates_membership_for_new_connections() {
async fn epoch_transition_updates_membership_for_new_connections() {
// Initially, edge_swarm_1's peer id is in the membership (core), so it
// should be rejected. After session transition with a different membership
// should be rejected. After epoch transition with a different membership
// that does NOT contain edge_swarm_1, it becomes an edge node.
let mut edge_swarm = TestSwarm::new_ephemeral(|_| StreamBehaviour::new());
let edge_peer_id = *edge_swarm.local_peer_id();
@ -110,7 +108,7 @@ async fn session_transition_updates_membership_for_new_connections() {
}
}
// Transition to a new session where the membership no longer includes
// Transition to a new epoch where the membership no longer includes
// edge_peer_id. Now edge_peer_id is a real edge node.
let other_core_peer = PeerId::random();
let new_membership = Membership::new_without_local(&[Node {
@ -118,9 +116,7 @@ async fn session_transition_updates_membership_for_new_connections() {
id: other_core_peer,
public_key: Ed25519PublicKey::from_bytes(&[0; _]).unwrap(),
}]);
blend_swarm
.behaviour_mut()
.start_new_session(new_membership);
blend_swarm.behaviour_mut().start_new_epoch(new_membership);
// Now edge_swarm should be able to connect and upgrade.
let _stream = edge_swarm
@ -130,6 +126,6 @@ async fn session_transition_updates_membership_for_new_connections() {
assert_eq!(
blend_swarm.behaviour().upgraded_edge_peers.len(),
1,
"Edge peer should be accepted after session transition updated membership"
"Edge peer should be accepted after epoch transition updated membership"
);
}

View File

@ -1,4 +1,4 @@
mod bootstrapping;
mod epoch;
mod message_handling;
mod session;
mod utils;

View File

@ -34,8 +34,6 @@ pub fn valid_proof_of_core_quota_inputs(
.unwrap()
.into(),
},
// Not relevant for core quota proofs
session: 1,
leader: LeaderInputs {
pol_epoch_nonce: BigUint::from(1u64).into(),
pol_ledger_aged: BigUint::from(1u64).into(),
@ -168,8 +166,6 @@ pub fn valid_proof_of_leadership_quota_inputs(
lottery_0,
lottery_1,
},
// Not relevant for leadership quota proofs
session: 1,
core: CoreInputs {
zk_root: BigUint::from(1u64).into(),
quota: 1,

View File

@ -29,7 +29,6 @@ impl TryFrom<Inputs> for PoQWitnessInputs {
core_root: value.public.core.zk_root,
pol_epoch_nonce: value.public.leader.pol_epoch_nonce,
pol_ledger_aged: value.public.leader.pol_ledger_aged,
session: value.public.session,
lottery_0: value.public.leader.lottery_0,
lottery_1: value.public.leader.lottery_1,
};

View File

@ -5,7 +5,10 @@ use zeroize::ZeroizeOnDrop;
use crate::{
CorePathAndSelectors, ZkHash,
quota::{SelectionRandomnessSecretInput, inputs::prove::PublicInputs},
quota::{
SelectionRandomnessSecretInput,
inputs::prove::{PublicInputs, public::LeaderInputs},
},
};
/// Private inputs for all types of Proof of Quota. Spec: <https://www.notion.so/nomos-tech/Proof-of-Quota-Specification-215261aa09df81d88118ee22205cbafe?source=copy_link#215261aa09df81a18576f67b910d34d4>.
@ -48,12 +51,17 @@ impl Inputs {
#[must_use]
pub fn get_secret_selection_randomness_sk(
&self,
PublicInputs { session, .. }: &PublicInputs,
PublicInputs {
leader: LeaderInputs {
pol_epoch_nonce, ..
},
..
}: &PublicInputs,
) -> SelectionRandomnessSecretInput {
match &self.proof_type {
ProofType::CoreQuota(core_quota_private_inputs) => {
SelectionRandomnessSecretInput::Core {
session_number: *session,
epoch_nonce: *pol_epoch_nonce,
sk: core_quota_private_inputs.core_sk,
}
}

View File

@ -9,7 +9,6 @@ use crate::{ZkHash, quota::Ed25519PublicKey};
#[derive(Clone, Copy)]
pub struct Inputs {
pub signing_key: Ed25519PublicKey,
pub session: u64,
pub core: CoreInputs,
pub leader: LeaderInputs,
}
@ -18,7 +17,6 @@ impl Debug for Inputs {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Inputs")
.field("signing_key", &hex::encode(self.signing_key.as_bytes()))
.field("session", &self.session)
.field("core", &self.core)
.field("leader", &self.leader)
.finish()
@ -32,7 +30,6 @@ impl Default for Inputs {
Self {
signing_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(),
session: 1,
core: CoreInputs::default(),
leader: LeaderInputs::default(),
}

View File

@ -45,7 +45,6 @@ impl From<Inputs> for PoQVerifierInput {
leader_quota: value.prove_inputs.leader.message_quota,
pol_epoch_nonce: value.prove_inputs.leader.pol_epoch_nonce,
pol_ledger_aged: value.prove_inputs.leader.pol_ledger_aged,
session: value.prove_inputs.session,
lottery_0: value.prove_inputs.leader.lottery_0,
lottery_1: value.prove_inputs.leader.lottery_1,
}

View File

@ -210,7 +210,7 @@ impl PartialEq<ProofOfQuota> for VerifiedProofOfQuota {
pub enum SelectionRandomnessSecretInput {
Core {
sk: ZkHash,
session_number: u64,
epoch_nonce: ZkHash,
},
Leadership {
note_secret_key: ZkHash,
@ -228,9 +228,7 @@ fn generate_secret_selection_randomness(
key_index: u64,
) -> ZkHash {
let (first_element, second_element) = match input {
SelectionRandomnessSecretInput::Core { sk, session_number } => {
(sk, (*session_number).into())
}
SelectionRandomnessSecretInput::Core { sk, epoch_nonce } => (sk, (*epoch_nonce)),
SelectionRandomnessSecretInput::Leadership {
note_secret_key,
slot_number,

View File

@ -133,12 +133,10 @@ fn generate_inputs<const INPUTS: usize>() -> PoQInputs<INPUTS> {
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
};
let session = 1;
let signing_key = Ed25519PublicKey::from_bytes(&[10; ED25519_PUBLIC_KEY_SIZE]).unwrap();
PublicInputs {
core: core_inputs,
leader: leader_inputs,
session,
signing_key,
}
};

View File

@ -14,12 +14,12 @@ const LOG_TARGET: &str = blend::scheduling::COVER;
/// A scheduler for cover messages that can yield a new cover message when
/// polled, as per the specification.
pub struct SessionCoverTraffic<Rng, RoundClock> {
pub struct EpochCoverTraffic<Rng, RoundClock> {
/// The clock ticking at the beginning of each new round.
round_clock: RoundClock,
/// The number of cover messages still to be emitted.
remaining_messages: u64,
/// The number of rounds remaining in this session (including the current
/// The number of rounds remaining in this epoch (including the current
/// one).
remaining_rounds: Round,
/// The count of data messages generated by the node that have not yet been
@ -33,39 +33,30 @@ pub struct SessionCoverTraffic<Rng, RoundClock> {
rng: Rng,
}
impl<Rng, RoundClock> SessionCoverTraffic<Rng, RoundClock>
impl<Rng, RoundClock> EpochCoverTraffic<Rng, RoundClock>
where
Rng: rand::Rng,
{
/// Initialize the scheduler with the required details.
pub fn new(
Settings {
additional_safety_intervals,
expected_intervals_per_session,
rounds_per_interval,
rounds_per_epoch,
message_count,
}: Settings,
rng: Rng,
round_clock: RoundClock,
) -> Self {
let total_intervals = expected_intervals_per_session
.get()
.checked_add(additional_safety_intervals)
.expect("Overflow when calculating total intervals per session.");
let total_rounds = total_intervals
.checked_mul(rounds_per_interval.get())
.expect("Overflow when calculating total rounds per session.");
trace!(target: LOG_TARGET, "Creating new cover message scheduler with {total_rounds} total rounds.");
trace!(target: LOG_TARGET, "Creating new cover message scheduler with {rounds_per_epoch} total rounds.");
assert!(
message_count <= total_rounds,
message_count <= rounds_per_epoch.get(),
"The number of messages to emit must be at most the total number of rounds."
);
Self {
round_clock,
remaining_messages: message_count,
remaining_rounds: u128::from(total_rounds).into(),
remaining_rounds: NonZeroU128::from(rounds_per_epoch).get().into(),
unprocessed_data_messages: 0,
rng,
}
@ -132,7 +123,7 @@ where
}
}
impl<Rng, RoundClock> SessionCoverTraffic<Rng, RoundClock> {
impl<Rng, RoundClock> EpochCoverTraffic<Rng, RoundClock> {
#[cfg(test)]
pub const fn with_test_values(
round_clock: RoundClock,
@ -157,7 +148,7 @@ impl<Rng, RoundClock> SessionCoverTraffic<Rng, RoundClock> {
/// Notify the scheduler that a new data message has been queued and will be
/// released in the next round, which will result in one of the unscheduled
/// cover messages from now to the end of the session to be randomly
/// cover messages from now to the end of the epoch to be randomly
/// skipped.
pub fn notify_new_data_message(&mut self) {
self.unprocessed_data_messages = self
@ -168,7 +159,7 @@ impl<Rng, RoundClock> SessionCoverTraffic<Rng, RoundClock> {
}
}
impl<Rng, RoundClock> Stream for SessionCoverTraffic<Rng, RoundClock>
impl<Rng, RoundClock> Stream for EpochCoverTraffic<Rng, RoundClock>
where
RoundClock: Stream<Item = Round> + Unpin,
Rng: rand::Rng + Unpin,
@ -197,14 +188,9 @@ where
/// The settings to initialize the cover message scheduler.
#[derive(Debug, Clone, Copy)]
pub struct Settings {
/// The number of intervals to use as the safety buffer in case of longer
/// sessions, as per the spec.
pub additional_safety_intervals: u64,
/// The number of expected intervals per session.
pub expected_intervals_per_session: NonZeroU64,
/// The number of rounds per interval.
pub rounds_per_interval: NonZeroU64,
/// The maximum number of messages to generate in this session, before
/// The number of rounds per epoch.
pub rounds_per_epoch: NonZeroU64,
/// The maximum number of messages to generate in this epoch, before
/// accounting for any generated data messages.
pub message_count: u64,
}
@ -217,11 +203,11 @@ mod tests {
use rand::rngs::OsRng;
use tokio_stream::iter;
use crate::cover_traffic::SessionCoverTraffic;
use crate::cover_traffic::EpochCoverTraffic;
#[tokio::test]
async fn no_emission_on_empty_schedule() {
let mut scheduler = SessionCoverTraffic {
let mut scheduler = EpochCoverTraffic {
round_clock: iter([0u128]).map(Into::into),
remaining_messages: 0,
remaining_rounds: 1.into(),
@ -235,7 +221,7 @@ mod tests {
#[tokio::test]
async fn no_emission_on_unscheduled_round() {
let mut scheduler = SessionCoverTraffic {
let mut scheduler = EpochCoverTraffic {
round_clock: iter([0u128]).map(Into::into),
remaining_messages: 0,
remaining_rounds: 0.into(),
@ -251,7 +237,7 @@ mod tests {
async fn emission_on_scheduled_round_without_processed_messages() {
// With remaining_messages == remaining_rounds, probability is 1.0,
// so this round is guaranteed to be a release round.
let mut scheduler = SessionCoverTraffic {
let mut scheduler = EpochCoverTraffic {
round_clock: iter([0u128]).map(Into::into),
remaining_messages: 1,
remaining_rounds: 1.into(),
@ -271,7 +257,7 @@ mod tests {
// guaranteed to be a release round. With 1 unprocessed data message
// and 1 pending cover message, the skip threshold is 1/1 = 1.0, so
// the cover message is always skipped.
let mut scheduler = SessionCoverTraffic {
let mut scheduler = EpochCoverTraffic {
round_clock: iter([0u128]).map(Into::into),
remaining_messages: 1,
remaining_rounds: 1.into(),
@ -292,7 +278,7 @@ mod tests {
// remaining_messages == remaining_rounds == 3 guarantees every round
// is a release round. With 2 unprocessed data messages, exactly 2
// release rounds will be skipped and 1 cover message will be emitted.
let mut scheduler = SessionCoverTraffic {
let mut scheduler = EpochCoverTraffic {
round_clock: iter([0u128, 1u128, 2u128]).map(Into::into),
remaining_messages: 3,
remaining_rounds: 3.into(),
@ -318,7 +304,7 @@ mod tests {
#[test]
fn notify_new_data_message_without_scheduled_rounds() {
let mut scheduler = SessionCoverTraffic {
let mut scheduler = EpochCoverTraffic {
round_clock: empty(),
remaining_messages: 0,
remaining_rounds: 0.into(),
@ -331,7 +317,7 @@ mod tests {
#[test]
fn notify_new_data_message_with_scheduled_rounds() {
let mut scheduler = SessionCoverTraffic {
let mut scheduler = EpochCoverTraffic {
round_clock: empty(),
remaining_messages: 1,
remaining_rounds: 1.into(),

View File

@ -0,0 +1,228 @@
use std::{
future::Future as _,
pin::Pin,
task::{Context, Poll},
time::Duration,
};
use futures::StreamExt as _;
use tokio::time::{Sleep, sleep};
use crate::stream::{FirstReadyStreamError, UninitializedFirstReadyStream};
/// A staging type that initializes a [`EpochEventStream`] by consuming
/// the first [`Event`] from the underlying stream, expected to be yielded
/// within a short timeout.
pub struct UninitializedEpochEventStream<EventStream> {
stream: UninitializedFirstReadyStream<EventStream>,
transition_period: Duration,
}
impl<EventStream> UninitializedEpochEventStream<EventStream> {
#[must_use]
pub const fn new(event_stream: EventStream, transition_period: Duration) -> Self {
Self {
stream: UninitializedFirstReadyStream::new(event_stream),
transition_period,
}
}
}
impl<EventStream> UninitializedEpochEventStream<EventStream>
where
EventStream: futures::Stream + Unpin,
{
/// Initializes a [`EpochEventStream`] by consuming the first [`Epoch`]
/// from the underlying stream.
///
/// It returns the first [`Epoch`] and the initialized
/// [`EpochEventStream`], awaiting the first epoch for as long as
/// necessary.
/// It returns an error only if the underlying stream closes before yielding
/// an epoch.
pub async fn await_first_ready(
self,
) -> Result<(EventStream::Item, EpochEventStream<EventStream>), FirstReadyStreamError> {
let (first_epoch, remaining_stream) = self.stream.first().await?;
Ok((
first_epoch,
EpochEventStream::new(remaining_stream, self.transition_period),
))
}
}
#[derive(Clone, Debug)]
pub enum EpochEvent<Event> {
NewEpoch(Event),
TransitionPeriodExpired,
}
/// A stream that alternates between yielding [`EpochEvent::NewEpoch`]
/// and [`EpochEvent::TransitionPeriodExpired`].
///
/// It wraps a stream of [`Epoch`]s and yields a [`EpochEvent::NewEpoch`]
/// as soon as a new [`Epoch`] is available from the inner stream.
/// Then, it yields a [`EpochEvent::TransitionPeriodExpired`] after
/// the transition period has elapsed.
///
/// # Stream Timeline
/// ```text
/// event stream : O--E-------O--E--------------O--E-------
/// epoch stream : |----E1----|--------E2-------|----E3----
///
/// (O: NewEpoch, E: TransitionPeriodExpired, E*: Epochs)
/// ```
pub struct EpochEventStream<EventStream> {
event_stream: EventStream,
transition_period: Duration,
transition_period_timer: Option<Pin<Box<Sleep>>>,
}
impl<EventStream> EpochEventStream<EventStream> {
#[must_use]
const fn new(event_stream: EventStream, transition_period: Duration) -> Self {
Self {
event_stream,
transition_period,
transition_period_timer: None,
}
}
}
impl<EventStream> futures::Stream for EpochEventStream<EventStream>
where
EventStream: futures::Stream + Unpin,
{
type Item = EpochEvent<EventStream::Item>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
// Check if a new epoch is available.
match self.event_stream.poll_next_unpin(cx) {
Poll::Ready(Some(epoch)) => {
// Start the transition period timer, and yield the new epoch.
// If the previous transition period timer has not been expired yet,
// it will be overwritten.
self.transition_period_timer = Some(Box::pin(sleep(self.transition_period)));
return Poll::Ready(Some(EpochEvent::NewEpoch(epoch)));
}
Poll::Ready(None) => return Poll::Ready(None),
Poll::Pending => {}
}
// Check if the transition period has expired.
if let Some(timer) = &mut self.transition_period_timer
&& timer.as_mut().poll(cx).is_ready()
{
self.transition_period_timer = None;
return Poll::Ready(Some(EpochEvent::TransitionPeriodExpired));
}
Poll::Pending
}
}
#[cfg(test)]
mod tests {
use futures::StreamExt as _;
use tokio::time::{Instant, interval};
use tokio_stream::wrappers::IntervalStream;
use super::*;
#[tokio::test]
async fn yield_two_events_alternately() {
let epoch_duration = Duration::from_secs(1);
let transition_period = Duration::from_millis(200);
let time_tolerance = Duration::from_millis(100);
let mut stream = EpochEventStream::new(
Box::pin(IntervalStream::new(interval(epoch_duration))),
transition_period,
);
// NewEpoch should be emitted immediately.
let start_time = Instant::now();
assert!(matches!(stream.next().await, Some(EpochEvent::NewEpoch(_))));
let elapsed = start_time.elapsed();
let tolerance = Duration::from_millis(50);
assert!(elapsed <= tolerance, "elapsed:{elapsed:?}");
// TransitionEnd should be emitted after transition_period.
let start_time = Instant::now();
assert!(matches!(
stream.next().await,
Some(EpochEvent::TransitionPeriodExpired)
));
let elapsed = start_time.elapsed();
assert!(
elapsed.abs_diff(transition_period) <= time_tolerance,
"elapsed:{elapsed:?}, expected:{transition_period:?}",
);
// NewEpoch should be emitted after epoch_duration - transition_period.
let start_time = Instant::now();
assert!(matches!(stream.next().await, Some(EpochEvent::NewEpoch(_))));
let elapsed = start_time.elapsed();
assert!(
elapsed.abs_diff(epoch_duration.checked_sub(transition_period).unwrap())
<= time_tolerance,
"elapsed:{elapsed:?}, expected:{:?}",
epoch_duration.checked_sub(transition_period).unwrap()
);
// TransitionEnd should be emitted after transition_period.
let start_time = Instant::now();
assert!(matches!(
stream.next().await,
Some(EpochEvent::TransitionPeriodExpired)
));
let elapsed = start_time.elapsed();
assert!(
elapsed.abs_diff(transition_period) <= time_tolerance,
"elapsed:{elapsed:?}, expected:{transition_period:?}",
);
}
#[tokio::test]
async fn transition_period_shorter_than_epoch() {
let epoch_duration = Duration::from_millis(500);
let transition_period = Duration::from_millis(600);
let time_tolerance = Duration::from_millis(50);
let mut stream = EpochEventStream::new(
Box::pin(IntervalStream::new(interval(epoch_duration))),
transition_period,
);
// NewEpoch should be emitted immediately.
let start_time = Instant::now();
assert!(matches!(stream.next().await, Some(EpochEvent::NewEpoch(_))));
let elapsed = start_time.elapsed();
assert!(elapsed <= time_tolerance, "elapsed:{elapsed:?}");
// NewEpoch should be emitted again after epoch_duration.
let start_time = Instant::now();
assert!(matches!(stream.next().await, Some(EpochEvent::NewEpoch(_))));
let elapsed = start_time.elapsed();
assert!(
elapsed.abs_diff(epoch_duration) <= time_tolerance,
"elapsed:{elapsed:?}, expected:{epoch_duration:?}",
);
}
#[tokio::test]
async fn first_ready_stream_yields_first_item_immediately() {
// Use an underlying stream that yields the first item nearly immediately.
let stream = UninitializedFirstReadyStream::new(
IntervalStream::new(interval(Duration::from_secs(1)))
.enumerate()
.map(|(i, _)| i),
);
let (first, mut stream) = stream.first().await.expect("first item should be yielded");
assert_eq!(first, 0);
// Next items are yielded normally.
assert_eq!(stream.next().await, Some(1));
assert_eq!(stream.next().await, Some(2));
}
}

View File

@ -1,12 +1,12 @@
pub mod epoch;
pub mod membership;
pub mod message_blend;
pub mod session;
pub use message_blend::crypto::{
deserialize_encapsulated_message, serialize_encapsulated_message_with_verified_public_header,
serialize_encapsulated_message_with_verified_signature,
};
pub mod message_scheduler;
pub use message_scheduler::SessionMessageScheduler;
pub use message_scheduler::EpochMessageScheduler;
pub mod stream;
mod cover_traffic;

View File

@ -8,7 +8,7 @@ use multiaddr::Multiaddr;
use rand::{Rng, seq::IteratorRandom as _};
use serde::{Deserialize, Serialize};
/// A set of core nodes in a session.
/// A set of core nodes in an epoch.
#[derive(Clone, Debug)]
pub struct Membership<NodeId> {
/// All nodes, including local and remote.

View File

@ -5,9 +5,7 @@ use lb_blend_message::{
Error, PaddedPayloadBody, PayloadType, crypto::proofs::PoQVerificationInputsMinusSigningKey,
input::EncapsulationInput,
};
use lb_blend_proofs::quota::inputs::prove::{
private::ProofOfLeadershipQuotaInputs, public::LeaderInputs,
};
use lb_blend_proofs::quota::inputs::prove::private::ProofOfLeadershipQuotaInputs;
use lb_cryptarchia_engine::Epoch;
use lb_groth16::fr_to_bytes;
use lb_key_management_system_keys::keys::X25519PrivateKey;
@ -16,28 +14,27 @@ use crate::{
membership::Membership,
message_blend::{
crypto::{
EncapsulatedMessageWithVerifiedPublicHeader, SessionCryptographicProcessorSettings,
EncapsulatedMessageWithVerifiedPublicHeader, EpochCryptographicProcessorSettings,
},
provers::{ProofsGeneratorSettings, core_and_leader::CoreAndLeaderProofsGenerator},
},
};
/// [`SessionCryptographicProcessor`] is responsible for only wrapping
/// [`EpochCryptographicProcessor`] is responsible for only wrapping
/// cover and data messages for the message indistinguishability.
///
/// Each instance is meant to be used during a single session.
pub struct SessionCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator> {
/// Each instance is meant to be used during a single epoch.
pub struct EpochCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator> {
num_blend_layers: NonZeroU64,
/// The non-ephemeral encryption key (NEK) for decapsulating messages.
non_ephemeral_encryption_key: X25519PrivateKey,
membership: Membership<NodeId>,
session: u64,
proofs_generator: ProofsGenerator,
_phantom: PhantomData<CorePoQGenerator>,
}
impl<NodeId, CorePoQGenerator, ProofsGenerator>
SessionCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator>
EpochCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator>
{
pub(super) const fn non_ephemeral_encryption_key(&self) -> &X25519PrivateKey {
&self.non_ephemeral_encryption_key
@ -47,10 +44,6 @@ impl<NodeId, CorePoQGenerator, ProofsGenerator>
&self.membership
}
pub const fn session(&self) -> u64 {
self.session
}
#[cfg(test)]
pub const fn proofs_generator(&self) -> &ProofsGenerator {
&self.proofs_generator
@ -58,20 +51,20 @@ impl<NodeId, CorePoQGenerator, ProofsGenerator>
}
impl<NodeId, CorePoQGenerator, ProofsGenerator>
SessionCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator>
EpochCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator>
where
ProofsGenerator: CoreAndLeaderProofsGenerator<CorePoQGenerator>,
{
#[must_use]
pub fn new(
settings: SessionCryptographicProcessorSettings,
settings: EpochCryptographicProcessorSettings,
membership: Membership<NodeId>,
public_info: PoQVerificationInputsMinusSigningKey,
core_proof_of_quota_generator: CorePoQGenerator,
epoch: Epoch,
) -> Self {
tracing::trace!(
"Creating session cryptographic processor with public info {public_info:?} and epoch {epoch:?}"
"Creating epoch cryptographic processor with public info {public_info:?} and epoch {epoch:?}"
);
let generator_settings = ProofsGeneratorSettings {
@ -89,35 +82,22 @@ where
generator_settings,
core_proof_of_quota_generator,
),
session: public_info.session,
_phantom: PhantomData,
}
}
pub fn rotate_epoch(&mut self, new_epoch_public_info: LeaderInputs, new_epoch: Epoch) {
tracing::trace!(
"Rotating epoch with new public info {new_epoch_public_info:?} and new epoch {new_epoch:?}"
);
self.proofs_generator
.rotate_epoch(new_epoch_public_info, new_epoch);
}
pub fn set_epoch_private(
&mut self,
new_epoch_private: ProofOfLeadershipQuotaInputs,
new_epoch_public_info: LeaderInputs,
new_epoch: Epoch,
target_epoch: Epoch,
) {
self.proofs_generator.set_epoch_private(
new_epoch_private,
new_epoch_public_info,
new_epoch,
);
self.proofs_generator
.set_epoch_private(new_epoch_private, target_epoch);
}
}
impl<NodeId, CorePoQGenerator, ProofsGenerator>
SessionCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator>
EpochCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator>
where
NodeId: Eq + Hash + 'static,
ProofsGenerator: CoreAndLeaderProofsGenerator<CorePoQGenerator>,
@ -229,67 +209,18 @@ mod test {
use lb_cryptarchia_engine::Epoch;
use lb_groth16::{Field as _, Fr};
use lb_key_management_system_keys::keys::{ED25519_PUBLIC_KEY_SIZE, Ed25519PublicKey};
use multiaddr::{Multiaddr, PeerId};
use libp2p::PeerId;
use multiaddr::Multiaddr;
use super::SessionCryptographicProcessor;
use super::EpochCryptographicProcessor;
use crate::{
membership::{Membership, Node},
message_blend::crypto::{
SessionCryptographicProcessorSettings,
EpochCryptographicProcessorSettings,
test_utils::{MockCorePoQGenerator, TestEpochChangeCoreAndLeaderProofsGenerator},
},
};
#[test]
fn epoch_rotation() {
let mut processor = SessionCryptographicProcessor::<
_,
_,
TestEpochChangeCoreAndLeaderProofsGenerator,
>::new(
SessionCryptographicProcessorSettings {
non_ephemeral_encryption_key: [0; _].into(),
num_blend_layers: NonZeroU64::new(1).unwrap(),
},
Membership::new_without_local(&[Node {
address: Multiaddr::empty(),
id: PeerId::random(),
public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(),
}]),
PoQVerificationInputsMinusSigningKey {
session: 1,
core: CoreInputs {
quota: 1,
zk_root: ZkHash::ZERO,
},
leader: LeaderInputs {
message_quota: 1,
pol_epoch_nonce: ZkHash::ZERO,
pol_ledger_aged: ZkHash::ZERO,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
},
},
MockCorePoQGenerator,
Epoch::new(0),
);
let new_leader_inputs = LeaderInputs {
pol_ledger_aged: ZkHash::ONE,
pol_epoch_nonce: ZkHash::ONE,
message_quota: 2,
lottery_0: Fr::ONE,
lottery_1: Fr::ONE,
};
processor.rotate_epoch(new_leader_inputs, Epoch::new(1));
assert_eq!(
processor.proofs_generator.0.public_inputs.leader,
new_leader_inputs
);
}
#[test]
fn set_epoch_private() {
let leader_inputs = LeaderInputs {
@ -299,31 +230,28 @@ mod test {
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
};
let mut processor = SessionCryptographicProcessor::<
_,
_,
TestEpochChangeCoreAndLeaderProofsGenerator,
>::new(
SessionCryptographicProcessorSettings {
non_ephemeral_encryption_key: [0; _].into(),
num_blend_layers: NonZeroU64::new(1).unwrap(),
},
Membership::new_without_local(&[Node {
address: Multiaddr::empty(),
id: PeerId::random(),
public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(),
}]),
PoQVerificationInputsMinusSigningKey {
session: 1,
core: CoreInputs {
quota: 1,
zk_root: ZkHash::ZERO,
let mut processor =
EpochCryptographicProcessor::<_, _, TestEpochChangeCoreAndLeaderProofsGenerator>::new(
EpochCryptographicProcessorSettings {
non_ephemeral_encryption_key: [0; _].into(),
num_blend_layers: NonZeroU64::new(1).unwrap(),
},
leader: leader_inputs,
},
MockCorePoQGenerator,
Epoch::new(0),
);
Membership::new_without_local(&[Node {
address: Multiaddr::empty(),
id: PeerId::random(),
public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE])
.unwrap(),
}]),
PoQVerificationInputsMinusSigningKey {
core: CoreInputs {
quota: 1,
zk_root: ZkHash::ZERO,
},
leader: leader_inputs,
},
MockCorePoQGenerator,
Epoch::new(0),
);
let new_private_inputs = ProofOfLeadershipQuotaInputs {
aged_path_and_selectors: [(ZkHash::ONE, true); _],
@ -334,8 +262,8 @@ mod test {
transaction_hash: ZkHash::ONE,
};
processor.set_epoch_private(new_private_inputs.clone(), leader_inputs, Epoch::new(1));
processor.set_epoch_private(new_private_inputs.clone(), Epoch::new(1));
assert!(processor.proofs_generator.1 == Some(new_private_inputs));
assert!(processor.proofs_generator.0 == Some(new_private_inputs));
}
}

View File

@ -8,49 +8,47 @@ use lb_blend_message::{
validated::RequiredProofOfSelectionVerificationInputs,
},
};
use lb_blend_proofs::quota::inputs::prove::public::LeaderInputs;
use lb_cryptarchia_engine::Epoch;
use crate::{
membership::Membership,
message_blend::{
crypto::{
EncapsulatedMessageWithVerifiedPublicHeader, SessionCryptographicProcessorSettings,
core_and_leader::send::SessionCryptographicProcessor as SenderSessionCryptographicProcessor,
EncapsulatedMessageWithVerifiedPublicHeader, EpochCryptographicProcessorSettings,
core_and_leader::send::EpochCryptographicProcessor as SenderEpochCryptographicProcessor,
},
provers::core_and_leader::CoreAndLeaderProofsGenerator,
},
};
/// [`SessionCryptographicProcessor`] is responsible for wrapping both cover and
/// [`EpochCryptographicProcessor`] is responsible for wrapping both cover and
/// data messages and unwrapping messages for the message indistinguishability.
///
/// Each instance is meant to be used during a single session.
/// Each instance is meant to be used during a single epoch.
///
/// This processor is suitable for core nodes.
pub struct SessionCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier>
{
sender_processor:
SenderSessionCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator>,
pub struct EpochCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier> {
sender_processor: SenderEpochCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator>,
proofs_verifier: ProofsVerifier,
epoch: Epoch,
}
impl<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier>
SessionCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier>
EpochCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier>
where
ProofsGenerator: CoreAndLeaderProofsGenerator<CorePoQGenerator>,
ProofsVerifier: ProofsVerifierTrait,
{
#[must_use]
pub fn new(
settings: SessionCryptographicProcessorSettings,
settings: EpochCryptographicProcessorSettings,
membership: Membership<NodeId>,
public_info: PoQVerificationInputsMinusSigningKey,
core_proof_of_quota_generator: CorePoQGenerator,
epoch: Epoch,
) -> Self {
Self {
sender_processor: SenderSessionCryptographicProcessor::new(
sender_processor: SenderEpochCryptographicProcessor::new(
settings,
membership,
public_info,
@ -58,27 +56,25 @@ where
epoch,
),
proofs_verifier: ProofsVerifier::new(public_info),
epoch,
}
}
pub fn rotate_epoch(&mut self, new_epoch_public: LeaderInputs, new_epoch: Epoch) {
self.sender_processor
.rotate_epoch(new_epoch_public, new_epoch);
self.proofs_verifier
.start_epoch_transition(new_epoch_public);
}
}
impl<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier>
SessionCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier>
EpochCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier>
{
pub const fn verifier(&self) -> &ProofsVerifier {
&self.proofs_verifier
}
pub const fn epoch(&self) -> Epoch {
self.epoch
}
}
impl<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier>
SessionCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier>
EpochCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier>
where
ProofsVerifier: ProofsVerifierTrait,
{
@ -98,18 +94,14 @@ where
&self.proofs_verifier,
)
}
pub fn complete_epoch_transition(&mut self) {
self.proofs_verifier.complete_epoch_transition();
}
}
// `Deref` and `DerefMut` so we can call the `encapsulate*` methods exposed by
// the send-only processor.
impl<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier> Deref
for SessionCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier>
for EpochCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier>
{
type Target = SenderSessionCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator>;
type Target = SenderEpochCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator>;
fn deref(&self) -> &Self::Target {
&self.sender_processor
@ -117,7 +109,7 @@ impl<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier> Deref
}
impl<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier> DerefMut
for SessionCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier>
for EpochCryptographicProcessor<NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier>
{
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sender_processor
@ -142,8 +134,8 @@ mod test {
use crate::{
membership::{Membership, Node},
message_blend::crypto::{
SessionCryptographicProcessorSettings,
core_and_leader::send_and_receive::SessionCryptographicProcessor,
EpochCryptographicProcessorSettings,
core_and_leader::send_and_receive::EpochCryptographicProcessor,
test_utils::{
MockCorePoQGenerator, TestEpochChangeCoreAndLeaderProofsGenerator,
TestEpochChangeProofsVerifier,
@ -151,218 +143,6 @@ mod test {
},
};
#[test]
fn epoch_rotation() {
let mut processor = SessionCryptographicProcessor::<
_,
_,
TestEpochChangeCoreAndLeaderProofsGenerator,
TestEpochChangeProofsVerifier,
>::new(
SessionCryptographicProcessorSettings {
non_ephemeral_encryption_key: [0; _].into(),
num_blend_layers: NonZeroU64::new(1).unwrap(),
},
Membership::new_without_local(&[Node {
address: Multiaddr::empty(),
id: PeerId::random(),
public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(),
}]),
PoQVerificationInputsMinusSigningKey {
session: 1,
core: CoreInputs {
quota: 1,
zk_root: ZkHash::ZERO,
},
leader: LeaderInputs {
message_quota: 1,
pol_epoch_nonce: ZkHash::ZERO,
pol_ledger_aged: ZkHash::ZERO,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
},
},
MockCorePoQGenerator,
Epoch::new(0),
);
let new_leader_inputs = LeaderInputs {
pol_ledger_aged: ZkHash::ONE,
pol_epoch_nonce: ZkHash::ONE,
message_quota: 2,
lottery_0: Fr::ONE,
lottery_1: Fr::ONE,
};
processor.rotate_epoch(new_leader_inputs, Epoch::new(1));
assert_eq!(processor.proofs_verifier.0.leader, new_leader_inputs);
assert_eq!(
processor.proofs_verifier.1,
Some(LeaderInputs {
message_quota: 1,
pol_epoch_nonce: ZkHash::ZERO,
pol_ledger_aged: ZkHash::ZERO,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
})
);
assert_eq!(
processor.proofs_generator().0.public_inputs.leader,
new_leader_inputs
);
processor.complete_epoch_transition();
assert!(processor.proofs_verifier.1.is_none());
}
/// Consecutive epoch rotations: each call replaces the previous epoch
/// inputs with the old current inputs.
#[test]
fn consecutive_epoch_rotations() {
let initial_leader = LeaderInputs {
message_quota: 1,
pol_epoch_nonce: ZkHash::ZERO,
pol_ledger_aged: ZkHash::ZERO,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
};
let mut processor = SessionCryptographicProcessor::<
_,
_,
TestEpochChangeCoreAndLeaderProofsGenerator,
TestEpochChangeProofsVerifier,
>::new(
SessionCryptographicProcessorSettings {
non_ephemeral_encryption_key: [0; _].into(),
num_blend_layers: NonZeroU64::new(1).unwrap(),
},
Membership::new_without_local(&[Node {
address: Multiaddr::empty(),
id: PeerId::random(),
public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(),
}]),
PoQVerificationInputsMinusSigningKey {
session: 1,
core: CoreInputs {
quota: 1,
zk_root: ZkHash::ZERO,
},
leader: initial_leader,
},
MockCorePoQGenerator,
Epoch::new(0),
);
// Rotate to epoch 1.
let epoch_1_leader = LeaderInputs {
pol_ledger_aged: ZkHash::ONE,
pol_epoch_nonce: ZkHash::ONE,
message_quota: 2,
lottery_0: Fr::ONE,
lottery_1: Fr::ONE,
};
processor.rotate_epoch(epoch_1_leader, Epoch::new(1));
assert_eq!(processor.proofs_verifier.0.leader, epoch_1_leader);
assert_eq!(processor.proofs_verifier.1, Some(initial_leader));
// Rotate again to epoch 2 without completing the epoch 0→1 transition.
// The previous epoch inputs should now be epoch 1's inputs.
let epoch_2_leader = LeaderInputs {
pol_ledger_aged: ZkHash::ZERO,
pol_epoch_nonce: ZkHash::ONE,
message_quota: 3,
lottery_0: Fr::ZERO,
lottery_1: Fr::ONE,
};
processor.rotate_epoch(epoch_2_leader, Epoch::new(2));
assert_eq!(processor.proofs_verifier.0.leader, epoch_2_leader);
assert_eq!(processor.proofs_verifier.1, Some(epoch_1_leader));
assert_eq!(
processor.proofs_generator().0.public_inputs.leader,
epoch_2_leader
);
// Complete the transition — clears previous epoch.
processor.complete_epoch_transition();
assert!(processor.proofs_verifier.1.is_none());
assert_eq!(processor.proofs_verifier.0.leader, epoch_2_leader);
}
/// `complete_epoch_transition` followed by
/// `NewEpochAndOldEpochTransitionExpired` pattern: complete first, then
/// rotate.
#[test]
fn complete_then_rotate_epoch() {
let initial_leader = LeaderInputs {
message_quota: 1,
pol_epoch_nonce: ZkHash::ZERO,
pol_ledger_aged: ZkHash::ZERO,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
};
let mut processor = SessionCryptographicProcessor::<
_,
_,
TestEpochChangeCoreAndLeaderProofsGenerator,
TestEpochChangeProofsVerifier,
>::new(
SessionCryptographicProcessorSettings {
non_ephemeral_encryption_key: [0; _].into(),
num_blend_layers: NonZeroU64::new(1).unwrap(),
},
Membership::new_without_local(&[Node {
address: Multiaddr::empty(),
id: PeerId::random(),
public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(),
}]),
PoQVerificationInputsMinusSigningKey {
session: 1,
core: CoreInputs {
quota: 1,
zk_root: ZkHash::ZERO,
},
leader: initial_leader,
},
MockCorePoQGenerator,
Epoch::new(0),
);
// Rotate to epoch 1.
let epoch_1_leader = LeaderInputs {
pol_ledger_aged: ZkHash::ONE,
pol_epoch_nonce: ZkHash::ONE,
message_quota: 2,
lottery_0: Fr::ONE,
lottery_1: Fr::ONE,
};
processor.rotate_epoch(epoch_1_leader, Epoch::new(1));
// Simulate NewEpochAndOldEpochTransitionExpired:
// first complete, then rotate.
processor.complete_epoch_transition();
assert!(processor.proofs_verifier.1.is_none());
let epoch_2_leader = LeaderInputs {
pol_ledger_aged: ZkHash::ZERO,
pol_epoch_nonce: ZkHash::ONE,
message_quota: 3,
lottery_0: Fr::ZERO,
lottery_1: Fr::ONE,
};
processor.rotate_epoch(epoch_2_leader, Epoch::new(2));
// Verifier now has epoch 2 as current, epoch 1 as previous.
assert_eq!(processor.proofs_verifier.0.leader, epoch_2_leader);
assert_eq!(processor.proofs_verifier.1, Some(epoch_1_leader));
// Generator updated to epoch 2.
assert_eq!(
processor.proofs_generator().0.public_inputs.leader,
epoch_2_leader
);
}
/// `set_epoch_private` propagates private inputs for leader proof
/// generation.
#[test]
@ -374,13 +154,13 @@ mod test {
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
};
let mut processor = SessionCryptographicProcessor::<
let mut processor = EpochCryptographicProcessor::<
_,
_,
TestEpochChangeCoreAndLeaderProofsGenerator,
TestEpochChangeProofsVerifier,
>::new(
SessionCryptographicProcessorSettings {
EpochCryptographicProcessorSettings {
non_ephemeral_encryption_key: [0; _].into(),
num_blend_layers: NonZeroU64::new(1).unwrap(),
},
@ -390,7 +170,6 @@ mod test {
public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(),
}]),
PoQVerificationInputsMinusSigningKey {
session: 1,
core: CoreInputs {
quota: 1,
zk_root: ZkHash::ZERO,
@ -401,7 +180,7 @@ mod test {
Epoch::new(0),
);
assert!(processor.proofs_generator().1.is_none());
assert!(processor.proofs_generator().0.is_none());
let private_inputs = ProofOfLeadershipQuotaInputs {
aged_path_and_selectors: [(ZkHash::ONE, true); _],
@ -412,8 +191,8 @@ mod test {
transaction_hash: ZkHash::ONE,
};
processor.set_epoch_private(private_inputs.clone(), initial_leader, Epoch::new(1));
processor.set_epoch_private(private_inputs.clone(), Epoch::new(1));
assert!(processor.proofs_generator().1 == Some(private_inputs));
assert!(processor.proofs_generator().0 == Some(private_inputs));
}
}

View File

@ -5,9 +5,7 @@ use lb_blend_message::{
Error, PaddedPayloadBody, PayloadType, crypto::proofs::PoQVerificationInputsMinusSigningKey,
input::EncapsulationInput,
};
use lb_blend_proofs::quota::inputs::prove::{
private::ProofOfLeadershipQuotaInputs, public::LeaderInputs,
};
use lb_blend_proofs::quota::inputs::prove::private::ProofOfLeadershipQuotaInputs;
use lb_cryptarchia_engine::Epoch;
use crate::{
@ -21,20 +19,27 @@ use crate::{
},
};
/// [`SessionCryptographicProcessor`] is responsible for only wrapping data
/// [`EpochCryptographicProcessor`] is responsible for only wrapping data
/// messages (no cover messages) for the message indistinguishability.
///
/// Each instance is meant to be used during a single session.
/// Each instance is meant to be used during a single epoch.
///
/// This processor is suitable for non-core nodes that do not need to generate
/// any cover traffic and are hence only interested in blending data messages.
pub struct SessionCryptographicProcessor<NodeId, ProofsGenerator> {
pub struct EpochCryptographicProcessor<NodeId, ProofsGenerator> {
num_blend_layers: NonZeroU64,
membership: Membership<NodeId>,
proofs_generator: ProofsGenerator,
epoch: Epoch,
}
impl<NodeId, ProofsGenerator> SessionCryptographicProcessor<NodeId, ProofsGenerator>
impl<NodeId, ProofsGenerator> EpochCryptographicProcessor<NodeId, ProofsGenerator> {
pub const fn epoch(&self) -> Epoch {
self.epoch
}
}
impl<NodeId, ProofsGenerator> EpochCryptographicProcessor<NodeId, ProofsGenerator>
where
ProofsGenerator: LeaderProofsGenerator,
{
@ -57,21 +62,12 @@ where
num_blend_layers,
membership,
proofs_generator: ProofsGenerator::new(generator_settings, private_info),
epoch,
}
}
pub fn rotate_epoch(
&mut self,
new_epoch_public: LeaderInputs,
new_private_inputs: ProofOfLeadershipQuotaInputs,
new_epoch: Epoch,
) {
self.proofs_generator
.rotate_epoch(new_epoch_public, new_private_inputs, new_epoch);
}
}
impl<NodeId, ProofsGenerator> SessionCryptographicProcessor<NodeId, ProofsGenerator>
impl<NodeId, ProofsGenerator> EpochCryptographicProcessor<NodeId, ProofsGenerator>
where
NodeId: Eq + Hash + 'static,
ProofsGenerator: LeaderProofsGenerator,
@ -146,86 +142,3 @@ where
))
}
}
#[cfg(test)]
mod test {
use std::num::NonZeroU64;
use lb_blend_message::crypto::proofs::PoQVerificationInputsMinusSigningKey;
use lb_blend_proofs::quota::inputs::prove::{
private::ProofOfLeadershipQuotaInputs,
public::{CoreInputs, LeaderInputs},
};
use lb_core::crypto::ZkHash;
use lb_cryptarchia_engine::Epoch;
use lb_groth16::{Field as _, Fr};
use lb_key_management_system_keys::keys::{ED25519_PUBLIC_KEY_SIZE, Ed25519PublicKey};
use libp2p::{Multiaddr, PeerId};
use super::SessionCryptographicProcessor;
use crate::{
membership::{Membership, Node},
message_blend::crypto::test_utils::TestEpochChangeLeaderProofsGenerator,
};
#[test]
fn epoch_rotation() {
let mut processor =
SessionCryptographicProcessor::<_, TestEpochChangeLeaderProofsGenerator>::new(
NonZeroU64::new(1).unwrap(),
Membership::new_without_local(&[Node {
address: Multiaddr::empty(),
id: PeerId::random(),
public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE])
.unwrap(),
}]),
PoQVerificationInputsMinusSigningKey {
session: 1,
core: CoreInputs {
quota: 1,
zk_root: ZkHash::ZERO,
},
leader: LeaderInputs {
message_quota: 1,
pol_epoch_nonce: ZkHash::ZERO,
pol_ledger_aged: ZkHash::ZERO,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
},
},
ProofOfLeadershipQuotaInputs {
aged_path_and_selectors: [(ZkHash::ZERO, false); _],
note_value: 1,
output_number: 1,
secret_key: ZkHash::ZERO,
slot: 1,
transaction_hash: ZkHash::ZERO,
},
Epoch::new(0),
);
let new_leader_inputs = LeaderInputs {
pol_ledger_aged: ZkHash::ONE,
pol_epoch_nonce: ZkHash::ONE,
message_quota: 2,
lottery_0: Fr::ONE,
lottery_1: Fr::ONE,
};
let new_private_inputs = ProofOfLeadershipQuotaInputs {
aged_path_and_selectors: [(ZkHash::ONE, true); _],
note_value: 2,
output_number: 2,
secret_key: ZkHash::ONE,
slot: 2,
transaction_hash: ZkHash::ONE,
};
processor.rotate_epoch(new_leader_inputs, new_private_inputs.clone(), Epoch::new(1));
assert_eq!(
processor.proofs_generator.0.public_inputs.leader,
new_leader_inputs
);
assert!(processor.proofs_generator.1 == new_private_inputs);
}
}

View File

@ -15,18 +15,18 @@ use lb_key_management_system_keys::keys::X25519PrivateKey;
pub mod core_and_leader;
pub use self::core_and_leader::{
send::SessionCryptographicProcessor as CoreAndLeaderSenderOnlySessionCryptographicProcessor,
send_and_receive::SessionCryptographicProcessor as CoreAndLeaderSendAndReceiveSessionCryptographicProcessor,
send::EpochCryptographicProcessor as CoreAndLeaderSenderOnlyEpochCryptographicProcessor,
send_and_receive::EpochCryptographicProcessor as CoreAndLeaderSendAndReceiveEpochCryptographicProcessor,
};
pub mod leader;
pub use self::leader::send::SessionCryptographicProcessor as LeaderSenderOnlySessionCryptographicProcessor;
pub use self::leader::send::EpochCryptographicProcessor as LeaderSenderOnlyEpochCryptographicProcessor;
#[cfg(test)]
mod test_utils;
#[derive(Clone, Derivative)]
#[derivative(Debug)]
pub struct SessionCryptographicProcessorSettings {
pub struct EpochCryptographicProcessorSettings {
/// The non-ephemeral encryption key (NEK) derived from the secret key
/// corresponding to the public key registered in the membership (SDP).
#[derivative(Debug = "ignore")]

View File

@ -8,57 +8,21 @@ use lb_blend_message::{
use lb_blend_proofs::{
quota::{
self, ProofOfQuota, VerifiedProofOfQuota,
inputs::prove::{
PublicInputs, private::ProofOfLeadershipQuotaInputs, public::LeaderInputs,
},
inputs::prove::{PublicInputs, private::ProofOfLeadershipQuotaInputs},
},
selection::{ProofOfSelection, VerifiedProofOfSelection, inputs::VerifyInputs},
};
use lb_core::crypto::ZkHash;
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::{Ed25519PublicKey, UnsecuredEd25519Key};
use lb_key_management_system_keys::keys::Ed25519PublicKey;
use crate::message_blend::{
CoreProofOfQuotaGenerator,
provers::{
BlendLayerProof, ProofsGeneratorSettings, core_and_leader::CoreAndLeaderProofsGenerator,
leader::LeaderProofsGenerator,
},
};
pub struct TestEpochChangeLeaderProofsGenerator(
pub ProofsGeneratorSettings,
pub ProofOfLeadershipQuotaInputs,
);
#[async_trait]
impl LeaderProofsGenerator for TestEpochChangeLeaderProofsGenerator {
fn new(
settings: ProofsGeneratorSettings,
private_inputs: ProofOfLeadershipQuotaInputs,
) -> Self {
Self(settings, private_inputs)
}
fn rotate_epoch(
&mut self,
new_epoch_public: LeaderInputs,
new_private_inputs: ProofOfLeadershipQuotaInputs,
_new_epoch: Epoch,
) {
self.0.public_inputs.leader = new_epoch_public;
self.1 = new_private_inputs;
}
async fn get_next_proof(&mut self) -> BlendLayerProof {
BlendLayerProof {
proof_of_quota: VerifiedProofOfQuota::from_bytes_unchecked([0; _]),
proof_of_selection: VerifiedProofOfSelection::from_bytes_unchecked([0; _]),
ephemeral_signing_key: UnsecuredEd25519Key::from_bytes(&[0; _]),
}
}
}
pub struct MockCorePoQGenerator;
impl CoreProofOfQuotaGenerator for MockCorePoQGenerator {
@ -77,30 +41,25 @@ impl CoreProofOfQuotaGenerator for MockCorePoQGenerator {
}
}
pub struct TestEpochChangeCoreAndLeaderProofsGenerator(
pub ProofsGeneratorSettings,
pub Option<ProofOfLeadershipQuotaInputs>,
);
pub struct TestEpochChangeCoreAndLeaderProofsGenerator(pub Option<ProofOfLeadershipQuotaInputs>);
#[async_trait]
impl<CorePoQGenerator> CoreAndLeaderProofsGenerator<CorePoQGenerator>
for TestEpochChangeCoreAndLeaderProofsGenerator
{
fn new(settings: ProofsGeneratorSettings, _proof_of_quota_generator: CorePoQGenerator) -> Self {
Self(settings, None)
}
fn rotate_epoch(&mut self, new_epoch_public: LeaderInputs, _new_epoch: Epoch) {
self.0.public_inputs.leader = new_epoch_public;
fn new(
_settings: ProofsGeneratorSettings,
_proof_of_quota_generator: CorePoQGenerator,
) -> Self {
Self(None)
}
fn set_epoch_private(
&mut self,
new_epoch_private: ProofOfLeadershipQuotaInputs,
_new_epoch_public: LeaderInputs,
_new_epoch: Epoch,
_target_epoch: Epoch,
) {
self.1 = Some(new_epoch_private);
self.0 = Some(new_epoch_private);
}
async fn get_next_core_proof(&mut self) -> Option<BlendLayerProof> {
@ -112,26 +71,14 @@ impl<CorePoQGenerator> CoreAndLeaderProofsGenerator<CorePoQGenerator>
}
}
pub struct TestEpochChangeProofsVerifier(
pub PoQVerificationInputsMinusSigningKey,
pub Option<LeaderInputs>,
);
pub struct TestEpochChangeProofsVerifier;
#[async_trait]
impl ProofsVerifier for TestEpochChangeProofsVerifier {
type Error = Infallible;
fn new(public_inputs: PoQVerificationInputsMinusSigningKey) -> Self {
Self(public_inputs, None)
}
fn start_epoch_transition(&mut self, new_pol_inputs: LeaderInputs) {
self.1 = Some(self.0.leader);
self.0.leader = new_pol_inputs;
}
fn complete_epoch_transition(&mut self) {
self.1 = None;
fn new(_public_inputs: PoQVerificationInputsMinusSigningKey) -> Self {
Self
}
fn verify_proof_of_quota(

View File

@ -1,15 +1,11 @@
use core::pin::Pin;
use core::{marker::PhantomData, pin::Pin};
use async_trait::async_trait;
use futures::stream::{self, Stream, StreamExt as _};
use lb_blend_message::crypto::{
key_ext::Ed25519SecretKeyExt as _, proofs::PoQVerificationInputsMinusSigningKey,
};
use lb_blend_proofs::{
quota::inputs::prove::{PublicInputs, public::LeaderInputs},
selection::VerifiedProofOfSelection,
};
use lb_cryptarchia_engine::Epoch;
use lb_blend_proofs::{quota::inputs::prove::PublicInputs, selection::VerifiedProofOfSelection};
use lb_groth16::fr_to_bytes;
use lb_key_management_system_keys::keys::UnsecuredEd25519Key;
use lb_log_targets::blend;
@ -29,28 +25,18 @@ const LOG_TARGET: &str = blend::scheduling::proofs::CORE;
/// Proof generator for core `PoQ` variants.
#[async_trait]
pub trait CoreProofsGenerator<PoQGenerator>: Sized {
/// Instantiate a new generator for the duration of a session.
/// Instantiate a new generator for the duration of an epoch.
fn new(settings: ProofsGeneratorSettings, proof_of_quota_generator: PoQGenerator) -> Self;
/// Notify the proof generator that a new epoch has started mid-session.
/// This will trigger proof re-generation due to the change in the set of
/// public inputs.
fn rotate_epoch(&mut self, new_epoch_public: LeaderInputs);
/// Request a new core proof from the prover. It returns `None` if the
/// maximum core quota has already been reached for this session.
/// maximum core quota has already been reached for this epoch.
async fn get_next_proof(&mut self) -> Option<BlendLayerProof>;
}
pub struct RealCoreProofsGenerator<PoQGenerator> {
remaining_quota: u64,
pub(super) settings: ProofsGeneratorSettings,
pub(super) proof_of_quota_generator: PoQGenerator,
proofs_stream: Pin<Box<dyn Stream<Item = BlendLayerProof> + Send + Sync>>,
}
impl<PoQGenerator> RealCoreProofsGenerator<PoQGenerator> {
pub(super) const fn current_epoch(&self) -> Epoch {
self.settings.epoch
}
_phantom: PhantomData<PoQGenerator>,
}
#[async_trait]
@ -62,35 +48,16 @@ where
Self {
proofs_stream: Box::pin(create_proof_stream(
settings.public_inputs,
proof_of_quota_generator.clone(),
proof_of_quota_generator,
0,
buffer_size(settings.encapsulation_layers.get() as usize),
)),
proof_of_quota_generator,
remaining_quota: settings.public_inputs.core.quota,
settings,
_phantom: PhantomData,
}
}
fn rotate_epoch(&mut self, new_epoch_public: LeaderInputs) {
tracing::info!(target: LOG_TARGET, "Rotating epoch...");
// On epoch rotation, we maintain the remaining session quota for core proofs
// and we only update the PoL part of the public inputs, before regenerating all
// proofs.
self.settings.public_inputs.leader = new_epoch_public;
let next_key_index = self
.settings
.public_inputs
.core
.quota
.checked_sub(self.remaining_quota)
.expect("Remaining quota should never be larger than total quota.");
// Compute new proofs with the updated settings.
self.generate_new_proofs_stream(next_key_index);
}
async fn get_next_proof(&mut self) -> Option<BlendLayerProof> {
let start = Instant::now();
self.remaining_quota = self.remaining_quota.checked_sub(1)?;
@ -100,27 +67,6 @@ where
}
}
impl<PoQGenerator> RealCoreProofsGenerator<PoQGenerator>
where
PoQGenerator: CoreProofOfQuotaGenerator + Clone + Send + Sync + 'static,
{
// This will kill the previous running task, if any, since we swap the receiver
// channel, hence the old task will fail to send new proofs and will abort on
// its own.
fn generate_new_proofs_stream(&mut self, starting_key_index: u64) {
if self.remaining_quota == 0 {
return;
}
self.proofs_stream = Box::pin(create_proof_stream(
self.settings.public_inputs,
self.proof_of_quota_generator.clone(),
starting_key_index,
buffer_size(self.settings.encapsulation_layers.get() as usize),
));
}
}
fn create_proof_stream<Generator>(
public_inputs: PoQVerificationInputsMinusSigningKey,
proof_of_quota_generator: Generator,
@ -156,7 +102,6 @@ where
signing_key: ephemeral_signing_key.public_key().into_inner(),
core: public_inputs.core,
leader: public_inputs.leader,
session: public_inputs.session,
},
key_index,
).await.expect("Core PoQ generation should not fail.");

View File

@ -7,7 +7,7 @@ use crate::message_blend::provers::{
core::{CoreProofsGenerator as _, RealCoreProofsGenerator},
test_utils::{
CorePoQGeneratorFromPrivateCoreQuotaInputs,
poq_public_inputs_from_session_public_inputs_and_signing_key, valid_proof_of_quota_inputs,
poq_public_inputs_from_epoch_public_inputs_and_signing_key, valid_proof_of_quota_inputs,
},
};
@ -32,12 +32,9 @@ async fn proof_generation() {
let verified_proof_of_quota = proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
.verify(&poq_public_inputs_from_epoch_public_inputs_and_signing_key(
(public_inputs, proof.ephemeral_signing_key.public_key()),
))
.unwrap();
proof
.proof_of_selection
@ -54,116 +51,3 @@ async fn proof_generation() {
// Next proof should be `None` since we ran out of core quota.
assert!(core_proofs_generator.get_next_proof().await.is_none());
}
#[test(tokio::test)]
async fn epoch_rotation() {
let core_quota = 10;
let (public_inputs, private_inputs) = valid_proof_of_quota_inputs(core_quota);
let mut core_proofs_generator = RealCoreProofsGenerator::new(
ProofsGeneratorSettings {
local_node_index: None,
membership_size: 1,
public_inputs,
encapsulation_layers: 1.try_into().unwrap(),
epoch: Epoch::new(0),
},
CorePoQGeneratorFromPrivateCoreQuotaInputs::new(private_inputs.clone()),
);
// Request all but the last proof, before rotating epoch (with the same public
// data because proofs use hard-coded fixtures).
for _ in 0..(core_quota - 1) {
let proof = core_proofs_generator.get_next_proof().await.unwrap();
let verified_proof_of_quota = proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
.unwrap();
proof
.proof_of_selection
.into_inner()
.verify(&VerifyInputs {
expected_node_index: 0,
key_nullifier: verified_proof_of_quota.key_nullifier(),
total_membership_size: 1,
})
.unwrap();
}
// Generate and verify last proof.
let proof = core_proofs_generator.get_next_proof().await.unwrap();
let verified_proof_of_quota = proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
.unwrap();
proof
.proof_of_selection
.into_inner()
.verify(&VerifyInputs {
expected_node_index: 0,
key_nullifier: verified_proof_of_quota.key_nullifier(),
total_membership_size: 1,
})
.unwrap();
// Next proof should be `None` since we ran out of core quota.
assert!(core_proofs_generator.get_next_proof().await.is_none());
// Rotating epoch and requesting a new proof will also return `None.`
core_proofs_generator.rotate_epoch(public_inputs.leader);
assert!(core_proofs_generator.get_next_proof().await.is_none());
}
/// Mid-quota epoch rotation: consume some proofs, rotate epoch, and verify the
/// remaining quota is preserved (i.e. exactly the unconsumed proofs remain).
#[test(tokio::test)]
async fn epoch_rotation_mid_quota_preserves_remaining() {
let core_quota = 10;
let proofs_before_rotation = 4;
let (public_inputs, private_inputs) = valid_proof_of_quota_inputs(core_quota);
let mut core_proofs_generator = RealCoreProofsGenerator::new(
ProofsGeneratorSettings {
local_node_index: None,
membership_size: 1,
public_inputs,
encapsulation_layers: 1.try_into().unwrap(),
epoch: Epoch::new(0),
},
CorePoQGeneratorFromPrivateCoreQuotaInputs::new(private_inputs),
);
// Consume some proofs before rotating.
for _ in 0..proofs_before_rotation {
assert!(core_proofs_generator.get_next_proof().await.is_some());
}
// Rotate epoch mid-quota.
core_proofs_generator.rotate_epoch(public_inputs.leader);
// Remaining quota should allow exactly (core_quota - proofs_before_rotation)
// more proofs.
let remaining = core_quota - proofs_before_rotation;
for i in 0..remaining {
assert!(
core_proofs_generator.get_next_proof().await.is_some(),
"Expected proof {i} of {remaining} remaining after rotation"
);
}
// Quota should now be exhausted.
assert!(core_proofs_generator.get_next_proof().await.is_none());
}

View File

@ -1,10 +1,6 @@
use core::cmp::Ordering;
use async_trait::async_trait;
use lb_blend_message::crypto::proofs::PoQVerificationInputsMinusSigningKey;
use lb_blend_proofs::quota::inputs::prove::{
private::ProofOfLeadershipQuotaInputs, public::LeaderInputs,
};
use lb_blend_proofs::quota::inputs::prove::private::ProofOfLeadershipQuotaInputs;
use lb_cryptarchia_engine::Epoch;
use lb_log_targets::blend;
@ -31,27 +27,21 @@ const LOG_TARGET: &str = blend::scheduling::proofs::CORE_AND_LEADER;
/// generation for those nodes with low stake.
#[async_trait]
pub trait CoreAndLeaderProofsGenerator<CorePoQGenerator>: Sized {
/// Instantiate a new generator for the duration of a session.
/// Instantiate a new generator for the duration of an epoch.
fn new(
settings: ProofsGeneratorSettings,
core_proof_of_quota_generator: CorePoQGenerator,
) -> Self;
/// Notify the proof generator that a new epoch has started mid-session.
/// This will trigger core proof re-generation due to the change in the set
/// of public inputs. Previously computed leader proofs are discarded and
/// re-computation is halted until the new epoch private info are provided.
fn rotate_epoch(&mut self, new_epoch_public: LeaderInputs, new_epoch: Epoch);
/// Notify the proof generator about winning `PoL` slots and their related
/// info. After this information is provided for a new epoch, the generator
/// will be able to provide leadership `PoQ` variants.
fn set_epoch_private(
&mut self,
new_epoch_private: ProofOfLeadershipQuotaInputs,
new_epoch_public: LeaderInputs,
new_epoch: Epoch,
reference_epoch: Epoch,
);
/// Request a new core proof from the prover. It returns `None` if the
/// maximum core quota has already been reached for this session.
/// maximum core quota has already been reached for this epoch.
async fn get_next_core_proof(&mut self) -> Option<BlendLayerProof>;
/// Request a new leadership proof from the prover. It returns `None` if no
/// secret `PoL` info has been provided for the current epoch.
@ -92,76 +82,35 @@ where
}
}
// Changes epoch-related info for the core generator, and stops the old leader
// generator if it's still on the previous epoch. If not, `rotate_epoch` is
// effectively a no-op.
#[expect(
clippy::cognitive_complexity,
reason = "TODO: address this in a dedicated refactor"
)]
fn rotate_epoch(&mut self, new_epoch_public: LeaderInputs, new_epoch: Epoch) {
match self.core_proofs_generator.current_epoch().cmp(&new_epoch) {
Ordering::Less => {
tracing::info!(target: LOG_TARGET, "Rotating epoch...");
self.core_proofs_generator.rotate_epoch(new_epoch_public);
}
Ordering::Equal => {
tracing::debug!(target: LOG_TARGET, "Core proofs generator already on the new epoch, ignoring the new public epoch info received.");
}
Ordering::Greater => {
panic!(
"Public epoch info should never provide an epoch smaller than what the core proofs generator returns as current, as the public epoch info should never lag behind."
);
}
}
let Some(leader_proofs_generator) = self.leader_proofs_generator.take() else {
return;
};
match leader_proofs_generator.current_epoch().cmp(&new_epoch) {
Ordering::Less => {
tracing::debug!(target: LOG_TARGET, "Stopping old epoch leadership proofs generator until new secret PoL info is provided.");
}
Ordering::Equal => {
tracing::debug!(target: LOG_TARGET, "Leadership proofs generator already on the new epoch, ignoring the new public epoch info received.");
self.leader_proofs_generator = Some(leader_proofs_generator);
}
Ordering::Greater => {
panic!(
"Secret PoL info for new epoch should never provide an epoch greater than what the public epoch info returns, as the two should always be yielded together, or at most the secret PoL info would lag behind if the node has no or close to no stake."
);
}
}
}
// Creates a new leader proofs generator with the provided public+private
// secret.
fn set_epoch_private(
&mut self,
new_epoch_private: ProofOfLeadershipQuotaInputs,
new_epoch_public: LeaderInputs,
new_epoch: Epoch,
reference_epoch: Epoch,
) {
// Update core proof generation and optionally deactivates leadership proof
// generation, which is then re-created below.
self.rotate_epoch(new_epoch_public, new_epoch);
let current_session_local_node_index = self.core_proofs_generator.settings.local_node_index;
let current_session_membership_size = self.core_proofs_generator.settings.membership_size;
let current_session_core_public_inputs =
// TODO: Change trait API to avoid runtime panics.
let (current_generator_epoch, current_leader_inputs) = (
self.core_proofs_generator.settings.epoch,
self.core_proofs_generator.settings.public_inputs.leader,
);
assert!(
current_generator_epoch == reference_epoch,
"set_epoch_private should be called with a reference epoch matching the current core proofs generator's epoch."
);
let current_epoch_local_node_index = self.core_proofs_generator.settings.local_node_index;
let current_epoch_membership_size = self.core_proofs_generator.settings.membership_size;
let current_epoch_core_public_inputs =
self.core_proofs_generator.settings.public_inputs.core;
let current_session = self.core_proofs_generator.settings.public_inputs.session;
self.leader_proofs_generator = Some(RealLeaderProofsGenerator::new(
ProofsGeneratorSettings {
epoch: new_epoch,
local_node_index: current_session_local_node_index,
membership_size: current_session_membership_size,
epoch: reference_epoch,
local_node_index: current_epoch_local_node_index,
membership_size: current_epoch_membership_size,
public_inputs: PoQVerificationInputsMinusSigningKey {
core: current_session_core_public_inputs,
session: current_session,
leader: new_epoch_public,
core: current_epoch_core_public_inputs,
leader: current_leader_inputs,
},
encapsulation_layers: self.core_proofs_generator.settings.encapsulation_layers,
},
@ -174,7 +123,6 @@ where
tracing::trace!(
target: LOG_TARGET,
epoch = ?self.core_proofs_generator.settings.epoch,
session = self.core_proofs_generator.settings.public_inputs.session,
quota = self.core_proofs_generator.settings.public_inputs.core.quota,
membership_size = self.core_proofs_generator.settings.membership_size,
local_node_index = ?self.core_proofs_generator.settings.local_node_index,
@ -193,7 +141,6 @@ where
tracing::trace!(
target: LOG_TARGET,
epoch = ?leader_proofs_generator.settings.epoch,
session = leader_proofs_generator.settings.public_inputs.session,
quota = leader_proofs_generator.settings.public_inputs.core.quota,
membership_size = leader_proofs_generator.settings.membership_size,
local_node_index = ?leader_proofs_generator.settings.local_node_index,

View File

@ -7,7 +7,7 @@ use crate::message_blend::provers::{
core_and_leader::{CoreAndLeaderProofsGenerator as _, RealCoreAndLeaderProofsGenerator},
test_utils::{
CorePoQGeneratorFromPrivateCoreQuotaInputs,
poq_public_inputs_from_session_public_inputs_and_signing_key, valid_proof_of_leader_inputs,
poq_public_inputs_from_epoch_public_inputs_and_signing_key, valid_proof_of_leader_inputs,
valid_proof_of_quota_inputs,
},
};
@ -36,12 +36,9 @@ async fn proof_generation() {
let verified_proof_of_quota = proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
core_public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
.verify(&poq_public_inputs_from_epoch_public_inputs_and_signing_key(
(core_public_inputs, proof.ephemeral_signing_key.public_key()),
))
.unwrap();
proof
.proof_of_selection
@ -76,11 +73,7 @@ async fn proof_generation() {
encapsulation_layers: 1.try_into().unwrap(),
epoch: Epoch::new(0),
});
core_and_leader_proofs_generator.set_epoch_private(
leadership_private_inputs,
leadership_public_inputs.leader,
Epoch::new(1),
);
core_and_leader_proofs_generator.set_epoch_private(leadership_private_inputs, Epoch::new(0));
for _ in 0..leadership_quota {
let proof = core_and_leader_proofs_generator
@ -90,12 +83,12 @@ async fn proof_generation() {
let verified_proof_of_quota = proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
.verify(&poq_public_inputs_from_epoch_public_inputs_and_signing_key(
(
leadership_public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
),
))
.unwrap();
proof
.proof_of_selection
@ -109,96 +102,6 @@ async fn proof_generation() {
}
}
#[test(tokio::test)]
async fn epoch_rotation() {
let core_quota = 10;
let (public_inputs, private_inputs) = valid_proof_of_quota_inputs(core_quota);
let mut core_and_leader_proofs_generator = RealCoreAndLeaderProofsGenerator::new(
ProofsGeneratorSettings {
local_node_index: None,
membership_size: 1,
public_inputs,
encapsulation_layers: 1.try_into().unwrap(),
epoch: Epoch::new(1),
},
CorePoQGeneratorFromPrivateCoreQuotaInputs::new(private_inputs),
);
// Request all but the last proof, before rotating epoch (with the same public
// data because proofs use hard-coded fixtures).
for _ in 0..(core_quota - 1) {
let proof = core_and_leader_proofs_generator
.get_next_core_proof()
.await
.unwrap();
let verified_proof_of_quota = proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
.unwrap();
proof
.proof_of_selection
.into_inner()
.verify(&VerifyInputs {
expected_node_index: 0,
key_nullifier: verified_proof_of_quota.key_nullifier(),
total_membership_size: 1,
})
.unwrap();
}
// Verify any traces of leader proofs have been removed.
assert!(
core_and_leader_proofs_generator
.leader_proofs_generator
.is_none()
);
assert!(
core_and_leader_proofs_generator
.get_next_leader_proof()
.await
.is_none()
);
// Generate and verify last proof.
let proof = core_and_leader_proofs_generator
.get_next_core_proof()
.await
.unwrap();
let verified_proof_of_quota = proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
.unwrap();
proof
.proof_of_selection
.into_inner()
.verify(&VerifyInputs {
expected_node_index: 0,
key_nullifier: verified_proof_of_quota.key_nullifier(),
total_membership_size: 1,
})
.unwrap();
// Next proof should be `None` since we ran out of core quota.
assert!(
core_and_leader_proofs_generator
.get_next_core_proof()
.await
.is_none()
);
}
#[test(tokio::test)]
async fn epoch_private_info() {
let core_quota = 10;
@ -211,18 +114,24 @@ async fn epoch_private_info() {
ProofsGeneratorSettings {
local_node_index: None,
membership_size: 1,
public_inputs: leadership_public_inputs,
public_inputs: core_public_inputs,
encapsulation_layers: 1.try_into().unwrap(),
epoch: Epoch::new(0),
},
CorePoQGeneratorFromPrivateCoreQuotaInputs::new(core_private_inputs.clone()),
);
core_and_leader_proofs_generator.set_epoch_private(
leadership_private_inputs,
leadership_public_inputs.leader,
Epoch::new(1),
);
// Switch to leadership inputs before wiring leader private epoch info, because
// we use fixtures that yield different public inputs.
core_and_leader_proofs_generator.override_settings(ProofsGeneratorSettings {
local_node_index: None,
membership_size: 1,
public_inputs: leadership_public_inputs,
encapsulation_layers: 1.try_into().unwrap(),
epoch: Epoch::new(0),
});
core_and_leader_proofs_generator.set_epoch_private(leadership_private_inputs, Epoch::new(0));
// Leadership proof should be generated and verified correctly.
let proof = core_and_leader_proofs_generator
@ -232,12 +141,12 @@ async fn epoch_private_info() {
let verified_proof_of_quota = proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
.verify(&poq_public_inputs_from_epoch_public_inputs_and_signing_key(
(
leadership_public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
),
))
.unwrap();
proof
.proof_of_selection
@ -257,12 +166,12 @@ async fn epoch_private_info() {
let verified_proof_of_quota = proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
.verify(&poq_public_inputs_from_epoch_public_inputs_and_signing_key(
(
leadership_public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
),
))
.unwrap();
proof
.proof_of_selection
@ -284,7 +193,6 @@ async fn epoch_private_info() {
encapsulation_layers: 1.try_into().unwrap(),
epoch: Epoch::new(0),
});
core_and_leader_proofs_generator.rotate_epoch(core_public_inputs.leader, Epoch::new(1));
// We test that core proof generation still works fine
let proof = core_and_leader_proofs_generator
@ -294,12 +202,9 @@ async fn epoch_private_info() {
let verified_proof_of_quota = proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
core_public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
.verify(&poq_public_inputs_from_epoch_public_inputs_and_signing_key(
(core_public_inputs, proof.ephemeral_signing_key.public_key()),
))
.unwrap();
proof
.proof_of_selection
@ -312,138 +217,3 @@ async fn epoch_private_info() {
})
.unwrap();
}
/// `rotate_epoch` with the same epoch as the current one should be a no-op:
/// the leader generator (if present) should be preserved.
#[test(tokio::test)]
async fn rotate_epoch_equal_is_noop() {
let core_quota = 5;
let leadership_quota = 10;
let (_, core_private_inputs) = valid_proof_of_quota_inputs(core_quota);
let (leadership_public_inputs, leadership_private_inputs) =
valid_proof_of_leader_inputs(leadership_quota);
let mut generator = RealCoreAndLeaderProofsGenerator::new(
ProofsGeneratorSettings {
local_node_index: None,
membership_size: 1,
public_inputs: leadership_public_inputs,
encapsulation_layers: 1.try_into().unwrap(),
epoch: Epoch::new(1),
},
CorePoQGeneratorFromPrivateCoreQuotaInputs::new(core_private_inputs),
);
// Provide private info so a leader generator exists.
generator.set_epoch_private(
leadership_private_inputs,
leadership_public_inputs.leader,
Epoch::new(1),
);
assert!(generator.leader_proofs_generator.is_some());
// Rotating with the same epoch should be a no-op - leader generator
// preserved.
generator.rotate_epoch(leadership_public_inputs.leader, Epoch::new(1));
assert!(
generator.leader_proofs_generator.is_some(),
"Leader generator should be preserved when rotating to the same epoch"
);
// Leader proofs should still work.
let proof = generator.get_next_leader_proof().await;
assert!(proof.is_some());
}
/// `rotate_epoch` drops the leader generator. `set_epoch_private` then
/// recreates it for the new epoch.
#[test(tokio::test)]
async fn rotate_epoch_drops_leader_then_set_epoch_private_recreates() {
let core_quota = 5;
let leadership_quota = 10;
let (_, core_private_inputs) = valid_proof_of_quota_inputs(core_quota);
let (leadership_public_inputs, leadership_private_inputs) =
valid_proof_of_leader_inputs(leadership_quota);
let mut generator = RealCoreAndLeaderProofsGenerator::new(
ProofsGeneratorSettings {
local_node_index: None,
membership_size: 1,
public_inputs: leadership_public_inputs,
encapsulation_layers: 1.try_into().unwrap(),
epoch: Epoch::new(0),
},
CorePoQGeneratorFromPrivateCoreQuotaInputs::new(core_private_inputs),
);
// Provide private info for epoch 0.
generator.set_epoch_private(
leadership_private_inputs.clone(),
leadership_public_inputs.leader,
Epoch::new(0),
);
assert!(generator.leader_proofs_generator.is_some());
// Rotate to epoch 1 - leader generator should be dropped because its
// epoch is behind.
generator.rotate_epoch(leadership_public_inputs.leader, Epoch::new(1));
assert!(
generator.leader_proofs_generator.is_none(),
"Leader generator should be dropped after rotating to a newer epoch"
);
assert!(generator.get_next_leader_proof().await.is_none());
// Provide private info for epoch 1 - leader generator should be
// recreated.
generator.set_epoch_private(
leadership_private_inputs,
leadership_public_inputs.leader,
Epoch::new(1),
);
assert!(
generator.leader_proofs_generator.is_some(),
"Leader generator should be recreated after set_epoch_private"
);
let proof = generator.get_next_leader_proof().await;
assert!(proof.is_some());
}
/// Two consecutive `rotate_epoch` calls without `set_epoch_private` in between:
/// the first drops the leader generator, the second is a no-op on the leader
/// side since there is nothing to drop.
#[test(tokio::test)]
async fn double_rotate_epoch_without_set_epoch_private() {
let core_quota = 10;
let (public_inputs, private_inputs) = valid_proof_of_quota_inputs(core_quota);
let mut generator = RealCoreAndLeaderProofsGenerator::new(
ProofsGeneratorSettings {
local_node_index: None,
membership_size: 1,
public_inputs,
encapsulation_layers: 1.try_into().unwrap(),
epoch: Epoch::new(0),
},
CorePoQGeneratorFromPrivateCoreQuotaInputs::new(private_inputs),
);
// Consume some core proofs.
for _ in 0u8..3 {
assert!(generator.get_next_core_proof().await.is_some());
}
// First rotation: epoch 0 -> 1.
generator.rotate_epoch(public_inputs.leader, Epoch::new(1));
assert!(generator.leader_proofs_generator.is_none());
// Second rotation: epoch 1 -> 2.
generator.rotate_epoch(public_inputs.leader, Epoch::new(2));
assert!(generator.leader_proofs_generator.is_none());
// Core proofs should still work - remaining quota preserved across rotations.
// We consumed 3 out of 10, so 7 remain.
for _ in 0u8..7 {
assert!(generator.get_next_core_proof().await.is_some());
}
assert!(generator.get_next_core_proof().await.is_none());
}

View File

@ -8,10 +8,7 @@ use lb_blend_message::crypto::{
use lb_blend_proofs::{
quota::{
VerifiedProofOfQuota,
inputs::prove::{
PrivateInputs, PublicInputs, private::ProofOfLeadershipQuotaInputs,
public::LeaderInputs,
},
inputs::prove::{PrivateInputs, PublicInputs, private::ProofOfLeadershipQuotaInputs},
},
selection::VerifiedProofOfSelection,
};
@ -40,24 +37,22 @@ pub trait LeaderProofsGenerator: Sized {
/// `PoL` values.
fn new(settings: ProofsGeneratorSettings, private_inputs: ProofOfLeadershipQuotaInputs)
-> Self;
/// Signal an epoch transition in the middle of the current session, with
/// new public and secret inputs.
fn rotate_epoch(
&mut self,
new_epoch_public: LeaderInputs,
new_private_inputs: ProofOfLeadershipQuotaInputs,
new_epoch: Epoch,
);
/// Get the next leadership proof.
async fn get_next_proof(&mut self) -> BlendLayerProof;
}
pub struct RealLeaderProofsGenerator {
pub(super) settings: ProofsGeneratorSettings,
private_inputs: ProofOfLeadershipQuotaInputs,
proofs_stream: Pin<Box<dyn Stream<Item = BlendLayerProof> + Send + Sync>>,
}
impl RealLeaderProofsGenerator {
#[must_use]
pub const fn epoch(&self) -> Epoch {
self.settings.epoch
}
}
#[async_trait]
impl LeaderProofsGenerator for RealLeaderProofsGenerator {
fn new(
@ -66,7 +61,6 @@ impl LeaderProofsGenerator for RealLeaderProofsGenerator {
) -> Self {
Self {
settings,
private_inputs: private_inputs.clone(),
proofs_stream: Box::pin(create_proof_stream(
settings.public_inputs,
private_inputs,
@ -75,24 +69,6 @@ impl LeaderProofsGenerator for RealLeaderProofsGenerator {
}
}
fn rotate_epoch(
&mut self,
new_epoch_public: LeaderInputs,
new_private: ProofOfLeadershipQuotaInputs,
new_epoch: Epoch,
) {
tracing::info!(target: LOG_TARGET, "Rotating epoch...");
// On epoch rotation, we maintain the current session info and only change the
// PoL relevant parts.
self.settings.public_inputs.leader = new_epoch_public;
self.settings.epoch = new_epoch;
self.private_inputs = new_private;
// Compute new proofs with the updated settings.
self.generate_new_proofs_stream();
}
async fn get_next_proof(&mut self) -> BlendLayerProof {
let start = Instant::now();
let proof = self
@ -105,20 +81,6 @@ impl LeaderProofsGenerator for RealLeaderProofsGenerator {
}
}
impl RealLeaderProofsGenerator {
fn generate_new_proofs_stream(&mut self) {
self.proofs_stream = Box::pin(create_proof_stream(
self.settings.public_inputs,
self.private_inputs.clone(),
buffer_size(self.settings.public_inputs.leader.message_quota as usize),
));
}
pub(super) const fn current_epoch(&self) -> Epoch {
self.settings.epoch
}
}
fn create_proof_stream(
public_inputs: PoQVerificationInputsMinusSigningKey,
private_inputs: ProofOfLeadershipQuotaInputs,
@ -131,7 +93,7 @@ fn create_proof_stream(
stream::iter(0u64..)
.map(move |current_index| {
// This represents the total number of encapsulations sent out for each message.
// E.g., for a session with data message replication factor of `1`, we get
// E.g., for an epoch with data message replication factor of `1`, we get
// indices `0` to `2` that belong to the first copy encapsulation, and indices
// `3` to `5` that belong to the second copy encapsulation.
// In the end, because the expected maximum message quota is `6` (if we take `3`
@ -156,7 +118,6 @@ fn create_proof_stream(
signing_key: ephemeral_signing_key.public_key().into_inner(),
core: public_inputs.core,
leader: public_inputs.leader,
session: public_inputs.session,
},
PrivateInputs::new_proof_of_leadership_quota_inputs(
message_release_index,

View File

@ -9,7 +9,7 @@ use crate::message_blend::provers::{
ProofsGeneratorSettings,
leader::{LeaderProofsGenerator as _, RealLeaderProofsGenerator},
test_utils::{
poq_public_inputs_from_session_public_inputs_and_signing_key, valid_proof_of_leader_inputs,
poq_public_inputs_from_epoch_public_inputs_and_signing_key, valid_proof_of_leader_inputs,
},
};
@ -34,12 +34,9 @@ async fn proof_generation() {
let verified_proof_of_quota = proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
.verify(&poq_public_inputs_from_epoch_public_inputs_and_signing_key(
(public_inputs, proof.ephemeral_signing_key.public_key()),
))
.unwrap();
proof
.proof_of_selection
@ -62,131 +59,3 @@ async fn proof_generation() {
.await
.unwrap();
}
#[test(tokio::test)]
async fn epoch_rotation() {
let leadership_quota = 15;
let (public_inputs, private_inputs) = valid_proof_of_leader_inputs(leadership_quota);
let mut leader_proofs_generator = RealLeaderProofsGenerator::new(
ProofsGeneratorSettings {
local_node_index: None,
membership_size: 1,
public_inputs,
encapsulation_layers: 1.try_into().unwrap(),
epoch: Epoch::new(0),
},
private_inputs,
);
let proof = leader_proofs_generator.get_next_proof().await;
let verified_proof_of_quota = proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
.unwrap();
proof
.proof_of_selection
.into_inner()
.verify(&VerifyInputs {
expected_node_index: 0,
key_nullifier: verified_proof_of_quota.key_nullifier(),
total_membership_size: 1,
})
.unwrap();
// Generate and verify new proof.
let proof = leader_proofs_generator.get_next_proof().await;
let verified_proof_of_quota = proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
.unwrap();
proof
.proof_of_selection
.into_inner()
.verify(&VerifyInputs {
expected_node_index: 0,
key_nullifier: verified_proof_of_quota.key_nullifier(),
total_membership_size: 1,
})
.unwrap();
}
/// Verify that calling `rotate_epoch` actually updates the epoch and
/// regenerates proofs, unlike the above test which only generates proofs
/// without rotating.
#[test(tokio::test)]
async fn rotate_epoch_updates_epoch_and_regenerates_proofs() {
let leadership_quota = 15;
let (public_inputs, private_inputs) = valid_proof_of_leader_inputs(leadership_quota);
let mut leader_proofs_generator = RealLeaderProofsGenerator::new(
ProofsGeneratorSettings {
local_node_index: None,
membership_size: 1,
public_inputs,
encapsulation_layers: 1.try_into().unwrap(),
epoch: Epoch::new(0),
},
private_inputs.clone(),
);
// Generate one proof on epoch 0.
drop(leader_proofs_generator.get_next_proof().await);
assert_eq!(leader_proofs_generator.current_epoch(), Epoch::new(0));
// Rotate to epoch 1 - this should update epoch and regenerate the proofs
// stream.
leader_proofs_generator.rotate_epoch(
public_inputs.leader,
private_inputs.clone(),
Epoch::new(1),
);
assert_eq!(leader_proofs_generator.current_epoch(), Epoch::new(1));
// Proofs should still be generated successfully after rotation.
let proof = leader_proofs_generator.get_next_proof().await;
proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
.unwrap();
// Rotate to epoch 2.
leader_proofs_generator.rotate_epoch(public_inputs.leader, private_inputs, Epoch::new(2));
assert_eq!(leader_proofs_generator.current_epoch(), Epoch::new(2));
// Proofs should still verify after a second rotation.
let proof = timeout(
Duration::from_secs(20),
leader_proofs_generator.get_next_proof(),
)
.await
.unwrap();
proof
.proof_of_quota
.into_inner()
.verify(
&poq_public_inputs_from_session_public_inputs_and_signing_key((
public_inputs,
proof.ephemeral_signing_key.public_key(),
)),
)
.unwrap();
}

View File

@ -1,9 +1,6 @@
use ::core::num::NonZeroU64;
use lb_blend_message::crypto::proofs::PoQVerificationInputsMinusSigningKey;
use lb_blend_proofs::{
quota::{VerifiedProofOfQuota, inputs::prove::public::CoreInputs},
selection::VerifiedProofOfSelection,
};
use lb_blend_proofs::{quota::VerifiedProofOfQuota, selection::VerifiedProofOfSelection};
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::UnsecuredEd25519Key;
@ -32,11 +29,3 @@ pub struct ProofsGeneratorSettings {
pub encapsulation_layers: NonZeroU64,
pub epoch: Epoch,
}
#[derive(Debug, Clone, Copy)]
pub struct NewCoreSessionPublicInputs {
pub session: u64,
pub local_node_index: usize,
pub membership_size: usize,
pub inputs: CoreInputs,
}

View File

@ -13,47 +13,30 @@ use lb_key_management_system_keys::keys::{ED25519_PUBLIC_KEY_SIZE, Ed25519Public
use crate::message_blend::CoreProofOfQuotaGenerator;
pub const fn poq_public_inputs_from_session_public_inputs_and_signing_key(
(
PoQVerificationInputsMinusSigningKey {
core,
leader,
session,
},
signing_key,
): (PoQVerificationInputsMinusSigningKey, Ed25519PublicKey),
pub const fn poq_public_inputs_from_epoch_public_inputs_and_signing_key(
(PoQVerificationInputsMinusSigningKey { core, leader }, signing_key): (
PoQVerificationInputsMinusSigningKey,
Ed25519PublicKey,
),
) -> PoQPublicInputs {
PoQPublicInputs {
signing_key: signing_key.into_inner(),
core,
leader,
session,
}
}
pub fn valid_proof_of_quota_inputs(
core_quota: u64,
) -> (PoQVerificationInputsMinusSigningKey, ProofOfCoreQuotaInputs) {
let (
PoQPublicInputs {
core,
leader,
session,
..
},
private_inputs,
) = valid_proof_of_core_quota_inputs(
let (PoQPublicInputs { core, leader, .. }, private_inputs) = valid_proof_of_core_quota_inputs(
Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE])
.unwrap()
.into_inner(),
core_quota,
);
(
PoQVerificationInputsMinusSigningKey {
core,
leader,
session,
},
PoQVerificationInputsMinusSigningKey { core, leader },
private_inputs,
)
}
@ -64,26 +47,15 @@ pub fn valid_proof_of_leader_inputs(
PoQVerificationInputsMinusSigningKey,
ProofOfLeadershipQuotaInputs,
) {
let (
PoQPublicInputs {
core,
leader,
session,
..
},
private_inputs,
) = valid_proof_of_leadership_quota_inputs(
Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE])
.unwrap()
.into_inner(),
leader_quota,
);
let (PoQPublicInputs { core, leader, .. }, private_inputs) =
valid_proof_of_leadership_quota_inputs(
Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE])
.unwrap()
.into_inner(),
leader_quota,
);
(
PoQVerificationInputsMinusSigningKey {
core,
leader,
session,
},
PoQVerificationInputsMinusSigningKey { core, leader },
private_inputs,
)
}

View File

@ -0,0 +1,12 @@
use lb_cryptarchia_engine::Epoch;
/// Information regarding an epoch that the message scheduler needs to
/// initialize its sub-streams.
#[derive(Debug, Clone, Copy)]
pub struct EpochInfo {
/// The initial quota that the cover message scheduler will use to
/// pre-compute the rounds to yield a new message.
pub core_quota: u64,
/// The identifier for the current epoch.
pub epoch: Epoch,
}

View File

@ -16,16 +16,16 @@ use tokio_stream::wrappers::IntervalStream;
use tracing::trace;
use crate::{
cover_traffic::SessionCoverTraffic,
cover_traffic::EpochCoverTraffic,
message_scheduler::{
epoch_info::EpochInfo,
round_info::{RoundClock, RoundInfo, RoundReleaseType},
session_info::SessionInfo,
},
release_delayer::SessionProcessedMessageDelayer,
release_delayer::EpochProcessedMessageDelayer,
};
pub mod epoch_info;
pub mod round_info;
pub mod session_info;
#[cfg(test)]
mod tests;
@ -39,28 +39,28 @@ pub trait ProcessedMessageScheduler<ProcessedMessage> {
fn schedule_processed_message(&mut self, message: ProcessedMessage);
}
/// Message scheduler that is valid only for a specific session.
pub struct SessionMessageScheduler<Rng, ProcessedMessage, DataMessage> {
/// Message scheduler that is valid only for a specific epoch.
pub struct EpochMessageScheduler<Rng, ProcessedMessage, DataMessage> {
/// The module responsible for randomly generated cover messages, given the
/// allowed session quota and accounting for data messages generated within
/// the session.
cover_traffic: SessionCoverTraffic<Rng, RoundClock>,
/// allowed epoch quota and accounting for data messages generated within
/// the epoch.
cover_traffic: EpochCoverTraffic<Rng, RoundClock>,
/// The module responsible for delaying the release of processed messages
/// that have not been fully decapsulated.
release_delayer: SessionProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage>,
release_delayer: EpochProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage>,
/// The queue of data messages that are stored in between rounds.
data_messages: Vec<DataMessage>,
/// The multi-consumer stream forked on each sub-stream.
round_clock: RoundClock,
}
impl<Rng, ProcessedMessage, DataMessage> SessionMessageScheduler<Rng, ProcessedMessage, DataMessage>
impl<Rng, ProcessedMessage, DataMessage> EpochMessageScheduler<Rng, ProcessedMessage, DataMessage>
where
Rng: RngCore + Clone + Unpin,
ProcessedMessage: Debug + Unpin,
DataMessage: Debug + Unpin,
{
pub fn new(session_info: SessionInfo, rng: Rng, settings: Settings) -> Self {
pub fn new(epoch_info: EpochInfo, rng: Rng, settings: Settings) -> Self {
let interval = {
let mut interval = interval(settings.round_duration);
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
@ -74,19 +74,17 @@ where
)
.fork();
let cover_traffic = SessionCoverTraffic::new(
let cover_traffic = EpochCoverTraffic::new(
crate::cover_traffic::Settings {
additional_safety_intervals: settings.additional_safety_intervals,
expected_intervals_per_session: settings.expected_intervals_per_session,
rounds_per_interval: settings.rounds_per_interval,
message_count: session_info
rounds_per_epoch: settings.rounds_per_epoch,
message_count: epoch_info
.core_quota
.div_ceil(settings.num_blend_layers.into()),
},
rng.clone(),
Box::new(round_clock.clone()) as RoundClock,
);
let release_delayer = SessionProcessedMessageDelayer::new(
let release_delayer = EpochProcessedMessageDelayer::new(
crate::release_delayer::Settings {
maximum_release_delay_in_rounds: settings.maximum_release_delay_in_rounds,
},
@ -102,27 +100,23 @@ where
}
}
pub fn consume(self) -> OldSessionMessageScheduler<Rng, ProcessedMessage> {
OldSessionMessageScheduler(self.release_delayer)
pub fn consume(self) -> OldEpochMessageScheduler<Rng, ProcessedMessage> {
OldEpochMessageScheduler(self.release_delayer)
}
pub fn rotate_session(
pub fn rotate_epoch(
self,
new_session_info: SessionInfo,
new_epoch_info: EpochInfo,
settings: Settings,
) -> (Self, OldSessionMessageScheduler<Rng, ProcessedMessage>) {
) -> (Self, OldEpochMessageScheduler<Rng, ProcessedMessage>) {
(
Self::new(
new_session_info,
self.release_delayer.rng().clone(),
settings,
),
OldSessionMessageScheduler(self.release_delayer),
Self::new(new_epoch_info, self.release_delayer.rng().clone(), settings),
OldEpochMessageScheduler(self.release_delayer),
)
}
/// Notify the cover message submodule that a new data message has been
/// generated in this session, which will reduce the number of cover
/// generated in this epoch, which will reduce the number of cover
/// messages generated going forward.
pub fn queue_data_message(&mut self, message: DataMessage) {
self.data_messages.push(message);
@ -130,13 +124,11 @@ where
}
}
impl<Rng, ProcessedMessage, DataMessage>
SessionMessageScheduler<Rng, ProcessedMessage, DataMessage>
{
impl<Rng, ProcessedMessage, DataMessage> EpochMessageScheduler<Rng, ProcessedMessage, DataMessage> {
#[cfg(test)]
pub fn with_test_values(
cover_traffic: SessionCoverTraffic<Rng, RoundClock>,
release_delayer: SessionProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage>,
cover_traffic: EpochCoverTraffic<Rng, RoundClock>,
release_delayer: EpochProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage>,
round_clock: RoundClock,
data_messages: Vec<DataMessage>,
) -> Self {
@ -151,13 +143,13 @@ impl<Rng, ProcessedMessage, DataMessage>
#[cfg(any(test, feature = "unsafe-test-functions"))]
pub fn release_delayer(
&self,
) -> &SessionProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage> {
) -> &EpochProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage> {
&self.release_delayer
}
}
impl<Rng, ProcessedMessage, DataMessage> ProcessedMessageScheduler<ProcessedMessage>
for SessionMessageScheduler<Rng, ProcessedMessage, DataMessage>
for EpochMessageScheduler<Rng, ProcessedMessage, DataMessage>
{
fn schedule_processed_message(&mut self, message: ProcessedMessage) {
self.release_delayer.schedule_message(message);
@ -165,7 +157,7 @@ impl<Rng, ProcessedMessage, DataMessage> ProcessedMessageScheduler<ProcessedMess
}
impl<Rng, ProcessedMessage, DataMessage> Stream
for SessionMessageScheduler<Rng, ProcessedMessage, DataMessage>
for EpochMessageScheduler<Rng, ProcessedMessage, DataMessage>
where
Rng: rand::Rng + Clone + Unpin,
ProcessedMessage: Debug + Unpin,
@ -246,11 +238,9 @@ where
#[derive(Debug, Clone, Copy)]
pub struct Settings {
pub additional_safety_intervals: u64,
pub expected_intervals_per_session: NonZeroU64,
pub maximum_release_delay_in_rounds: NonZeroU64,
pub round_duration: Duration,
pub rounds_per_interval: NonZeroU64,
pub rounds_per_epoch: NonZeroU64,
pub num_blend_layers: NonZeroU64,
}
@ -258,34 +248,32 @@ pub struct Settings {
impl Default for Settings {
fn default() -> Self {
Self {
additional_safety_intervals: 0,
expected_intervals_per_session: NonZeroU64::try_from(1).unwrap(),
maximum_release_delay_in_rounds: NonZeroU64::try_from(1).unwrap(),
round_duration: Duration::from_secs(1),
rounds_per_interval: NonZeroU64::try_from(1).unwrap(),
rounds_per_epoch: NonZeroU64::try_from(1).unwrap(),
num_blend_layers: NonZeroU64::try_from(1).unwrap(),
}
}
}
/// Message scheduler that is only for an old session during session transition.
/// Message scheduler that is only for an old epoch during epoch transition.
///
/// Unlike [`SessionMessageScheduler`], this supports only scheduling processed
/// Unlike [`EpochMessageScheduler`], this supports only scheduling processed
/// messages. Data messages cannot be scheduled, and it does not generate cover
/// messages.
pub struct OldSessionMessageScheduler<Rng, ProcessedMessage>(
SessionProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage>,
pub struct OldEpochMessageScheduler<Rng, ProcessedMessage>(
EpochProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage>,
);
impl<Rng, ProcessedMessage> ProcessedMessageScheduler<ProcessedMessage>
for OldSessionMessageScheduler<Rng, ProcessedMessage>
for OldEpochMessageScheduler<Rng, ProcessedMessage>
{
fn schedule_processed_message(&mut self, message: ProcessedMessage) {
self.0.schedule_message(message);
}
}
impl<Rng, ProcessedMessage> Stream for OldSessionMessageScheduler<Rng, ProcessedMessage>
impl<Rng, ProcessedMessage> Stream for OldEpochMessageScheduler<Rng, ProcessedMessage>
where
Rng: rand::Rng + Unpin,
ProcessedMessage: Unpin,
@ -297,11 +285,11 @@ where
}
}
impl<Rng, ProcessedMessage> OldSessionMessageScheduler<Rng, ProcessedMessage> {
impl<Rng, ProcessedMessage> OldEpochMessageScheduler<Rng, ProcessedMessage> {
#[cfg(any(test, feature = "unsafe-test-functions"))]
pub fn release_delayer(
&self,
) -> &SessionProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage> {
) -> &EpochProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage> {
&self.0
}
}

View File

@ -1,40 +0,0 @@
use core::{
fmt,
fmt::{Display, Formatter},
};
use futures::Stream;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Session(u128);
impl From<u128> for Session {
fn from(value: u128) -> Self {
Self(value)
}
}
impl From<Session> for u128 {
fn from(session: Session) -> Self {
session.0
}
}
impl Display for Session {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
/// Information regarding a session that the message scheduler needs to
/// initialize its sub-streams.
#[derive(Debug, Clone, Copy)]
pub struct SessionInfo {
/// The initial quota that the cover message scheduler will use to
/// pre-compute the rounds to yield a new message.
pub core_quota: u64,
/// The identifier for the current session.
pub session_number: Session,
}
pub type SessionClock = Box<dyn Stream<Item = SessionInfo> + Unpin>;

View File

@ -9,23 +9,23 @@ use rand::SeedableRng as _;
use tokio_stream::iter;
use crate::{
cover_traffic::SessionCoverTraffic,
cover_traffic::EpochCoverTraffic,
message_scheduler::{
SessionMessageScheduler,
EpochMessageScheduler,
round_info::{Round, RoundInfo, RoundReleaseType},
},
release_delayer::SessionProcessedMessageDelayer,
release_delayer::EpochProcessedMessageDelayer,
};
#[tokio::test]
async fn no_substream_ready_and_no_data_messages() {
let rng = BlakeRng::from_entropy();
let rounds = [Round::from(0)];
let mut scheduler = SessionMessageScheduler::<_, (), ()>::with_test_values(
let mut scheduler = EpochMessageScheduler::<_, (), ()>::with_test_values(
// No cover messages to emit, tick will yield round `0`.
SessionCoverTraffic::with_test_values(Box::new(iter(rounds)), 0, 1.into(), rng.clone(), 0),
EpochCoverTraffic::with_test_values(Box::new(iter(rounds)), 0, 1.into(), rng.clone(), 0),
// Round `1` scheduled, tick will yield round `0`.
SessionProcessedMessageDelayer::with_test_values(
EpochProcessedMessageDelayer::with_test_values(
NonZeroU64::try_from(1).unwrap(),
1u128.into(),
rng,
@ -47,11 +47,11 @@ async fn no_substream_ready_and_no_data_messages() {
async fn no_substream_ready_with_data_messages() {
let rng = BlakeRng::from_entropy();
let rounds = [Round::from(0)];
let mut scheduler = SessionMessageScheduler::<_, (), u32>::with_test_values(
let mut scheduler = EpochMessageScheduler::<_, (), u32>::with_test_values(
// No cover messages to emit, tick will yield round `0`.
SessionCoverTraffic::with_test_values(Box::new(iter(rounds)), 0, 1.into(), rng.clone(), 0),
EpochCoverTraffic::with_test_values(Box::new(iter(rounds)), 0, 1.into(), rng.clone(), 0),
// Round `1` scheduled, tick will yield round `0`.
SessionProcessedMessageDelayer::with_test_values(
EpochProcessedMessageDelayer::with_test_values(
NonZeroU64::try_from(1).unwrap(),
1u128.into(),
rng,
@ -81,11 +81,11 @@ async fn no_substream_ready_with_data_messages() {
async fn cover_traffic_substream_ready() {
let rng = BlakeRng::from_entropy();
let rounds = [Round::from(0)];
let mut scheduler = SessionMessageScheduler::<_, (), u32>::with_test_values(
let mut scheduler = EpochMessageScheduler::<_, (), u32>::with_test_values(
// 1 cover message over 1 round: guaranteed emission.
SessionCoverTraffic::with_test_values(Box::new(iter(rounds)), 1, 1.into(), rng.clone(), 0),
EpochCoverTraffic::with_test_values(Box::new(iter(rounds)), 1, 1.into(), rng.clone(), 0),
// Round `1` scheduled, tick will yield round `0`.
SessionProcessedMessageDelayer::with_test_values(
EpochProcessedMessageDelayer::with_test_values(
NonZeroU64::try_from(1).unwrap(),
1u128.into(),
rng,
@ -112,11 +112,11 @@ async fn cover_traffic_substream_ready() {
async fn release_delayer_substream_ready() {
let rng = BlakeRng::from_entropy();
let rounds = [Round::from(0)];
let mut scheduler = SessionMessageScheduler::<_, u32, u32>::with_test_values(
let mut scheduler = EpochMessageScheduler::<_, u32, u32>::with_test_values(
// No cover messages to emit.
SessionCoverTraffic::with_test_values(Box::new(iter(rounds)), 0, 1.into(), rng.clone(), 0),
EpochCoverTraffic::with_test_values(Box::new(iter(rounds)), 0, 1.into(), rng.clone(), 0),
// Round `0` scheduled, tick will yield round `0`.
SessionProcessedMessageDelayer::with_test_values(
EpochProcessedMessageDelayer::with_test_values(
NonZeroU64::try_from(1).unwrap(),
0u128.into(),
rng,
@ -143,11 +143,11 @@ async fn release_delayer_substream_ready() {
async fn both_substreams_ready() {
let rng = BlakeRng::from_entropy();
let rounds = [Round::from(0)];
let mut scheduler = SessionMessageScheduler::<_, u32, ()>::with_test_values(
let mut scheduler = EpochMessageScheduler::<_, u32, ()>::with_test_values(
// 1 cover message over 1 round: guaranteed emission.
SessionCoverTraffic::with_test_values(Box::new(iter(rounds)), 1, 1.into(), rng.clone(), 0),
EpochCoverTraffic::with_test_values(Box::new(iter(rounds)), 1, 1.into(), rng.clone(), 0),
// Round `0` scheduled, tick will yield round `0`.
SessionProcessedMessageDelayer::with_test_values(
EpochProcessedMessageDelayer::with_test_values(
NonZeroU64::try_from(1).unwrap(),
0u128.into(),
rng,
@ -180,13 +180,13 @@ async fn round_change() {
Round::from(2),
Round::from(3),
];
let mut scheduler = SessionMessageScheduler::<_, (), u32>::with_test_values(
let mut scheduler = EpochMessageScheduler::<_, (), u32>::with_test_values(
// 2 cover messages over 2 remaining rounds: every round is guaranteed to be a
// release round. After the first emission, a data message will cause the second
// release to be skipped (threshold = 1/1 = 1.0).
SessionCoverTraffic::with_test_values(Box::new(iter(rounds)), 2, 2.into(), rng.clone(), 0),
EpochCoverTraffic::with_test_values(Box::new(iter(rounds)), 2, 2.into(), rng.clone(), 0),
// Round `3` scheduled, tick will yield rounds `0` through `3`.
SessionProcessedMessageDelayer::with_test_values(
EpochProcessedMessageDelayer::with_test_values(
NonZeroU64::try_from(1).unwrap(),
3u128.into(),
rng,

View File

@ -15,7 +15,7 @@ const LOG_TARGET: &str = blend::scheduling::DELAY;
/// A scheduler for delaying processed messages and batch them into release
/// rounds, as per the specification.
pub struct SessionProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage> {
pub struct EpochProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage> {
/// The maximum delay, in terms of rounds, between two consecutive release
/// rounds.
maximum_release_delay_in_rounds: NonZeroU64,
@ -31,7 +31,7 @@ pub struct SessionProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage> {
}
impl<RoundClock, Rng, ProcessedMessage>
SessionProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage>
EpochProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage>
where
Rng: rand::Rng,
{
@ -58,7 +58,7 @@ where
}
impl<RoundClock, Rng, ProcessedMessage>
SessionProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage>
EpochProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage>
{
#[cfg(test)]
pub const fn with_test_values(
@ -126,7 +126,7 @@ where
}
impl<RoundClock, Rng, ProcessedMessage> Stream
for SessionProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage>
for EpochProcessedMessageDelayer<RoundClock, Rng, ProcessedMessage>
where
RoundClock: Stream<Item = Round> + Unpin,
Rng: rand::Rng + Unpin,
@ -179,11 +179,11 @@ mod tests {
use rand::SeedableRng as _;
use tokio_stream::iter;
use crate::release_delayer::SessionProcessedMessageDelayer;
use crate::release_delayer::EpochProcessedMessageDelayer;
#[test]
fn poll_none_on_unscheduled_round() {
let mut delayer = SessionProcessedMessageDelayer::<_, _, ()> {
let mut delayer = EpochProcessedMessageDelayer::<_, _, ()> {
maximum_release_delay_in_rounds: NonZeroU64::new(3).expect("Non-zero usize"),
next_release_round: 1u128.into(),
rng: BlakeRng::from_entropy(),
@ -199,7 +199,7 @@ mod tests {
#[test]
fn poll_empty_on_scheduled_round_with_empty_queue() {
let mut delayer = SessionProcessedMessageDelayer::<_, _, ()> {
let mut delayer = EpochProcessedMessageDelayer::<_, _, ()> {
maximum_release_delay_in_rounds: NonZeroU64::new(3).expect("Non-zero usize"),
next_release_round: 1u128.into(),
rng: BlakeRng::from_entropy(),
@ -216,7 +216,7 @@ mod tests {
#[test]
fn poll_non_empty_on_scheduled_round_with_non_empty_queue() {
let mut delayer = SessionProcessedMessageDelayer::<_, _, ()> {
let mut delayer = EpochProcessedMessageDelayer::<_, _, ()> {
maximum_release_delay_in_rounds: NonZeroU64::new(3).expect("Non-zero usize"),
next_release_round: 1u128.into(),
rng: BlakeRng::from_entropy(),
@ -236,7 +236,7 @@ mod tests {
#[test]
fn add_message_to_queue() {
let mut delayer = SessionProcessedMessageDelayer::<_, _, bool> {
let mut delayer = EpochProcessedMessageDelayer::<_, _, bool> {
maximum_release_delay_in_rounds: NonZeroU64::new(3).expect("Non-zero usize"),
next_release_round: 1u128.into(),
rng: BlakeRng::from_entropy(),

View File

@ -1,262 +0,0 @@
use std::{
future::Future as _,
pin::Pin,
task::{Context, Poll},
time::Duration,
};
use futures::StreamExt as _;
use tokio::time::{Sleep, sleep};
use crate::stream::{FirstReadyStreamError, UninitializedFirstReadyStream};
/// A staging type that initializes a [`SessionEventStream`] by consuming
/// the first [`Session`] from the underlying stream, expected to be yielded
/// within a short timeout.
pub struct UninitializedSessionEventStream<Stream> {
stream: UninitializedFirstReadyStream<Stream>,
transition_period: Duration,
}
impl<Stream> UninitializedSessionEventStream<Stream> {
#[must_use]
pub const fn new(
session_stream: Stream,
first_ready_timeout: Duration,
transition_period: Duration,
) -> Self {
Self {
stream: UninitializedFirstReadyStream::new(session_stream, first_ready_timeout),
transition_period,
}
}
}
impl<Stream, Session> UninitializedSessionEventStream<Stream>
where
Stream: futures::Stream<Item = Session> + Unpin,
{
/// Initializes a [`SessionEventStream`] by consuming the first [`Session`]
/// from the underlying stream.
///
/// It returns the first [`Session`] and the initialized
/// [`SessionEventStream`].
/// It returns an error if the first session is not yielded within the
/// configured timeout.
pub async fn await_first_ready(
self,
) -> Result<(Session, SessionEventStream<Stream>), FirstReadyStreamError> {
let (first_session, remaining_stream) = self.stream.first().await?;
Ok((
first_session,
SessionEventStream::new(remaining_stream, self.transition_period),
))
}
}
#[derive(Clone, Debug)]
pub enum SessionEvent<Session> {
NewSession(Session),
TransitionPeriodExpired,
}
/// A stream that alternates between yielding [`SessionEvent::NewSession`]
/// and [`SessionEvent::TransitionPeriodExpired`].
///
/// It wraps a stream of [`Session`]s and yields a [`SessionEvent::NewSession`]
/// as soon as a new [`Session`] is available from the inner stream.
/// Then, it yields a [`SessionEvent::TransitionPeriodExpired`] after
/// the transition period has elapsed.
///
/// # Stream Timeline
/// ```text
/// event stream : O--E-------O--E--------------O--E-------
/// session stream: |----S1----|--------S2-------|----S3----
///
/// (O: NewSession, E: TransitionPeriodExpired, S*: Sessions)
/// ```
pub struct SessionEventStream<Stream> {
session_stream: Stream,
transition_period: Duration,
transition_period_timer: Option<Pin<Box<Sleep>>>,
}
impl<Stream> SessionEventStream<Stream> {
#[must_use]
const fn new(session_stream: Stream, transition_period: Duration) -> Self {
Self {
session_stream,
transition_period,
transition_period_timer: None,
}
}
}
impl<Stream, Session> futures::Stream for SessionEventStream<Stream>
where
Stream: futures::Stream<Item = Session> + Unpin,
{
type Item = SessionEvent<Session>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
// Check if a new session is available.
match self.session_stream.poll_next_unpin(cx) {
Poll::Ready(Some(session)) => {
// Start the transition period timer, and yield the new session.
// If the previous transition period timer has not been expired yet,
// it will be overwritten.
self.transition_period_timer = Some(Box::pin(sleep(self.transition_period)));
return Poll::Ready(Some(SessionEvent::NewSession(session)));
}
Poll::Ready(None) => return Poll::Ready(None),
Poll::Pending => {}
}
// Check if the transition period has expired.
if let Some(timer) = &mut self.transition_period_timer
&& timer.as_mut().poll(cx).is_ready()
{
self.transition_period_timer = None;
return Poll::Ready(Some(SessionEvent::TransitionPeriodExpired));
}
Poll::Pending
}
}
#[cfg(test)]
mod tests {
use futures::StreamExt as _;
use tokio::time::{Instant, interval, interval_at};
use tokio_stream::wrappers::IntervalStream;
use super::*;
#[tokio::test]
async fn yield_two_events_alternately() {
let session_duration = Duration::from_secs(1);
let transition_period = Duration::from_millis(200);
let time_tolerance = Duration::from_millis(100);
let mut stream = SessionEventStream::new(
Box::pin(IntervalStream::new(interval(session_duration))),
transition_period,
);
// NewSession should be emitted immediately.
let start_time = Instant::now();
assert!(matches!(
stream.next().await,
Some(SessionEvent::NewSession(_))
));
let elapsed = start_time.elapsed();
let tolerance = Duration::from_millis(50);
assert!(elapsed <= tolerance, "elapsed:{elapsed:?}");
// TransitionEnd should be emitted after transition_period.
let start_time = Instant::now();
assert!(matches!(
stream.next().await,
Some(SessionEvent::TransitionPeriodExpired)
));
let elapsed = start_time.elapsed();
assert!(
elapsed.abs_diff(transition_period) <= time_tolerance,
"elapsed:{elapsed:?}, expected:{transition_period:?}",
);
// NewSession should be emitted after session_duration - transition_period.
let start_time = Instant::now();
assert!(matches!(
stream.next().await,
Some(SessionEvent::NewSession(_))
));
let elapsed = start_time.elapsed();
assert!(
elapsed.abs_diff(session_duration.checked_sub(transition_period).unwrap())
<= time_tolerance,
"elapsed:{elapsed:?}, expected:{:?}",
session_duration.checked_sub(transition_period).unwrap()
);
// TransitionEnd should be emitted after transition_period.
let start_time = Instant::now();
assert!(matches!(
stream.next().await,
Some(SessionEvent::TransitionPeriodExpired)
));
let elapsed = start_time.elapsed();
assert!(
elapsed.abs_diff(transition_period) <= time_tolerance,
"elapsed:{elapsed:?}, expected:{transition_period:?}",
);
}
#[tokio::test]
async fn transition_period_shorter_than_session() {
let session_duration = Duration::from_millis(500);
let transition_period = Duration::from_millis(600);
let time_tolerance = Duration::from_millis(50);
let mut stream = SessionEventStream::new(
Box::pin(IntervalStream::new(interval(session_duration))),
transition_period,
);
// NewSession should be emitted immediately.
let start_time = Instant::now();
assert!(matches!(
stream.next().await,
Some(SessionEvent::NewSession(_))
));
let elapsed = start_time.elapsed();
assert!(elapsed <= time_tolerance, "elapsed:{elapsed:?}");
// NewSession should be emitted again after session_duration.
let start_time = Instant::now();
assert!(matches!(
stream.next().await,
Some(SessionEvent::NewSession(_))
));
let elapsed = start_time.elapsed();
assert!(
elapsed.abs_diff(session_duration) <= time_tolerance,
"elapsed:{elapsed:?}, expected:{session_duration:?}",
);
}
#[tokio::test]
async fn first_ready_stream_yields_first_item_immediately() {
// Use an underlying stream that yields the first item nearly immediately.
let stream = UninitializedFirstReadyStream::new(
IntervalStream::new(interval(Duration::from_secs(1)))
.enumerate()
.map(|(i, _)| i),
Duration::from_millis(100),
);
let (first, mut stream) = stream.first().await.expect("first item should be yielded");
assert_eq!(first, 0);
// Next items are yielded normally.
assert_eq!(stream.next().await, Some(1));
assert_eq!(stream.next().await, Some(2));
}
#[tokio::test]
async fn first_relay_stream_fails_if_first_item_is_not_ready() {
// Use an underlying stream that yield the first item too late.
let stream = UninitializedFirstReadyStream::new(
IntervalStream::new(interval_at(
// The first time will be yieled after 2s.
Instant::now() + Duration::from_secs(2),
Duration::from_secs(1),
)),
Duration::from_millis(100),
);
assert!(matches!(
stream.first().await,
Err(FirstReadyStreamError::FirstItemNotReady)
));
}
}

View File

@ -1,22 +1,19 @@
use core::time::Duration;
use futures::StreamExt as _;
use tokio::time::timeout;
/// A stream wrapper that expects the underlying stream to yield its first item
/// within the given timeout.
/// A stream wrapper that consumes the first item from the underlying stream.
///
/// Instead of implementing [`futures::Stream`], this provides only
/// [`Self::first`] that explicitly tries to read the first item and returns a
/// result.
/// [`Self::first`] that explicitly reads the first item and returns it together
/// with the remaining stream. It waits for the first item for as long as
/// necessary: blend membership and the slot clock only become available at
/// genesis, which may be arbitrarily far in the future, so there is no timeout.
pub struct UninitializedFirstReadyStream<Stream> {
stream: Stream,
timeout: Duration,
}
impl<Stream> UninitializedFirstReadyStream<Stream> {
pub const fn new(stream: Stream, timeout: Duration) -> Self {
Self { stream, timeout }
pub const fn new(stream: Stream) -> Self {
Self { stream }
}
}
@ -24,13 +21,17 @@ impl<Stream> UninitializedFirstReadyStream<Stream>
where
Stream: futures::Stream + Unpin,
{
/// Yields the first item if it is yielded within the timeout from the
/// underlying stream.
/// The remaining stream is also returned for continued use.
/// Yields the first item from the underlying stream, awaiting it for as
/// long as necessary. The remaining stream is also returned for
/// continued use.
///
/// Returns [`FirstReadyStreamError::StreamClosed`] if the underlying stream
/// ends before yielding any item.
pub async fn first(mut self) -> Result<(Stream::Item, Stream), FirstReadyStreamError> {
let item = timeout(self.timeout, self.stream.next())
let item = self
.stream
.next()
.await
.map_err(|_| FirstReadyStreamError::FirstItemNotReady)?
.ok_or(FirstReadyStreamError::StreamClosed)?;
Ok((item, self.stream))
}
@ -38,8 +39,6 @@ where
#[derive(Debug, thiserror::Error)]
pub enum FirstReadyStreamError {
#[error("The first item was not yielded in time")]
FirstItemNotReady,
#[error("The underlying stream was closed before yielding the first item")]
StreamClosed,
}

View File

@ -1,3 +1,8 @@
use core::{
cmp::Ordering,
fmt::{self, Display, Formatter},
ops::AddAssign,
};
use std::{
num::NonZero,
ops::{Add, Sub},
@ -44,6 +49,41 @@ impl Epoch {
pub const fn saturating_add(self, rhs: Self) -> Self {
Self(self.0.saturating_add(rhs.0))
}
#[must_use]
pub const fn saturating_sub(self, rhs: Self) -> Self {
Self(self.0.saturating_sub(rhs.0))
}
}
impl Display for Epoch {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Epoch({})", self.0)
}
}
impl PartialEq<u32> for Epoch {
fn eq(&self, other: &u32) -> bool {
self.0 == *other
}
}
impl PartialEq<Epoch> for u32 {
fn eq(&self, other: &Epoch) -> bool {
*self == other.0
}
}
impl PartialOrd<u32> for Epoch {
fn partial_cmp(&self, other: &u32) -> Option<Ordering> {
self.0.partial_cmp(other)
}
}
impl PartialOrd<Epoch> for u32 {
fn partial_cmp(&self, other: &Epoch) -> Option<Ordering> {
self.partial_cmp(&other.0)
}
}
impl Slot {
@ -163,6 +203,28 @@ impl Add<u32> for Epoch {
}
}
impl AddAssign<u32> for Epoch {
fn add_assign(&mut self, rhs: u32) {
self.0 += rhs;
}
}
impl Add for Epoch {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Sub for Epoch {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 - rhs.0)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EpochConfig {
// The stake distribution is always taken at the beginning of the previous epoch.
@ -199,6 +261,11 @@ impl EpochConfig {
pub fn starting_slot(&self, epoch: &Epoch, base_period_length: NonZero<u64>) -> Slot {
Slot::from(u64::from(u32::from(*epoch)) * self.epoch_length(base_period_length))
}
#[must_use]
pub fn last_slot(&self, epoch: Epoch, base_period_length: NonZero<u64>) -> Slot {
Slot::from(u64::from(epoch.into_inner() + 1) * self.epoch_length(base_period_length) - 1)
}
}
#[must_use]

View File

@ -4,19 +4,19 @@ use lb_utils::math::NonNegativeF64;
#[must_use]
pub fn core_quota(
rounds_per_session: NonZeroU64,
rounds_per_epoch: NonZeroU64,
message_frequency_per_round: NonNegativeF64,
num_blend_layers: NonZeroU64,
membership_size: usize,
) -> u64 {
// `C`: Expected number of cover messages that are generated during a session by
// `C`: Expected number of cover messages that are generated during an epoch by
// the core nodes.
let expected_number_of_session_messages =
rounds_per_session.get() as f64 * message_frequency_per_round.get();
let expected_number_of_epoch_messages =
rounds_per_epoch.get() as f64 * message_frequency_per_round.get();
// `Q_c`: Messaging allowance that can be used by a core node during a single
// session. We assume `R_c` to be `0` for now, hence `Q_c = ceil(C * (ß_c
// epoch. We assume `R_c` to be `0` for now, hence `Q_c = ceil(C * (ß_c
// + 0 * ß_c)) / N = ceil(C * ß_c) / N`.
((expected_number_of_session_messages * num_blend_layers.get() as f64) / membership_size as f64)
((expected_number_of_epoch_messages * num_blend_layers.get() as f64) / membership_size as f64)
.ceil() as u64
}

View File

@ -42,7 +42,7 @@ use crate::{
// Maximum memory allocation size allowed for SDP activity metadata.
// Protects against unbounded allocation in `decode_sdp_active`
const MAX_ENCODE_DECODE_METADATA_SIZE: u32 = 234; // `ActiveMessage` has a fixed size of 234 bytes
const MAX_ENCODE_DECODE_METADATA_SIZE: u32 = 230; // `ActiveMessage` has a fixed size of 230 bytes
// Maximum byte size allowed for a locator in SDPDeclare operations.
const LOCATOR_BYTES_SIZE_LIMIT: usize = 329usize;
@ -1252,7 +1252,7 @@ mod tests {
let signing_key = Ed25519Key::from_bytes(&[1u8; 32]);
let blend_proof = ActivityProof {
session: 42,
epoch: 42.into(),
signing_key: signing_key.public_key(),
proof_of_quota: VerifiedProofOfQuota::from_bytes_unchecked([0u8; 160]).into(),
proof_of_selection: VerifiedProofOfSelection::from_bytes_unchecked([0u8; 32]).into(),
@ -1310,7 +1310,7 @@ mod tests {
};
let blend_proof = ActivityProof {
session: u64::MAX,
epoch: u32::MAX.into(),
signing_key: signing_key.public_key(),
proof_of_quota: VerifiedProofOfQuota::from_bytes_unchecked([0u8; 160]).into(),
proof_of_selection: VerifiedProofOfSelection::from_bytes_unchecked([0u8; 32]).into(),
@ -1842,7 +1842,7 @@ mod tests {
#[test]
fn test_encode_reject_excessive_sdp_active() {
let blend_proof = ActivityProof {
session: u64::MAX,
epoch: u32::MAX.into(),
signing_key: Ed25519Key::from_bytes(&[1; 32]).public_key(),
proof_of_quota: VerifiedProofOfQuota::from_bytes_unchecked([0u8; 160]).into(),
proof_of_selection: VerifiedProofOfSelection::from_bytes_unchecked([0u8; 32]).into(),

View File

@ -1,10 +1,10 @@
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::{ZkPublicKey, ZkSignature};
use lb_log_targets::mantle;
use tracing::debug;
use super::{SDPActiveOp, SdpError};
use crate::{
block::BlockNumber,
events::Events,
mantle::{
TxHash,
@ -21,7 +21,7 @@ pub struct SDPActiveValidationContext<'a> {
}
pub struct SDPActiveExecutionContext {
pub block_number: BlockNumber,
pub epoch: Epoch,
pub declarations: Declarations,
}
@ -64,13 +64,13 @@ impl Operation<SDPActiveValidationContext<'_>> for SDPActiveOp {
.get_mut(&self.declaration_id)
.expect("The operation should have been validated");
declaration.active = ctx.block_number;
declaration.active = ctx.epoch;
declaration.nonce = self.nonce;
debug!(
target: LOG_TARGET,
provider_id = ?declaration.provider_id,
active = declaration.active,
nonce = declaration.nonce,
active = ?declaration.active,
nonce = ?declaration.nonce,
"updated declaration with active message"
);

View File

@ -1,3 +1,4 @@
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::{Ed25519Signature, ZkPublicKey, ZkSignature};
use super::{MAX_DECLARATION_LOCATOR, SDPDeclareOp, SdpError};
@ -67,7 +68,7 @@ impl SDPDeclareValidationExt for SDPDeclareOp {
mut ctx: SDPDeclareExecutionContext,
) -> Result<(SDPDeclareExecutionContext, Events), SdpError> {
let declaration_id = self.id();
let declaration = Declaration::new(ctx.block_number, self);
let declaration = Declaration::new(ctx.epoch, self);
ctx.declarations = ctx.declarations.insert(declaration_id, declaration);
let utxo = ctx
.utxo_tree
@ -109,7 +110,7 @@ pub struct SDPDeclareGenesisValidationContext<'a> {
pub struct SDPDeclareExecutionContext {
pub utxo_tree: Utxos,
pub block_number: u64,
pub epoch: Epoch,
pub declarations: Declarations,
pub locked_notes: LockedNotes,
pub min_stake: MinStake,

View File

@ -1,31 +1,30 @@
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::{ZkPublicKey, ZkSignature};
use lb_log_targets::mantle;
use tracing::debug;
use super::{SDPWithdrawOp, SdpError};
use crate::{
block::BlockNumber,
events::Events,
mantle::{
TxHash,
ledger::{Declarations, Operation},
},
sdp::locked_notes::LockedNotes,
sdp::{NumberOfEpochs, locked_notes::LockedNotes},
};
const LOG_TARGET: &str = mantle::sdp::message::WITHDRAW;
pub struct SDPWithdrawValidationContext<'a> {
pub lock_period: &'a u64,
pub lock_period: NumberOfEpochs,
pub declarations: &'a Declarations,
pub block_number: &'a BlockNumber,
pub epoch: Epoch,
pub locked_notes: &'a LockedNotes,
pub tx_hash: &'a TxHash,
pub sdp_withdraw_sig: &'a ZkSignature,
}
pub struct SDPWithdrawExecutionContext {
pub block_number: BlockNumber,
pub declarations: Declarations,
pub locked_notes: LockedNotes,
}
@ -64,7 +63,7 @@ impl Operation<SDPWithdrawValidationContext<'_>> for SDPWithdrawOp {
}
// Check the note can be unlocked
if declaration.created + ctx.lock_period >= *ctx.block_number {
if declaration.created + ctx.lock_period >= ctx.epoch {
return Err(SdpError::WithdrawalWhileLocked);
}
@ -109,6 +108,8 @@ impl Operation<SDPWithdrawValidationContext<'_>> for SDPWithdrawOp {
"updated declaration with withdraw message"
);
// TODO: Delay the unlock for SNAPSHOT_FINALIZATION_DELAY epochs to prevent
// stake-less service provision: https://www.notion.so/RFC-Remove-Concept-of-a-Session-f39261aa09df826db83e81613bb454dc?source=copy_link#357261aa09df80d79dcbeccd5487b5a5
let _ = ctx
.locked_notes
.unlock(declaration.service_type, &self.locked_note_id)

View File

@ -2,18 +2,19 @@ use lb_blend_proofs::{
quota::{PROOF_OF_QUOTA_SIZE, ProofOfQuota},
selection::{PROOF_OF_SELECTION_SIZE, ProofOfSelection},
};
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::ED25519_PUBLIC_KEY_SIZE;
use nom::{IResult, Parser as _, bytes::complete::take, number::complete::u8 as nom_u8};
use serde::{Deserialize, Serialize};
use crate::{
mantle::ops::channel::Ed25519PublicKey,
sdp::{ACTIVE_METADATA_BLEND_TYPE, SessionNumber, parse_session_number},
sdp::{ACTIVE_METADATA_BLEND_TYPE, parse_epoch},
};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ActivityProof {
pub session: SessionNumber,
pub epoch: Epoch,
pub signing_key: Ed25519PublicKey,
pub proof_of_quota: ProofOfQuota,
pub proof_of_selection: ProofOfSelection,
@ -29,7 +30,7 @@ impl ActivityProof {
let proof_of_selection: [u8; _] = (&self.proof_of_selection).into();
let total_size = 2 // type + version byte
+ size_of::<SessionNumber>()
+ size_of::<Epoch>()
+ signing_key.len()
+ proof_of_quota.len()
+ proof_of_selection.len();
@ -37,7 +38,7 @@ impl ActivityProof {
let mut bytes = Vec::with_capacity(total_size);
bytes.push(ACTIVE_METADATA_BLEND_TYPE);
bytes.push(BLEND_ACTIVE_METADATA_VERSION_BYTE);
bytes.extend(&self.session.to_le_bytes());
bytes.extend(&self.epoch.into_inner().to_le_bytes());
bytes.extend(&signing_key);
bytes.extend(&proof_of_quota);
bytes.extend(&proof_of_selection);
@ -68,7 +69,7 @@ fn parse_activity_proof(input: &[u8]) -> IResult<&[u8], ActivityProof> {
nom::error::ErrorKind::Verify,
)));
}
let (input, session) = parse_session_number(input)?;
let (input, epoch) = parse_epoch(input)?;
let (input, signing_key) = parse_const_size_bytes::<ED25519_PUBLIC_KEY_SIZE>(input)?;
let signing_key = Ed25519PublicKey::from_bytes(&signing_key)
@ -94,7 +95,7 @@ fn parse_activity_proof(input: &[u8]) -> IResult<&[u8], ActivityProof> {
Ok((
input,
ActivityProof {
session,
epoch,
signing_key,
proof_of_quota,
proof_of_selection,
@ -121,7 +122,7 @@ mod tests {
#[test]
fn activity_proof_roundtrip() {
let proof = ActivityProof {
session: 10,
epoch: 10.into(),
signing_key: new_signing_key(0),
proof_of_quota: new_proof_of_quota_unchecked(0),
proof_of_selection: new_proof_of_selection_unchecked(1),
@ -136,7 +137,7 @@ mod tests {
#[test]
fn activity_proof_invalid_version() {
let proof = ActivityProof {
session: 10,
epoch: 10.into(),
signing_key: new_signing_key(0),
proof_of_quota: new_proof_of_quota_unchecked(0),
proof_of_selection: new_proof_of_selection_unchecked(1),
@ -161,7 +162,7 @@ mod tests {
#[test]
fn activity_proof_too_long() {
let proof = ActivityProof {
session: 10,
epoch: 10.into(),
signing_key: new_signing_key(0),
proof_of_quota: new_proof_of_quota_unchecked(0),
proof_of_selection: new_proof_of_selection_unchecked(1),
@ -177,7 +178,7 @@ mod tests {
#[test]
fn activity_metadata_roundtrip() {
let proof = ActivityProof {
session: 10,
epoch: 10.into(),
signing_key: new_signing_key(0),
proof_of_quota: new_proof_of_quota_unchecked(0),
proof_of_selection: new_proof_of_selection_unchecked(1),

View File

@ -1,10 +1,15 @@
pub mod blend;
pub mod locked_notes;
use core::str::FromStr;
use std::hash::Hash;
use core::{
ops::{Add, Sub},
str::FromStr,
};
use std::{collections::HashMap, hash::Hash};
use blake2::{Blake2b, Digest as _};
use bytes::Bytes;
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::ZkPublicKey;
use lb_utils::bounded_vec::LowerBoundedVec;
use multiaddr::{Multiaddr, Protocol};
@ -14,11 +19,11 @@ use strum::EnumIter;
use crate::{
block::BlockNumber,
codec::{self, DeserializeOp as _, SerializeOp as _},
mantle::{NoteId, ops::channel::Ed25519PublicKey},
utils::{display_hex_bytes_newtype, serde_bytes_newtype},
};
pub type SessionNumber = u64;
pub type StakeThreshold = u64;
const ACTIVE_METADATA_BLEND_TYPE: u8 = 0x01;
@ -31,17 +36,60 @@ pub struct MinStake {
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServiceParameters {
pub lock_period: u64,
pub inactivity_period: u64,
pub retention_period: u64,
pub timestamp: BlockNumber,
pub session_duration: BlockNumber,
/// Minimum epochs during which a declaration cannot be withdrawn
pub lock_period: NumberOfEpochs,
/// Maximum epochs during which an activity message must be sent
pub inactivity_period: NumberOfEpochs,
/// Epochs after which a declaration can be safely deleted by Garbage
/// Collection
pub retention_period: NumberOfEpochs,
// Epoch number at which this parameter was set
pub epoch: Epoch,
}
impl ServiceParameters {
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NumberOfEpochs(Epoch);
impl NumberOfEpochs {
#[must_use]
pub const fn session_for_block(&self, block_number: BlockNumber) -> SessionNumber {
block_number / self.session_duration
pub const fn new(epoch: Epoch) -> Self {
Self(epoch)
}
}
impl From<u32> for NumberOfEpochs {
fn from(value: u32) -> Self {
Self(value.into())
}
}
impl From<NumberOfEpochs> for u32 {
fn from(this: NumberOfEpochs) -> Self {
this.0.into_inner()
}
}
impl Add for NumberOfEpochs {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Add<NumberOfEpochs> for Epoch {
type Output = Self;
fn add(self, rhs: NumberOfEpochs) -> Self::Output {
self + rhs.0
}
}
impl Sub<NumberOfEpochs> for Epoch {
type Output = Self;
fn sub(self, rhs: NumberOfEpochs) -> Self::Output {
self - rhs.0
}
}
@ -182,9 +230,9 @@ pub struct Declaration {
pub locked_note_id: NoteId,
pub locators: Locators,
pub zk_id: ZkPublicKey,
pub created: BlockNumber,
pub active: BlockNumber,
pub withdrawn: Option<BlockNumber>,
pub created: Epoch,
pub active: Epoch,
pub withdrawn: Option<Epoch>,
pub nonce: Nonce,
}
@ -194,23 +242,66 @@ pub struct ProviderInfo {
pub zk_id: ZkPublicKey,
}
const SNAPSHOT_FINALIZATION_DELAY: Epoch = Epoch::new(2);
impl Declaration {
#[must_use]
pub fn new(block_number: BlockNumber, declaration_msg: &DeclarationMessage) -> Self {
pub fn new(epoch: Epoch, declaration_msg: &DeclarationMessage) -> Self {
Self {
service_type: declaration_msg.service_type,
provider_id: declaration_msg.provider_id,
locked_note_id: declaration_msg.locked_note_id,
locators: declaration_msg.locators.clone(),
zk_id: declaration_msg.zk_id,
created: block_number,
active: block_number,
created: epoch,
active: epoch + SNAPSHOT_FINALIZATION_DELAY,
withdrawn: None,
nonce: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct Declarations(HashMap<ServiceType, HashMap<DeclarationId, Declaration>>);
impl Declarations {
pub fn iter(
&self,
) -> impl Iterator<Item = (&ServiceType, &HashMap<DeclarationId, Declaration>)> {
self.0.iter()
}
}
impl From<HashMap<ServiceType, HashMap<DeclarationId, Declaration>>> for Declarations {
fn from(value: HashMap<ServiceType, HashMap<DeclarationId, Declaration>>) -> Self {
Self(value)
}
}
impl FromIterator<(ServiceType, HashMap<DeclarationId, Declaration>)> for Declarations {
fn from_iter<I: IntoIterator<Item = (ServiceType, HashMap<DeclarationId, Declaration>)>>(
iter: I,
) -> Self {
Self(iter.into_iter().collect())
}
}
impl TryFrom<Bytes> for Declarations {
type Error = codec::Error;
fn try_from(bytes: Bytes) -> Result<Self, Self::Error> {
Self::from_bytes(&bytes)
}
}
impl TryFrom<Declarations> for Bytes {
type Error = codec::Error;
fn try_from(this: Declarations) -> Result<Self, Self::Error> {
this.to_bytes()
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct DeclarationMessage {
pub service_type: ServiceType,
@ -289,16 +380,19 @@ impl ActivityMetadata {
}
}
fn parse_session_number(input: &[u8]) -> IResult<&[u8], SessionNumber> {
let (input, bytes) = take(size_of::<SessionNumber>()).parse(input)?;
let session_bytes: [u8; 8] = bytes
fn parse_epoch(input: &[u8]) -> IResult<&[u8], Epoch> {
let (input, bytes) = take(size_of::<Epoch>()).parse(input)?;
let epoch_bytes: [u8; 4] = bytes
.try_into()
.map_err(|_| nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Fail)))?;
Ok((input, SessionNumber::from_le_bytes(session_bytes)))
Ok((input, u32::from_le_bytes(epoch_bytes).into()))
}
#[cfg(test)]
mod tests {
use lb_groth16::{Field as _, Fr};
use lb_key_management_system_keys::keys::Ed25519Key;
use super::*;
#[test]
@ -382,4 +476,28 @@ mod tests {
"Input cannot be empty."
);
}
#[test]
fn declaration_initialization() {
let msg = DeclarationMessage {
service_type: ServiceType::BlendNetwork,
locators: vec!["/ip4/127.0.0.1/udp/3001/quic-v1".parse().unwrap()]
.try_into()
.unwrap(),
provider_id: Ed25519Key::from_bytes(&[0; _]).public_key().into(),
zk_id: ZkPublicKey::zero(),
locked_note_id: Fr::ZERO.into(),
};
let declaration = Declaration::new(Epoch::new(10), &msg);
assert_eq!(declaration.service_type, msg.service_type);
assert_eq!(declaration.provider_id, msg.provider_id);
assert_eq!(declaration.locked_note_id, msg.locked_note_id);
assert_eq!(declaration.locators, msg.locators);
assert_eq!(declaration.zk_id, msg.zk_id);
assert_eq!(declaration.created, Epoch::new(10));
assert_eq!(declaration.active, Epoch::new(12)); // created + SNAPSHOT_FINALIZATION_DELAY
assert_eq!(declaration.withdrawn, None);
assert_eq!(declaration.nonce, 0);
}
}

View File

@ -69,7 +69,7 @@ impl SecureKeyOperator for PoQOperator {
.await
.map_err(Self::Error::FailedOperatorCall)?;
drop(self.response_channel.send(poq_result).inspect_err(|_| {
trace!(target: LOG_TARGET, "Error sending generated proof of quota, most likely due to a session or epoch rotation that discarded the receiver side of the channel.");
trace!(target: LOG_TARGET, "Error sending generated proof of quota, most likely due to an epoch rotation that discarded the receiver side of the channel.");
}));
Ok(())
}

View File

@ -84,6 +84,12 @@ impl Config {
self.epoch_config
.epoch(slot, self.consensus_config.base_period_length())
}
#[must_use]
pub fn last_slot(&self, epoch: Epoch) -> Slot {
self.epoch_config
.last_slot(epoch, self.consensus_config.base_period_length())
}
}
#[cfg(test)]
@ -101,34 +107,37 @@ mod tests {
#[test]
fn epoch_snapshots() {
let epoch_config = EpochConfig {
epoch_stake_distribution_stabilization: NonZero::new(3u8).unwrap(),
epoch_period_nonce_buffer: NonZero::new(3).unwrap(),
epoch_period_nonce_stabilization: NonZero::new(4).unwrap(),
};
let consensus_config = lb_cryptarchia_engine::Config::new(
NonZero::new(5).unwrap(),
NonNegativeRatio::new(1, 2.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
);
let epoch_length = epoch_config.epoch_length(consensus_config.base_period_length());
let config = super::Config {
epoch_config: EpochConfig {
epoch_stake_distribution_stabilization: NonZero::new(3u8).unwrap(),
epoch_period_nonce_buffer: NonZero::new(3).unwrap(),
epoch_period_nonce_stabilization: NonZero::new(4).unwrap(),
},
consensus_config: lb_cryptarchia_engine::Config::new(
NonZero::new(5).unwrap(),
NonNegativeRatio::new(1, 2.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
),
epoch_config,
consensus_config,
sdp_config: crate::mantle::sdp::Config {
service_params: Arc::new(
[(
ServiceType::BlendNetwork,
ServiceParameters {
lock_period: 10,
inactivity_period: 1,
retention_period: 1,
timestamp: 0,
session_duration: 10,
lock_period: 10.into(),
inactivity_period: 1.into(),
retention_period: 1.into(),
epoch: 0.into(),
},
)]
.into(),
),
service_rewards_params: ServiceRewardsParameters {
blend: RewardsParameters {
rounds_per_session: NonZeroU64::new(10).unwrap(),
rounds_per_epoch: epoch_length.try_into().unwrap(),
message_frequency_per_round: NonNegativeF64::try_from(1.0).unwrap(),
num_blend_layers: NonZeroU64::new(3).unwrap(),
minimum_network_size: NonZeroU64::new(1).unwrap(),
@ -154,34 +163,37 @@ mod tests {
#[test]
fn slot_to_epoch() {
let epoch_config = EpochConfig {
epoch_stake_distribution_stabilization: NonZero::new(3u8).unwrap(),
epoch_period_nonce_buffer: NonZero::new(3).unwrap(),
epoch_period_nonce_stabilization: NonZero::new(4).unwrap(),
};
let consensus_config = lb_cryptarchia_engine::Config::new(
NonZero::new(5).unwrap(),
NonNegativeRatio::new(1, 2.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
);
let epoch_length = epoch_config.epoch_length(consensus_config.base_period_length());
let config = super::Config {
epoch_config: EpochConfig {
epoch_stake_distribution_stabilization: NonZero::new(3u8).unwrap(),
epoch_period_nonce_buffer: NonZero::new(3).unwrap(),
epoch_period_nonce_stabilization: NonZero::new(4).unwrap(),
},
consensus_config: lb_cryptarchia_engine::Config::new(
NonZero::new(5).unwrap(),
NonNegativeRatio::new(1, 2.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
),
epoch_config,
consensus_config,
sdp_config: crate::mantle::sdp::Config {
service_params: Arc::new(
[(
ServiceType::BlendNetwork,
ServiceParameters {
lock_period: 10,
inactivity_period: 1,
retention_period: 1,
timestamp: 0,
session_duration: 10,
lock_period: 10.into(),
inactivity_period: 1.into(),
retention_period: 1.into(),
epoch: 0.into(),
},
)]
.into(),
),
service_rewards_params: ServiceRewardsParameters {
blend: RewardsParameters {
rounds_per_session: NonZeroU64::new(10).unwrap(),
rounds_per_epoch: epoch_length.try_into().unwrap(),
message_frequency_per_round: NonNegativeF64::try_from(1.0).unwrap(),
num_blend_layers: NonZeroU64::new(3).unwrap(),
minimum_network_size: NonZeroU64::new(1).unwrap(),
@ -196,9 +208,9 @@ mod tests {
},
faucet_pk: None,
};
assert_eq!(config.epoch(1.into()), 0.into());
assert_eq!(config.epoch(100.into()), 1.into());
assert_eq!(config.epoch(101.into()), 1.into());
assert_eq!(config.epoch(200.into()), 2.into());
assert_eq!(config.epoch(1.into()), 0);
assert_eq!(config.epoch(100.into()), 1);
assert_eq!(config.epoch(101.into()), 1);
assert_eq!(config.epoch(200.into()), 2);
}
}

View File

@ -98,7 +98,7 @@ mod tests {
service_params: Arc::new(HashMap::new()),
service_rewards_params: ServiceRewardsParameters {
blend: RewardsParameters {
rounds_per_session: 10.try_into().unwrap(),
rounds_per_epoch: 10.try_into().unwrap(),
message_frequency_per_round: 1.0.try_into().unwrap(),
num_blend_layers: 3.try_into().unwrap(),
minimum_network_size: 1.try_into().unwrap(),

View File

@ -22,9 +22,12 @@ use lb_groth16::{Fr, fr_from_bytes};
use lb_key_management_system_keys::keys::ZkSignature;
use lb_utxotree::MerklePath;
use crate::cryptarchia::{
block_density::BlockDensity,
stake::{PRECISION, StakeInference},
use crate::{
cryptarchia::{
block_density::BlockDensity,
stake::{PRECISION, StakeInference},
},
mantle::sdp::SdpLedger,
};
// corresponds to the denominator of q
@ -51,7 +54,7 @@ pub type UtxoTree = lb_utxotree::UtxoTree<NoteId, Utxo, ZkHasher>;
use super::{Balance, Config, LedgerError, mantle};
use crate::WINDOW_SIZE;
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct EpochState {
/// The epoch this snapshot is for
pub epoch: Epoch,
@ -69,10 +72,23 @@ pub struct EpochState {
pub lottery_0: Fr,
#[serde(with = "lb_groth16::serde::serde_fr")]
pub lottery_1: Fr,
/// SDP service-provider declarations (membership) snapshot, frozen at the
/// same slot as the stake distribution (`stake_distribution_snapshot`).
/// Carried forward in ledger state so the epoch's membership is derivable
/// from the tip without walking storage.
///
/// TODO: This field reaches "up" into the mantle layer (`SdpLedger`), which
/// is why the `&SdpLedger` is threaded through `update_from_ledger`,
/// `update_epoch_state`, `try_apply_header`, and `epoch_state_for_slot`.
/// That threading is scaffolding: long-term, `EpochState` should be lifted
/// out of `cryptarchia` to sit alongside `cryptarchia_ledger` and
/// `mantle_ledger` in the outer `LedgerState`, where the freeze can read
/// both sub-ledgers directly and these `&SdpLedger` parameters disappear.
pub sdp: SdpLedger,
}
impl EpochState {
fn update_from_ledger(self, ledger: &LedgerState, config: &Config) -> Self {
fn update_from_ledger(self, ledger: &LedgerState, sdp: &SdpLedger, config: &Config) -> Self {
let nonce_snapshot_slot = config.nonce_snapshot(self.epoch);
let nonce = if ledger.slot < nonce_snapshot_slot {
ledger.nonce
@ -80,11 +96,14 @@ impl EpochState {
self.nonce
};
// The SDP membership snapshot is frozen at the same slot as the stake
// distribution, so the two halves of the epoch's public info stay
// consistent.
let stake_snapshot_slot = config.stake_distribution_snapshot(self.epoch);
let utxos = if ledger.slot < stake_snapshot_slot {
ledger.utxos.clone()
let (utxos, sdp) = if ledger.slot < stake_snapshot_slot {
(ledger.utxos.clone(), sdp.clone())
} else {
self.utxos
(self.utxos, self.sdp)
};
Self {
epoch: self.epoch,
@ -93,6 +112,7 @@ impl EpochState {
total_stake: self.total_stake,
lottery_0: self.lottery_0,
lottery_1: self.lottery_1,
sdp,
}
}
@ -136,7 +156,7 @@ impl EpochState {
/// NOTE: Most collection fields in this struct should use `rpds`
/// since we keep a copy of this state for each block.
#[derive(Derivative, serde::Serialize, serde::Deserialize)]
#[derivative(Clone, Eq, PartialEq)]
#[derivative(Clone, PartialEq)]
pub struct LedgerState {
// All available Unspent Transtaction Outputs (UTXOs) at the current slot
// TODO: move UTXOs in the mantle ledger. There is no reason to keep them here
@ -178,7 +198,12 @@ impl LedgerState {
clippy::too_many_lines,
reason = "TODO: fix/refactor updating next_epoch_state"
)]
fn update_epoch_state<Id>(self, slot: Slot, config: &Config) -> Result<Self, LedgerError<Id>> {
fn update_epoch_state<Id>(
self,
slot: Slot,
sdp: &SdpLedger,
config: &Config,
) -> Result<Self, LedgerError<Id>> {
if slot <= self.slot {
return Err(LedgerError::InvalidSlot {
parent: self.slot,
@ -197,7 +222,7 @@ impl LedgerState {
let next_epoch_state = self
.next_epoch_state
.clone()
.update_from_ledger(&self, config);
.update_from_ledger(&self, sdp, config);
// There are 3 cases to consider:
// 1. We are in the same epoch as the parent state: Update the next epoch state
@ -253,6 +278,7 @@ impl LedgerState {
total_stake,
lottery_0,
lottery_1,
sdp: sdp.clone(),
};
let (new_price, new_ema) = update_storage_market(
self.storage_gas_price,
@ -316,6 +342,7 @@ impl LedgerState {
total_stake,
lottery_0,
lottery_1,
sdp: sdp.clone(),
};
let next_epoch_state = EpochState {
epoch: new_epoch + 1,
@ -324,6 +351,7 @@ impl LedgerState {
total_stake,
lottery_0,
lottery_1,
sdp: sdp.clone(),
};
Ok(Self {
slot,
@ -390,6 +418,7 @@ impl LedgerState {
self,
slot: Slot,
proof: &LeaderProof,
sdp: &SdpLedger,
config: &Config,
) -> Result<Self, LedgerError<Id>>
where
@ -399,7 +428,7 @@ impl LedgerState {
// Then, apply the proof and update the nonce. Finally, increment block density
// since this function is called for a new block.
Ok(self
.update_epoch_state(slot, config)?
.update_epoch_state(slot, sdp, config)?
.try_apply_proof(slot, proof, config)?
.update_nonce(&proof.entropy(), slot)
.increment_block_density(slot))
@ -491,6 +520,20 @@ impl LedgerState {
&self.next_epoch_state
}
/// Seeds the genesis epoch-state snapshots with the genesis SDP ledger.
///
/// At genesis the cryptarchia ledger is built before the mantle `SdpLedger`
/// exists (the mantle ledger is derived from the cryptarchia epoch state),
/// so the initial epoch states start with an empty SDP snapshot. Once the
/// genesis `SdpLedger` is available, this seeds it into the epoch 0/1
/// snapshots so they carry the genesis membership. Genesis use only.
#[must_use]
pub fn with_genesis_sdp(mut self, sdp: SdpLedger) -> Self {
self.next_epoch_state.sdp = sdp.clone();
self.epoch_state.sdp = sdp;
self
}
#[must_use]
pub const fn latest_utxos(&self) -> &UtxoTree {
&self.utxos
@ -526,11 +569,12 @@ impl LedgerState {
pub fn epoch_state_for_slot<Id>(
&self,
slot: Slot,
sdp: &SdpLedger,
config: &Config,
) -> Result<EpochState, LedgerError<Id>> {
Ok(self
.clone()
.update_epoch_state(slot, config)?
.update_epoch_state(slot, sdp, config)?
.epoch_state()
.clone())
}
@ -591,6 +635,7 @@ impl LedgerState {
total_stake,
lottery_0,
lottery_1,
sdp: SdpLedger::new(1.into()),
},
epoch_state: EpochState {
epoch: 0.into(),
@ -599,6 +644,7 @@ impl LedgerState {
total_stake,
lottery_0,
lottery_1,
sdp: SdpLedger::new(0.into()),
},
block_density,
stake_inference,
@ -770,7 +816,7 @@ pub mod tests {
.unwrap()
.clone()
.cryptarchia_ledger
.update_epoch_state::<HeaderId>(slot, ledger.config())
.update_epoch_state::<HeaderId>(slot, &SdpLedger::new(0.into()), ledger.config())
.unwrap();
let id = make_id(parent, slot, utxo);
let proof = generate_proof(&ledger_state, &utxo, slot);
@ -829,30 +875,33 @@ pub mod tests {
service_params.insert(
lb_core::sdp::ServiceType::BlendNetwork,
ServiceParameters {
lock_period: 10,
inactivity_period: 1,
retention_period: 1,
timestamp: 0,
session_duration: 10,
lock_period: 10.into(),
inactivity_period: 1.into(),
retention_period: 1.into(),
epoch: 0.into(),
},
);
let epoch_config = EpochConfig {
epoch_stake_distribution_stabilization: NonZero::new(3).unwrap(),
epoch_period_nonce_buffer: NonZero::new(3).unwrap(),
epoch_period_nonce_stabilization: NonZero::new(4).unwrap(),
};
let consensus_config = lb_cryptarchia_engine::Config::new(
NonZero::new(1).unwrap(),
NonNegativeRatio::new(1, 10.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
);
let epoch_length = epoch_config.epoch_length(consensus_config.base_period_length());
Config {
epoch_config: EpochConfig {
epoch_stake_distribution_stabilization: NonZero::new(3).unwrap(),
epoch_period_nonce_buffer: NonZero::new(3).unwrap(),
epoch_period_nonce_stabilization: NonZero::new(4).unwrap(),
},
consensus_config: lb_cryptarchia_engine::Config::new(
NonZero::new(1).unwrap(),
NonNegativeRatio::new(1, 10.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
),
epoch_config,
consensus_config,
sdp_config: mantle::sdp::Config {
service_params: Arc::new(service_params),
service_rewards_params: ServiceRewardsParameters {
blend: rewards::blend::RewardsParameters {
rounds_per_session: NonZeroU64::new(10).unwrap(),
rounds_per_epoch: epoch_length.try_into().unwrap(),
message_frequency_per_round: NonNegativeF64::try_from(1.0).unwrap(),
num_blend_layers: NonZeroU64::new(3).unwrap(),
minimum_network_size: NonZeroU64::new(1).unwrap(),
@ -898,6 +947,7 @@ pub mod tests {
total_stake,
lottery_0,
lottery_1,
sdp: SdpLedger::new(1.into()),
},
epoch_state: EpochState {
epoch: 0.into(),
@ -906,6 +956,7 @@ pub mod tests {
total_stake,
lottery_0,
lottery_1,
sdp: SdpLedger::new(0.into()),
},
stake_inference,
fee_window: [0.into(); 120],
@ -946,11 +997,13 @@ pub mod tests {
// we still don't have transactions, so the only way to add a commitment to
// spendable utxos and test epoch snapshotting is by doing this
// manually
let mut block_state = ledger.states[&id].clone().cryptarchia_ledger;
block_state.utxos = block_state.utxos.insert(utxo_add.id(), utxo_add).0;
ledger
.states
.insert(id, full_ledger_state(block_state, &ledger.config));
let block_ledger = ledger.states.get_mut(&id).unwrap();
let new_utxos = block_ledger
.cryptarchia_ledger
.utxos
.insert(utxo_add.id(), utxo_add)
.0;
block_ledger.cryptarchia_ledger.utxos = new_utxos;
id
}
@ -994,10 +1047,7 @@ pub mod tests {
);
let h_1 = update_ledger(&mut ledger, genesis, 10, utxos[0]).unwrap();
assert_eq!(
ledger.states[&h_1].cryptarchia_ledger.epoch_state.epoch,
0.into()
);
assert_eq!(ledger.states[&h_1].cryptarchia_ledger.epoch_state.epoch, 0);
let h_2 = update_ledger(&mut ledger, h_1, 60, utxos[1]).unwrap();
@ -1006,8 +1056,8 @@ pub mod tests {
// Epoch jump: epoch 0 -> 2
// Jump to the slot that is not the 1st slot of epoch 2
let h_4 = update_ledger(&mut ledger, h_3, 222, utxos[3]).unwrap();
// nonce for epoch 2 should be taken at the end of slot 160, but in our case the
// last block is at slot 90 because of epoch jumps
// nonce for epoch 2 should be taken at the end of slot 160, but in our case
// the last block is at slot 90 because of epoch jumps
assert_eq!(
ledger.states[&h_4].cryptarchia_ledger.epoch_state.nonce,
ledger.states[&h_3].cryptarchia_ledger.nonce,
@ -1102,7 +1152,12 @@ pub mod tests {
// EPOCH 2
// the utxo is finally eligible 2 epochs after it was first minted
update_ledger(&mut ledger, h_0_1, 2 * epoch_length, utxo_1).unwrap();
//
// First, advance to epoch 1 using the `utxo` in genesis,
// because SDP ledger doesn't support epoch jumps yet.
let h_1_1 = update_ledger(&mut ledger, h_0_1, epoch_length, utxo).unwrap();
// Then, try to advance to epoch 2 using `utxo_1`
update_ledger(&mut ledger, h_1_1, 2 * epoch_length, utxo_1).unwrap();
}
/// Verifies that the TSI chain is computed correctly across epoch
@ -1148,10 +1203,7 @@ pub mod tests {
// 159]
let h5 = update_ledger(&mut ledger, h4, 100, utxo).unwrap();
let ts1 = inference.total_stake_inference::<PRECISION>(ts_genesis, 3);
assert_eq!(
ledger.states[&h5].cryptarchia_ledger.epoch_state.epoch,
1.into()
);
assert_eq!(ledger.states[&h5].cryptarchia_ledger.epoch_state.epoch, 1);
assert_eq!(
ledger.states[&h5]
.cryptarchia_ledger
@ -1174,10 +1226,7 @@ pub mod tests {
// Epoch 1 -> 2 transition --------------------
let h7 = update_ledger(&mut ledger, h6, 200, utxo).unwrap();
let ts2 = inference.total_stake_inference::<PRECISION>(ts1, 2);
assert_eq!(
ledger.states[&h7].cryptarchia_ledger.epoch_state.epoch,
2.into()
);
assert_eq!(ledger.states[&h7].cryptarchia_ledger.epoch_state.epoch, 2);
assert_eq!(
ledger.states[&h7]
.cryptarchia_ledger
@ -1198,12 +1247,12 @@ pub mod tests {
let slot = Slot::genesis() + 10;
let ledger_state2 = ledger_state
.cryptarchia_ledger
.update_epoch_state::<HeaderId>(slot, ledger_config)
.update_epoch_state::<HeaderId>(slot, &SdpLedger::new(0.into()), ledger_config)
.expect("Ledger needs to move forward");
let slot2 = Slot::genesis() + 1;
let update_epoch_err = ledger_state2
.update_epoch_state::<HeaderId>(slot2, ledger_config)
.update_epoch_state::<HeaderId>(slot2, &SdpLedger::new(0.into()), ledger_config)
.err();
// Time cannot flow backwards
@ -1562,25 +1611,25 @@ pub mod tests {
// Genesis state is at epoch 0, with epoch_state for epoch 0 and
// next_epoch_state for epoch 1
assert_eq!(ledger_state.epoch_state.epoch, 0.into());
assert_eq!(ledger_state.next_epoch_state.epoch, 1.into());
assert_eq!(ledger_state.epoch_state.epoch, 0);
assert_eq!(ledger_state.next_epoch_state.epoch, 1);
let initial_total_stake = ledger_state.epoch_state.total_stake;
// Query for epoch 0 (current epoch) - should return epoch_state
let epoch_0_slot: Slot = (epoch_length - 1).into();
let epoch_0_state = ledger_state
.epoch_state_for_slot::<HeaderId>(epoch_0_slot, &config)
.epoch_state_for_slot::<HeaderId>(epoch_0_slot, &SdpLedger::new(0.into()), &config)
.expect("Should return epoch state for current epoch");
assert_eq!(epoch_0_state.epoch, 0.into());
assert_eq!(epoch_0_state.epoch, 0);
assert_eq!(epoch_0_state.total_stake, initial_total_stake);
// Query for epoch 1
// Since epoch 0 has no block, total stake should be reduced
let epoch_1_slot: Slot = (epoch_length + 1).into();
let epoch_1_state = ledger_state
.epoch_state_for_slot::<HeaderId>(epoch_1_slot, &config)
.epoch_state_for_slot::<HeaderId>(epoch_1_slot, &SdpLedger::new(0.into()), &config)
.expect("Should return epoch state for next epoch");
assert_eq!(epoch_1_state.epoch, 1.into());
assert_eq!(epoch_1_state.epoch, 1);
// With 0 density and LEARNING_RATE=1, total stake drops to minimum (1)
assert_eq!(
epoch_1_state.total_stake, 1,
@ -1590,9 +1639,9 @@ pub mod tests {
// Query for epoch 3 (multiple skipped epochs) - stake stays at minimum
let epoch_2_slot: Slot = (2 * epoch_length + 1).into();
let epoch_2_state = ledger_state
.epoch_state_for_slot::<HeaderId>(epoch_2_slot, &config)
.epoch_state_for_slot::<HeaderId>(epoch_2_slot, &SdpLedger::new(0.into()), &config)
.expect("Should synthesize epoch state for skipped epoch");
assert_eq!(epoch_2_state.epoch, 2.into());
assert_eq!(epoch_2_state.epoch, 2);
assert_eq!(
epoch_2_state.total_stake, 1,
"Total stake should remain at minimum"
@ -1613,20 +1662,25 @@ pub mod tests {
// First, apply a header from epoch 0 to increase block density
let slot = Slot::from(1);
assert_eq!(config.epoch(slot), 0.into());
assert_eq!(config.epoch(slot), 0);
let proof = generate_proof(&genesis_state, &utxo, slot);
let ledger_state_1 = genesis_state
.try_apply_header::<DummyProof, HeaderId>(slot, &proof, &config)
.try_apply_header::<DummyProof, HeaderId>(
slot,
&proof,
&SdpLedger::new(0.into()),
&config,
)
.unwrap();
// Now, apply a header from the 2nd slot of epoch 2
let slot = Slot::from(config.epoch_length() * 2 + 1);
assert_eq!(config.epoch(slot), 2.into());
assert_eq!(config.epoch(slot), 2);
// First, synthesize epoch state for epoch 2
let synthesized_ledger_state = ledger_state_1
.clone()
.update_epoch_state::<HeaderId>(slot, &config)
.update_epoch_state::<HeaderId>(slot, &SdpLedger::new(0.into()), &config)
.unwrap();
assert_eq!(synthesized_ledger_state.slot, slot);
@ -1638,11 +1692,16 @@ pub mod tests {
// correctly to epoch 2 as the same as `synthesized_ledger_state`.
let ledger_state_2 = ledger_state_1
.clone()
.try_apply_header::<DummyProof, HeaderId>(slot, &proof, &config)
.try_apply_header::<DummyProof, HeaderId>(
slot,
&proof,
&SdpLedger::new(0.into()),
&config,
)
.unwrap();
assert_eq!(ledger_state_2.slot, slot);
assert_ne!(ledger_state_2.nonce, ledger_state_1.nonce); // advanced
assert_eq!(ledger_state_2.epoch_state.epoch, 2.into());
assert_eq!(ledger_state_2.epoch_state.epoch, 2);
}
fn stake_inference_from_config(config: &Config) -> StakeInference {

View File

@ -185,7 +185,7 @@ mod tests {
service_params: Arc::new(HashMap::new()),
service_rewards_params: ServiceRewardsParameters {
blend: blend::RewardsParameters {
rounds_per_session: 10.try_into().unwrap(),
rounds_per_epoch: 10.try_into().unwrap(),
message_frequency_per_round: 1.0.try_into().unwrap(),
num_blend_layers: 1.try_into().unwrap(),
data_replication_factor: 0,

View File

@ -28,7 +28,6 @@ use lb_core::{
tx::{GasPrices, MantleTxContext, MantleTxGasContext},
},
proofs::leader_proof,
sdp::{Declaration, DeclarationId, ProviderId, ProviderInfo, ServiceType, SessionNumber},
};
use lb_cryptarchia_engine::Slot;
use lb_groth16::{Field as _, Fr};
@ -242,10 +241,20 @@ impl LedgerState {
where
LeaderProof: leader_proof::LeaderProof,
{
let last_epoch_state = self.cryptarchia_ledger.epoch_state().clone();
let mut cryptarchia_ledger = self
.cryptarchia_ledger
.try_apply_header::<LeaderProof, Id>(slot, proof, config)?;
.try_apply_header::<LeaderProof, Id>(
slot,
proof,
// TODO: threading SDP here because EpochState is currently embedded in
// CryptarchiaLedger.
// In the future, we will pull EpochState up into LedgerState.
&self.mantle_ledger.sdp,
config,
)?;
let (mantle_ledger, reward_utxos) = self.mantle_ledger.try_apply_header(
&last_epoch_state,
cryptarchia_ledger.epoch_state(),
*proof.voucher_cm(),
config,
@ -418,6 +427,9 @@ impl LedgerState {
pub fn from_utxos(utxos: impl IntoIterator<Item = Utxo>, config: &Config) -> Self {
let cryptarchia_ledger = CryptarchiaLedger::from_utxos(utxos, config, Fr::ZERO);
let mantle_ledger = MantleLedger::new(config, cryptarchia_ledger.epoch_state());
// Seed the genesis epoch-state membership snapshots from the genesis SDP
// ledger, which only exists after the mantle ledger is built.
let cryptarchia_ledger = cryptarchia_ledger.with_genesis_sdp(mantle_ledger.sdp.clone());
Self {
block_number: 0,
cryptarchia_ledger,
@ -437,6 +449,9 @@ impl LedgerState {
cryptarchia_ledger.latest_utxos(),
cryptarchia_ledger.epoch_state(),
)?;
// Seed the genesis epoch-state membership snapshots from the genesis SDP
// ledger (which carries the genesis declarations applied above).
let cryptarchia_ledger = cryptarchia_ledger.with_genesis_sdp(mantle_ledger.sdp.clone());
Ok((
Self {
block_number: 0,
@ -474,7 +489,8 @@ impl LedgerState {
slot: Slot,
config: &Config,
) -> Result<EpochState, LedgerError<Id>> {
self.cryptarchia_ledger.epoch_state_for_slot(slot, config)
self.cryptarchia_ledger
.epoch_state_for_slot(slot, &self.mantle_ledger.sdp, config)
}
#[must_use]
@ -492,24 +508,6 @@ impl LedgerState {
&self.mantle_ledger
}
#[must_use]
pub fn sdp_declarations(&self) -> Vec<(DeclarationId, Declaration)> {
self.mantle_ledger.sdp_declarations()
}
#[must_use]
pub fn active_session_providers(
&self,
service_type: ServiceType,
) -> Option<HashMap<ProviderId, ProviderInfo>> {
self.mantle_ledger.active_session_providers(service_type)
}
#[must_use]
pub fn active_sessions(&self) -> HashMap<ServiceType, SessionNumber> {
self.mantle_ledger.active_sessions()
}
#[must_use]
pub fn tx_context(&self) -> MantleTxContext {
MantleTxContext {
@ -785,6 +783,18 @@ mod tests {
(ledger, [0; 32], utxo)
}
/// The genesis epoch-state membership snapshots must be seeded from the
/// genesis SDP ledger, not left as the empty `SdpLedger::new` placeholder
/// the cryptarchia genesis constructor initializes them with.
#[test]
fn genesis_seeds_epoch_state_sdp_from_mantle() {
let config = config();
let ledger = LedgerState::from_utxos([utxo()], &config);
assert_eq!(ledger.epoch_state().sdp, ledger.mantle_ledger.sdp);
assert_eq!(ledger.next_epoch_state().sdp, ledger.mantle_ledger.sdp);
}
fn create_test_keys_with_seed(seed: u8) -> (Ed25519Key, Ed25519PublicKey) {
let signing_key = Ed25519Key::from_bytes(&[seed; 32]);
let verifying_key = signing_key.public_key();

View File

@ -269,20 +269,20 @@ mod tests {
fn test_epoch_transition() {
let state = LeaderState::new();
let state = state.try_apply_header(1.into(), Fr::ZERO.into()).unwrap();
assert_eq!(state.epoch, 1.into());
assert_eq!(state.epoch, 1);
assert_eq!(state.vouchers_snapshot.count, 0);
let state = state.try_apply_header(2.into(), Fr::ONE.into()).unwrap();
assert_eq!(state.epoch, 2.into());
assert_eq!(state.epoch, 2);
assert_eq!(state.vouchers_snapshot.count, 1);
let state = state
.try_apply_header(2.into(), Fr::from(2u64).into())
.unwrap();
assert_eq!(state.epoch, 2.into());
assert_eq!(state.epoch, 2);
assert_eq!(state.vouchers_snapshot.count, 1);
let state = state
.try_apply_header(3.into(), Fr::from(3u64).into())
.unwrap();
assert_eq!(state.epoch, 3.into());
assert_eq!(state.epoch, 3);
assert_eq!(state.vouchers_snapshot.count, 3);
let err = state
.clone()
@ -298,7 +298,7 @@ mod tests {
let state = state
.try_apply_header(4.into(), Fr::from(5u64).into())
.unwrap();
assert_eq!(state.epoch, 4.into());
assert_eq!(state.epoch, 4);
assert_eq!(state.vouchers_snapshot.count, 4);
}

View File

@ -3,8 +3,6 @@ pub mod helpers;
pub mod leader;
pub mod sdp;
use std::collections::HashMap;
use lb_core::{
crypto::ZkHasher,
events::Events,
@ -26,10 +24,7 @@ use lb_core::{
},
},
proofs::channel_multi_sig_proof::ChannelMultiSigProof,
sdp::{
Declaration, DeclarationId, ProviderId, ProviderInfo, ServiceType, SessionNumber,
locked_notes::LockedNotes,
},
sdp::locked_notes::LockedNotes,
};
use lb_cryptarchia_engine::Slot;
use lb_key_management_system_keys::keys::{Ed25519Signature, ZkSignature};
@ -73,7 +68,7 @@ impl LedgerState {
pub fn new(config: &Config, epoch_state: &EpochState) -> Self {
Self {
channels: channel::Channels::new(),
sdp: sdp::SdpLedger::new()
sdp: sdp::SdpLedger::new(epoch_state.epoch())
.with_blend_service(&config.sdp_config.service_rewards_params.blend, epoch_state),
leaders: leader::LeaderState::new(),
}
@ -128,24 +123,6 @@ impl LedgerState {
Self { channels, ..self }
}
#[must_use]
pub fn active_session_providers(
&self,
service_type: ServiceType,
) -> Option<HashMap<ProviderId, ProviderInfo>> {
self.sdp.active_session_providers(service_type)
}
#[must_use]
pub fn active_sessions(&self) -> HashMap<ServiceType, SessionNumber> {
self.sdp.active_sessions()
}
#[must_use]
pub fn sdp_declarations(&self) -> Vec<(DeclarationId, Declaration)> {
self.sdp.declarations()
}
/// Get the root of the voucher commitments snapshot.
#[must_use]
pub const fn vouchers_snapshot_root(&self) -> RewardsRoot {
@ -165,12 +142,15 @@ impl LedgerState {
pub fn try_apply_header(
mut self,
last_epoch_state: &EpochState,
epoch_state: &EpochState,
voucher: VoucherCm,
config: &Config,
) -> Result<(Self, Vec<Utxo>), Error> {
self.leaders = self.leaders.try_apply_header(epoch_state.epoch, voucher)?;
let (new_sdp, reward_utxos) = self.sdp.try_apply_header(&config.sdp_config, epoch_state)?;
let (new_sdp, reward_utxos) =
self.sdp
.try_apply_header(&config.sdp_config, last_epoch_state, epoch_state)?;
self.sdp = new_sdp;
Ok((self, reward_utxos))
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,213 @@
use lb_blend_crypto::merkle::sort_nodes_and_build_merkle_tree;
use lb_blend_message::{
crypto::proofs::PoQVerificationInputsMinusSigningKey,
encap::ProofsVerifier as ProofsVerifierTrait, reward::EpochRandomness,
};
use lb_blend_proofs::quota::inputs::prove::public::{CoreInputs, LeaderInputs};
use lb_core::{
crypto::ZkHash,
mantle::Value,
sdp::{Declaration, ProviderId, ServiceType},
};
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::ZkPublicKey;
use rpds::HashTrieMapSync;
use tracing::debug;
use crate::{
EpochState,
mantle::sdp::rewards::blend::{LOG_TARGET, RewardsParameters, target_epoch::TargetEpochState},
};
/// Immutable state of the current epoch.
/// The epoch is `E` if `E-1` is the target epoch for which rewards
/// are being calculated.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CurrentEpochState {
epoch: Epoch,
/// Current epoch randomness
epoch_randomness: EpochRandomness,
/// The leader input derived from the current epoch state.
/// These will be used to create the proof verifier after the next
/// epoch update.
leader_input: LeaderInputs,
}
impl CurrentEpochState {
pub fn new(epoch_state: &EpochState, settings: &RewardsParameters) -> Self {
Self {
epoch: epoch_state.epoch(),
epoch_randomness: (*epoch_state.nonce()).into(),
leader_input: settings.leader_inputs(epoch_state),
}
}
pub const fn epoch(&self) -> Epoch {
self.epoch
}
pub const fn epoch_randomness(&self) -> EpochRandomness {
self.epoch_randomness
}
}
/// Collects income for the current epoch.
/// The current epoch is `E` if `E-1` is the target epoch for which rewards
/// are being calculated.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CurrentEpochTracker {
/// Collecting service rewards over the epoch
epoch_income: Value,
}
impl CurrentEpochTracker {
pub fn new() -> Self {
Self {
epoch_income: Value::default(),
}
}
pub(crate) const fn add_block_rewards(&self, block_rewards: Value) -> Self {
Self {
epoch_income: self.epoch_income + block_rewards,
}
}
/// Finalizes the current epoch tracker.
///
/// It returns [`CurrentEpochTrackerOutput::WithTargetEpoch`] by
/// creating a [`TargetEpochState`] using the collected information,
/// if the network size of the new target epoch is not below the
/// minimum required. Otherwise, it returns
/// [`CurrentEpochTrackerOutput::WithoutTargetEpoch`].
pub fn finalize<ProofsVerifier>(
&self,
current_reward_epoch_state: &CurrentEpochState,
last_epoch_state: &EpochState,
next_epoch_state: &EpochState,
settings: &RewardsParameters,
) -> CurrentEpochTrackerOutput<ProofsVerifier>
where
ProofsVerifier: ProofsVerifierTrait,
{
assert_eq!(
last_epoch_state.epoch,
current_reward_epoch_state.epoch(),
"last_epoch_state.epoch({}) must match current_reward_epoch_state.epoch({})",
last_epoch_state.epoch,
current_reward_epoch_state.epoch(),
);
assert!(
last_epoch_state.epoch < next_epoch_state.epoch,
"next_epoch_state.epoch({}) must be greater than last_epoch_state.epoch({})",
next_epoch_state.epoch,
last_epoch_state.epoch,
);
let declarations = last_epoch_state
.sdp
.declarations()
.iter()
.filter(|(service_type, _)| matches!(service_type, ServiceType::BlendNetwork))
.flat_map(|(_, declarations)| declarations.values())
.cloned()
.collect::<Vec<_>>();
if declarations.len() < settings.minimum_network_size.get() as usize {
debug!(target: LOG_TARGET, "Declaration count({}) is below minimum network size({}). Switching to WithoutTargetEpoch mode",
declarations.len(),
settings.minimum_network_size.get()
);
return CurrentEpochTrackerOutput::WithoutTargetEpoch {
current_epoch_state: CurrentEpochState::new(next_epoch_state, settings),
current_epoch_tracker: Self::new(),
};
}
let (providers, zk_root) = Self::providers_and_zk_root(declarations.iter());
let (core_quota, token_evaluation) = settings.core_quota_and_token_evaluation(
providers.size() as u64,
).expect("evaluation parameters shouldn't overflow. panicking since we can't process the new epoch");
let proof_verifier = Self::create_proof_verifier(
current_reward_epoch_state.leader_input,
zk_root,
core_quota,
);
CurrentEpochTrackerOutput::WithTargetEpoch {
target_epoch_state: TargetEpochState::new(
last_epoch_state.epoch(),
providers,
token_evaluation,
proof_verifier,
self.epoch_income,
),
current_epoch_state: CurrentEpochState::new(next_epoch_state, settings),
current_epoch_tracker: Self::new(),
}
}
#[expect(single_use_lifetimes, reason = "lifetime is required")]
fn providers_and_zk_root<'d>(
declarations: impl Iterator<Item = &'d Declaration>,
) -> (HashTrieMapSync<ProviderId, (ZkPublicKey, u64)>, ZkHash) {
let mut providers = declarations
.map(|declaration| (declaration.provider_id, declaration.zk_id))
.collect::<Vec<_>>();
let zk_root =
sort_nodes_and_build_merkle_tree(&mut providers, |(_, zk_id)| zk_id.into_inner())
.expect("Should not fail to build merkle tree of core nodes' zk public keys")
.root();
let providers = providers
.into_iter()
.enumerate()
.map(|(i, (provider_id, zk_id))| {
(
provider_id,
(
zk_id,
u64::try_from(i).expect("provider index must fit in u64"),
),
)
})
.collect();
(providers, zk_root)
}
fn create_proof_verifier<ProofsVerifier: ProofsVerifierTrait>(
leader_input: LeaderInputs,
zk_root: ZkHash,
core_quota: u64,
) -> ProofsVerifier {
ProofsVerifier::new(PoQVerificationInputsMinusSigningKey {
core: CoreInputs {
zk_root,
quota: core_quota,
},
leader: leader_input,
})
}
}
/// Result of finalizing the [`CurrentEpochTracker`].
pub enum CurrentEpochTrackerOutput<ProofsVerifier> {
/// Target epoch has been built with the information collected by
/// the current epoch tracker.
/// Also, the new current epoch state and tracker have been initialized.
WithTargetEpoch {
target_epoch_state: TargetEpochState<ProofsVerifier>,
current_epoch_state: CurrentEpochState,
current_epoch_tracker: CurrentEpochTracker,
},
/// No target epoch has been built because the network size in the
/// epoch is below the minimum required.
WithoutTargetEpoch {
current_epoch_state: CurrentEpochState,
current_epoch_tracker: CurrentEpochTracker,
},
}

View File

@ -1,213 +0,0 @@
use lb_blend_crypto::merkle::sort_nodes_and_build_merkle_tree;
use lb_blend_message::{
crypto::proofs::PoQVerificationInputsMinusSigningKey,
encap::ProofsVerifier as ProofsVerifierTrait, reward::SessionRandomness,
};
use lb_blend_proofs::quota::inputs::prove::public::{CoreInputs, LeaderInputs};
use lb_core::{
crypto::ZkHash,
mantle::Value,
sdp::{ProviderId, SessionNumber},
};
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::ZkPublicKey;
use rpds::HashTrieMapSync;
use tracing::debug;
use crate::{
EpochState,
mantle::sdp::{
SessionState,
rewards::blend::{LOG_TARGET, RewardsParameters, target_session::TargetSessionState},
},
};
/// Immutable state of the current session.
/// The current session is `s` if `s-1` is the target session for which rewards
/// are being calculated.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CurrentSessionState {
/// Current session randomness
session_randomness: SessionRandomness,
}
impl CurrentSessionState {
const fn new(session_randomness: SessionRandomness) -> Self {
Self { session_randomness }
}
pub const fn session_randomness(&self) -> SessionRandomness {
self.session_randomness
}
}
/// Collects epoch states seen in the current session.
/// The current session is `s` if `s-1` is the target session for which rewards
/// are being calculated.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CurrentSessionTracker {
/// Collecting leader inputs derived from epoch states seen in the current
/// session. These will be used to create proof verifiers after the next
/// session update.
leader_inputs: HashTrieMapSync<Epoch, LeaderInputs>,
/// Collecting service rewards over the session
session_income: Value,
}
impl CurrentSessionTracker {
pub fn new(first_epoch_state: &EpochState, settings: &RewardsParameters) -> Self {
Self {
leader_inputs: std::iter::once((
first_epoch_state.epoch,
settings.leader_inputs(first_epoch_state),
))
.collect(),
session_income: Value::default(),
}
}
pub fn collect_epoch(&self, epoch_state: &EpochState, settings: &RewardsParameters) -> Self {
Self {
leader_inputs: self
.leader_inputs
.insert(epoch_state.epoch, settings.leader_inputs(epoch_state)),
session_income: self.session_income,
}
}
pub(crate) fn add_block_rewards(&self, block_rewards: Value) -> Self {
Self {
leader_inputs: self.leader_inputs.clone(),
session_income: self.session_income + block_rewards,
}
}
/// Finalizes the current session tracker.
///
/// It returns [`CurrentSessionTrackerOutput::WithTargetSession`] by
/// creating a [`TargetSessionState`] using the collected information,
/// if the network size of the new target session is not below the
/// minimum required. Otherwise, it returns
/// [`CurrentSessionTrackerOutput::WithoutTargetSession`].
pub fn finalize<ProofsVerifier>(
&self,
last_active_session_state: &SessionState,
next_session_first_epoch_state: &EpochState,
settings: &RewardsParameters,
) -> CurrentSessionTrackerOutput<ProofsVerifier>
where
ProofsVerifier: ProofsVerifierTrait,
{
if last_active_session_state.declarations.size()
< settings.minimum_network_size.get() as usize
{
debug!(target: LOG_TARGET, "Declaration count({}) is below minimum network size({}). Switching to WithoutTargetSession mode",
last_active_session_state.declarations.size(),
settings.minimum_network_size.get()
);
return CurrentSessionTrackerOutput::WithoutTargetSession(Self::new(
next_session_first_epoch_state,
settings,
));
}
let (providers, zk_root) = Self::providers_and_zk_root(last_active_session_state);
let (core_quota, token_evaluation) = settings.core_quota_and_token_evaluation(
providers.size() as u64,
).expect("evaluation parameters shouldn't overflow. panicking since we can't process the new session");
let proof_verifiers = Self::create_proof_verifiers(
self.leader_inputs.values().copied(),
last_active_session_state.session_n,
zk_root,
core_quota,
);
CurrentSessionTrackerOutput::WithTargetSession {
target_session_state: TargetSessionState::new(
last_active_session_state.session_n,
providers,
token_evaluation,
proof_verifiers,
self.session_income,
),
current_session_state: CurrentSessionState::new(SessionRandomness::new(
last_active_session_state.session_n + 1,
&next_session_first_epoch_state.nonce,
)),
current_session_tracker: Self::new(next_session_first_epoch_state, settings),
}
}
fn providers_and_zk_root(
session_state: &SessionState,
) -> (HashTrieMapSync<ProviderId, (ZkPublicKey, u64)>, ZkHash) {
let mut providers = session_state
.declarations
.values()
.map(|declaration| (declaration.provider_id, declaration.zk_id))
.collect::<Vec<_>>();
let zk_root =
sort_nodes_and_build_merkle_tree(&mut providers, |(_, zk_id)| zk_id.into_inner())
.expect("Should not fail to build merkle tree of core nodes' zk public keys")
.root();
let providers = providers
.into_iter()
.enumerate()
.map(|(i, (provider_id, zk_id))| {
(
provider_id,
(
zk_id,
u64::try_from(i).expect("provider index must fit in u64"),
),
)
})
.collect();
(providers, zk_root)
}
fn create_proof_verifiers<ProofsVerifier: ProofsVerifierTrait>(
leader_inputs: impl Iterator<Item = LeaderInputs>,
session: SessionNumber,
zk_root: ZkHash,
core_quota: u64,
) -> Vec<ProofsVerifier> {
leader_inputs
.map(|leader| {
ProofsVerifier::new(PoQVerificationInputsMinusSigningKey {
session,
core: CoreInputs {
zk_root,
quota: core_quota,
},
leader,
})
})
.collect()
}
#[cfg(test)]
pub fn epoch_count(&self) -> usize {
self.leader_inputs.size()
}
}
/// Result of finalizing the [`CurrentSessionTracker`].
pub enum CurrentSessionTrackerOutput<ProofsVerifier> {
/// Target session has been built with the information collected by
/// the current session tracker.
/// Also, the new current session state and tracker have been initialized.
WithTargetSession {
target_session_state: TargetSessionState<ProofsVerifier>,
current_session_state: CurrentSessionState,
current_session_tracker: CurrentSessionTracker,
},
/// No target session has been built because the network size in the
/// session is below the minimum required.
WithoutTargetSession(CurrentSessionTracker),
}

File diff suppressed because it is too large Load Diff

View File

@ -6,57 +6,57 @@ use lb_blend_message::{
};
use lb_core::{
mantle::{Utxo, Value},
sdp::{ProviderId, ServiceType, SessionNumber},
sdp::{ProviderId, ServiceType},
};
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::ZkPublicKey;
use rpds::{HashTrieMapSync, HashTrieSetSync};
use crate::mantle::sdp::rewards::{
Error,
blend::{RewardsParameters, current_session::CurrentSessionState},
blend::{RewardsParameters, current_epoch::CurrentEpochState},
distribute_rewards,
};
/// The immutable state of the target session for which rewards are being
/// calculated. The target session is `s-1` if `s` is the current session.
/// The immutable state of the target epoch for which rewards are being
/// calculated. The target epoch is `E-1` if `E` is the current epoch.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct TargetSessionState<ProofsVerifier> {
/// The target session number
session_number: SessionNumber,
/// The providers in the target session (their public keys and indices).
pub struct TargetEpochState<ProofsVerifier> {
/// The target epoch
epoch: Epoch,
/// The providers in the target epoch (their public keys and indices).
providers: HashTrieMapSync<ProviderId, (ZkPublicKey, u64)>,
/// Parameters for evaluating activity proofs in the target session
/// Parameters for evaluating activity proofs in the target epoch
token_evaluation: BlendingTokenEvaluation,
/// Verifiers for `PoQ` and `PoSel`.
/// These are created from epoch states collected in the target session.
proof_verifiers: Vec<ProofsVerifier>,
/// Session incomes stabilized from session `s-1`
session_income: Value,
/// Verifier for `PoQ` and `PoSel` in the target epoch.
proof_verifier: ProofsVerifier,
/// Epoch incomes stabilized from epoch `E-1`
epoch_income: Value,
}
impl<ProofsVerifier> TargetSessionState<ProofsVerifier> {
impl<ProofsVerifier> TargetEpochState<ProofsVerifier> {
pub const fn new(
session_number: SessionNumber,
epoch: Epoch,
providers: HashTrieMapSync<ProviderId, (ZkPublicKey, u64)>,
token_evaluation: BlendingTokenEvaluation,
proof_verifiers: Vec<ProofsVerifier>,
session_income: Value,
proof_verifier: ProofsVerifier,
epoch_income: Value,
) -> Self {
Self {
session_number,
epoch,
providers,
token_evaluation,
proof_verifiers,
session_income,
proof_verifier,
epoch_income,
}
}
pub const fn session_number(&self) -> SessionNumber {
self.session_number
pub const fn epoch(&self) -> Epoch {
self.epoch
}
pub const fn session_income(&self) -> Value {
self.session_income
pub const fn epoch_income(&self) -> Value {
self.epoch_income
}
pub fn providers(&self) -> impl Iterator<Item = (&ProviderId, &(ZkPublicKey, u64))> {
@ -71,7 +71,7 @@ impl<ProofsVerifier> TargetSessionState<ProofsVerifier> {
}
}
impl<ProofsVerifier> TargetSessionState<ProofsVerifier>
impl<ProofsVerifier> TargetEpochState<ProofsVerifier>
where
ProofsVerifier: ProofsVerifierTrait,
{
@ -79,13 +79,13 @@ where
&self,
provider_id: &ProviderId,
proof: &lb_core::sdp::blend::ActivityProof,
current_session_state: &CurrentSessionState,
current_epoch_state: &CurrentEpochState,
settings: &RewardsParameters,
) -> Result<(ZkPublicKey, HammingDistance), Error> {
if proof.session != self.session_number {
return Err(Error::InvalidSession {
expected: self.session_number,
got: proof.session,
if proof.epoch != self.epoch {
return Err(Error::InvalidEpoch {
expected: self.epoch,
got: proof.epoch,
});
}
@ -99,29 +99,22 @@ where
.get(provider_id)
.ok_or_else(|| Error::UnknownProvider(Box::new(*provider_id)))?;
// Try to verify the proof with each of the available verifiers
let verified_proof = self
.proof_verifiers
.iter()
.find_map(|verifier| {
lb_blend_message::reward::ActivityProof::verify_and_build(
proof,
verifier,
index,
num_providers,
)
.ok()
})
.ok_or(Error::InvalidProof)?;
let verified_proof = lb_blend_message::reward::ActivityProof::verify_and_build(
proof,
&self.proof_verifier,
index,
num_providers,
)
.map_err(|_| Error::InvalidProof)?;
tracing::trace!(
"Verifying activity proof {:?} with session randomness: {:?}",
"Verifying activity proof {:?} with epoch randomness: {:?}",
verified_proof.token().signing_key(),
current_session_state.session_randomness()
current_epoch_state.epoch_randomness()
);
let Some(hamming_distance) = self.token_evaluation.evaluate(
verified_proof.token(),
current_session_state.session_randomness(),
current_epoch_state.epoch_randomness(),
) else {
return Err(Error::HammingDistanceTooLarge);
};
@ -130,23 +123,23 @@ where
}
}
/// Tracks activity proofs submitted for the target session whose rewards are
/// being calculated. The target session is `s-1` if `s` is the current session.
/// Tracks activity proofs submitted for the target epoch whose rewards are
/// being calculated. The target epoch is `E-1` if `E` is the current epoch.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct TargetSessionTracker {
/// Collecting proofs submitted by providers in the target session.
pub struct TargetEpochTracker {
/// Collecting proofs submitted by providers in the target epoch.
submitted_proofs: HashTrieMapSync<ProviderId, (ZkPublicKey, HammingDistance)>,
/// Tracking the minimum Hamming distance among submitted proofs.
min_hamming_distance: MinHammingDistance,
}
impl Default for TargetSessionTracker {
impl Default for TargetEpochTracker {
fn default() -> Self {
Self::new()
}
}
impl TargetSessionTracker {
impl TargetEpochTracker {
pub fn new() -> Self {
Self {
submitted_proofs: HashTrieMapSync::new_sync(),
@ -157,13 +150,13 @@ impl TargetSessionTracker {
pub fn insert(
&self,
provider_id: ProviderId,
session: SessionNumber,
epoch: Epoch,
zk_id: ZkPublicKey,
hamming_distance: HammingDistance,
) -> Result<Self, Error> {
if self.submitted_proofs.contains_key(&provider_id) {
return Err(Error::DuplicateActiveMessage {
session,
epoch,
provider_id: Box::new(provider_id),
});
}
@ -177,10 +170,9 @@ impl TargetSessionTracker {
})
}
pub fn finalize(
pub fn finalize<ProofsVerifier>(
&self,
session_number: SessionNumber,
session_income: u64,
target_epoch_state: &TargetEpochState<ProofsVerifier>,
) -> (Self, Vec<Utxo>) {
if self.submitted_proofs.is_empty() {
return (Self::new(), vec![]);
@ -190,7 +182,7 @@ impl TargetSessionTracker {
let premium_providers = &self.min_hamming_distance.providers;
// Calculate base reward
let base_reward = session_income
let base_reward = target_epoch_state.epoch_income()
/ (self.submitted_proofs.size() as u64 + premium_providers.size() as u64);
// Calculate reward for each provider
@ -206,7 +198,11 @@ impl TargetSessionTracker {
(
Self::new(),
distribute_rewards(rewards, session_number, ServiceType::BlendNetwork),
distribute_rewards(
rewards,
target_epoch_state.epoch(),
ServiceType::BlendNetwork,
),
)
}
}

View File

@ -5,16 +5,15 @@ mod test_utils;
use std::collections::HashMap;
use lb_core::{
block::BlockNumber,
codec::SerializeOp as _,
crypto::{Digest, Hash, Hasher},
mantle::{Note, Utxo, Value},
sdp::{ActivityMetadata, ProviderId, ServiceParameters, ServiceType, SessionNumber},
sdp::{ActivityMetadata, ProviderId, ServiceParameters, ServiceType},
};
use lb_cryptarchia_engine::Epoch;
use lb_key_management_system_keys::keys::ZkPublicKey;
use thiserror::Error;
use super::SessionState;
use crate::EpochState;
pub type RewardAmount = u64;
@ -22,7 +21,7 @@ pub type RewardAmount = u64;
/// Generic trait for service-specific reward calculation.
///
/// Each service can implement its own rewards logic by implementing this trait.
/// The rewards object is updated with active messages and session transitions,
/// The rewards object is updated with active messages and epoch transitions,
/// and can calculate expected rewards for each provider based on the service's
/// internal logic.
pub trait Rewards: Clone + PartialEq + Send + Sync + std::fmt::Debug {
@ -37,58 +36,43 @@ pub trait Rewards: Clone + PartialEq + Send + Sync + std::fmt::Debug {
&self,
declaration_id: ProviderId,
metadata: &ActivityMetadata,
block_number: BlockNumber,
params: &Self::Params,
) -> Result<Self, Error>;
/// Update rewards state when sessions transition and calculate rewards to
/// Update rewards state when epoch transition and calculate rewards to
/// distribute.
///
/// Called during session boundaries when active, `past_session`, and
/// next sessions are updated. Returns a map of `ProviderId` to
/// reward amounts for providers eligible for rewards in this session
/// Called during epoch boundaries. Returns a map of `ProviderId` to
/// reward amounts for providers eligible for rewards in this epoch
/// transition.
///
/// The internal calculation logic is opaque to the SDP ledger and
/// determined by the service-specific implementation.
///
/// # Arguments
/// * `last_active` - The state of the session that just ended.
/// * `next_session_first_epoch_state` - The epoch state corresponding to
/// the 1st block of the session `last_active + 1`.
fn update_session(
fn update_epoch(
&self,
last_active: &SessionState,
next_session_first_epoch_state: &EpochState,
// The state of epoch that just ended.
last_epoch_state: &EpochState,
// The state of the new epoch
next_epoch_state: &EpochState,
config: &ServiceParameters,
params: &Self::Params,
) -> (Self, Vec<Utxo>);
/// Update rewards state when a new epoch begins while the session remains
/// unchanged.
///
/// If the epoch has already been processed previously, this method performs
/// no update and returns the current state unchanged.
#[must_use]
fn update_epoch(&self, epoch_state: &EpochState, params: &Self::Params) -> Self;
#[must_use]
fn add_income(&self, income: Value) -> Self;
}
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum Error {
#[error("Target session is not set")]
TargetSessionNotSet,
#[error("Invalid session: expected {expected}, got {got}")]
InvalidSession {
expected: SessionNumber,
got: SessionNumber,
},
#[error("Target epoch is not set")]
TargetEpochNotSet,
#[error("Invalid epoch: expected {expected}, got {got}")]
InvalidEpoch { expected: Epoch, got: Epoch },
#[error("Invalid opinion length: expected {expected}, got {got}")]
InvalidOpinionLength { expected: usize, got: usize },
#[error("Duplicate active message for session {session}, provider {provider_id:?}")]
#[error("Duplicate active message for epoch {epoch}, provider {provider_id:?}")]
DuplicateActiveMessage {
session: SessionNumber,
epoch: Epoch,
provider_id: Box<ProviderId>,
},
#[error("Invalid proof type")]
@ -103,18 +87,18 @@ pub enum Error {
/// Creates a deterministic transaction hash for reward distribution.
///
/// The hash is computed from a version constant, session number, and service
/// The hash is computed from a version constant, epoch number, and service
/// type, ensuring all nodes produce identical transaction hashes for reward
/// notes.
fn create_reward_op_id(session_n: SessionNumber, service_type: ServiceType) -> Hash {
fn create_reward_op_id(epoch: Epoch, service_type: ServiceType) -> Hash {
let mut hasher = Hasher::default();
let session_u8 = session_n.to_le_bytes().to_vec();
let epoch_u8 = epoch.into_inner().to_le_bytes().to_vec();
let service_type_u8 = service_type
.to_bytes()
.expect("conversion to bytes should succeed")
.to_vec();
<Hasher as Digest>::update(&mut hasher, &service_type_u8);
<Hasher as Digest>::update(&mut hasher, &session_u8);
<Hasher as Digest>::update(&mut hasher, &epoch_u8);
hasher.finalize().into()
}
@ -127,7 +111,7 @@ fn create_reward_op_id(session_n: SessionNumber, service_type: ServiceType) -> H
/// - Filters out 0-value rewards
fn distribute_rewards(
rewards: HashMap<ZkPublicKey, RewardAmount>,
session_n: SessionNumber,
epoch: Epoch,
service_type: ServiceType,
) -> Vec<Utxo> {
let mut sorted_rewards: Vec<(ZkPublicKey, RewardAmount)> = rewards
@ -136,7 +120,7 @@ fn distribute_rewards(
.collect();
sorted_rewards.sort_by_key(|(zk_id, _)| *zk_id);
let op_id = create_reward_op_id(session_n, service_type);
let op_id = create_reward_op_id(epoch, service_type);
sorted_rewards
.into_iter()

View File

@ -1,40 +1,86 @@
use std::marker::PhantomData;
use lb_core::{
crypto::ZkHash,
mantle::Note,
sdp::{
Declaration, DeclarationId, Locator, ProviderId, ServiceParameters, ServiceType,
SessionNumber,
Declaration, DeclarationId, Locator, MinStake, ProviderId, ServiceParameters, ServiceType,
locked_notes::LockedNotes,
},
};
use lb_cryptarchia_engine::Epoch;
use lb_groth16::{Field as _, Fr};
use lb_key_management_system_keys::keys::{Ed25519Key, ZkPublicKey};
use num_bigint::BigUint;
use rpds::HashTrieMapSync;
use crate::{EpochState, UtxoTree, mantle::sdp::SessionState};
use crate::{
EpochState, UtxoTree,
mantle::sdp::{SdpLedger, Service, ServiceState, rewards::blend},
};
pub fn create_test_session_state(
pub fn create_epoch_state(
provider_ids: &[ProviderId],
service_type: ServiceType,
session_n: SessionNumber,
) -> SessionState {
epoch: Epoch,
nonce: Fr,
_params: &blend::RewardsParameters,
) -> EpochState {
let mut declarations = rpds::RedBlackTreeMapSync::new_sync();
let mut locked_notes = LockedNotes::new();
for (i, provider_id) in provider_ids.iter().enumerate() {
let note = Note::new(100, ZkPublicKey::zero());
let note_id = Fr::from(i as u64).into();
let declaration = Declaration {
service_type,
provider_id: *provider_id,
locked_note_id: Fr::from(i as u64).into(),
locked_note_id: note_id,
locators: "/ip4/1.1.1.1/udp/0".parse::<Locator>().unwrap().into(),
zk_id: ZkPublicKey::new(BigUint::from(i as u64).into()),
created: 0,
active: 0,
created: 0.into(),
active: 0.into(),
withdrawn: None,
nonce: 0,
};
declarations = declarations.insert(DeclarationId([i as u8; 32]), declaration);
locked_notes = locked_notes
.lock(
&MinStake {
threshold: 1,
timestamp: 0,
},
service_type,
note,
&note_id,
)
.unwrap();
}
SessionState {
declarations,
session_n,
}
let mut epoch_state = EpochState {
epoch,
nonce,
utxos: UtxoTree::default(),
total_stake: 0,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
sdp: SdpLedger::new(epoch),
};
let ledger = SdpLedger {
services: HashTrieMapSync::new_sync().insert(
ServiceType::BlendNetwork,
Service::BlendNetwork(ServiceState {
declarations,
// TODO: enable after making `rewards` module stable
// rewards: blend::Rewards::new(params, &epoch_state),
_phantom: PhantomData,
}),
),
locked_notes,
epoch,
};
epoch_state.sdp = ledger;
epoch_state
}
pub fn create_provider_id(byte: u8) -> ProviderId {
@ -46,25 +92,9 @@ pub fn create_provider_id(byte: u8) -> ProviderId {
pub fn create_service_parameters() -> ServiceParameters {
ServiceParameters {
lock_period: 10,
inactivity_period: 1,
retention_period: 1,
timestamp: 0,
session_duration: 10,
}
}
pub fn dummy_epoch_state() -> EpochState {
dummy_epoch_state_with(0, 0)
}
pub fn dummy_epoch_state_with(epoch: u32, nonce: u64) -> EpochState {
EpochState {
epoch: epoch.into(),
nonce: ZkHash::from(BigUint::from(nonce)),
utxos: UtxoTree::default(),
total_stake: 0,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
lock_period: 10.into(),
inactivity_period: 1.into(),
retention_period: 1.into(),
epoch: 0.into(),
}
}

View File

@ -24,27 +24,13 @@ impl Settings {
*slot_duration
}
/// Number of rounds per session, calculated as the number of slots per
/// Number of rounds per epoch, calculated as the number of slots per
/// epoch, correctly scaled to account for the slot/round ratio.
#[must_use]
pub fn rounds_per_session(&self, slots_per_epoch: u64, slot_duration: &Duration) -> NonZeroU64 {
pub fn rounds_per_epoch(&self, slots_per_epoch: u64, slot_duration: &Duration) -> NonZeroU64 {
((slots_per_epoch * slot_duration.as_secs()) / self.round_duration(slot_duration).as_secs())
.try_into()
.expect("There must be at least one round per session.")
}
/// Number of rounds per interval, calculated as the average number of slots
/// per block (slot activation threshold), correctly scaled to account for
/// the slot/round ratio.
#[must_use]
pub fn rounds_per_interval(
&self,
slots_per_block: u64,
slot_duration: &Duration,
) -> NonZeroU64 {
((slots_per_block * slot_duration.as_secs()) / self.round_duration(slot_duration).as_secs())
.try_into()
.expect("There must be at least one round per interval.")
.expect("There must be at least one round per epoch.")
}
/// Number of rounds per observation window.
@ -65,36 +51,17 @@ impl Settings {
.unwrap()
}
/// Number of rounds per session transition period.
///
/// The Blend spec defines this as roughly the same as
/// [`rounds_per_interval`].
#[must_use]
pub fn rounds_per_session_transition_period(
&self,
slots_per_block: u64,
slot_duration: &Duration,
) -> NonZeroU64 {
self.rounds_per_interval(slots_per_block, slot_duration)
}
/// Number of rounds per epoch transition period.
/// Duration of the epoch transition period.
///
/// The Blend spec defines this as roughly the same time it takes to propose
/// a new block.
#[must_use]
pub fn slots_per_epoch_transition_period(
pub const fn epoch_transition(
&self,
slots_per_block: u64,
slot_duration: &Duration,
) -> NonZeroU64 {
let rounds_per_session_transition_period =
self.rounds_per_session_transition_period(slots_per_block, slot_duration);
((self.round_duration(slot_duration).as_secs()
* rounds_per_session_transition_period.get())
/ slot_duration.as_secs())
.try_into()
.expect("There must be at least one slot per epoch transition period.")
) -> Duration {
Duration::from_secs(slot_duration.as_secs() * slots_per_block)
}
#[must_use]
@ -109,7 +76,7 @@ impl Settings {
message_frequency_per_round: self.core.scheduler.cover.message_frequency_per_round,
minimum_network_size: self.common.minimum_network_size.into(),
num_blend_layers: self.common.num_blend_layers,
rounds_per_session: self.rounds_per_session(
rounds_per_epoch: self.rounds_per_epoch(
cryptarchia_deployment.slots_per_epoch(),
&time_deployment.slot_duration,
),
@ -161,9 +128,6 @@ pub struct SchedulerSettings {
pub struct CoverTrafficSettings {
/// `F_c`: frequency at which cover messages are generated per round.
pub message_frequency_per_round: NonNegativeF64,
// `max`: safety buffer length, expressed in intervals
// TODO: Can we derive this?
pub intervals_for_safety_buffer: u64,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@ -181,12 +145,9 @@ mod tests {
#[test]
fn blend_devnet() {
const EXPECTED_ROUND_DURATION: Duration = Duration::from_secs(1);
const EXPECTED_ROUNDS_PER_SESSION: NonZeroU64 = NonZeroU64::new(6_000).unwrap();
const EXPECTED_ROUNDS_PER_INTERVAL: NonZeroU64 = NonZeroU64::new(20).unwrap();
const EXPECTED_ROUNDS_PER_EPOCH: NonZeroU64 = NonZeroU64::new(6_000).unwrap();
const EXPECTED_ROUNDS_PER_OBSERVATION_WINDOW: NonZeroU64 = NonZeroU64::new(10).unwrap();
const EXPECTED_ROUNDS_PER_SESSION_TRANSITION_PERIOD: NonZeroU64 =
NonZeroU64::new(20).unwrap();
const EXPECTED_SLOTS_PER_EPOCH_TRANSITION_PERIOD: NonZeroU64 = NonZeroU64::new(20).unwrap();
const EXPECTED_EPOCH_TRANSITION_PERIOD: Duration = Duration::from_secs(20);
let deployment: DeploymentSettings = WellKnownDeployment::Devnet.into();
@ -199,15 +160,8 @@ mod tests {
assert_eq!(
deployment
.blend
.rounds_per_session(slots_per_epoch, &slot_duration),
EXPECTED_ROUNDS_PER_SESSION
);
assert_eq!(
deployment
.blend
.rounds_per_interval(slots_per_block, &slot_duration),
EXPECTED_ROUNDS_PER_INTERVAL
.rounds_per_epoch(slots_per_epoch, &slot_duration),
EXPECTED_ROUNDS_PER_EPOCH
);
assert_eq!(
@ -218,15 +172,8 @@ mod tests {
assert_eq!(
deployment
.blend
.rounds_per_session_transition_period(slots_per_block, &slot_duration),
EXPECTED_ROUNDS_PER_SESSION_TRANSITION_PERIOD
);
assert_eq!(
deployment
.blend
.slots_per_epoch_transition_period(slots_per_block, &slot_duration),
EXPECTED_SLOTS_PER_EPOCH_TRANSITION_PERIOD
.epoch_transition(slots_per_block, &slot_duration),
EXPECTED_EPOCH_TRANSITION_PERIOD
);
}
}

View File

@ -65,20 +65,14 @@ impl ServiceConfig {
minimum_network_size: self.deployment.common.minimum_network_size.into(),
recovery_path_prefix,
time: TimingSettings {
epoch_transition_period_in_slots: self
epoch_transition_period: self
.deployment
.slots_per_epoch_transition_period(slots_per_block, &slot_duration),
.epoch_transition(slots_per_block, &slot_duration),
round_duration: self.deployment.round_duration(&slot_duration),
rounds_per_interval: self
.deployment
.rounds_per_interval(slots_per_block, &slot_duration),
rounds_per_observation_window: self.deployment.rounds_per_observation_window(),
rounds_per_session: self
rounds_per_epoch: self
.deployment
.rounds_per_session(slots_per_epoch, &slot_duration),
rounds_per_session_transition_period: self
.deployment
.rounds_per_session_transition_period(slots_per_block, &slot_duration),
.rounds_per_epoch(slots_per_epoch, &slot_duration),
},
data_replication_factor: self.deployment.common.data_replication_factor,
},
@ -103,12 +97,6 @@ impl ServiceConfig {
},
scheduler: SchedulerSettings {
cover: CoverTrafficSettings {
intervals_for_safety_buffer: self
.deployment
.core
.scheduler
.cover
.intervals_for_safety_buffer,
message_frequency_per_round: self
.deployment
.core

View File

@ -1,9 +1,10 @@
use core::num::{NonZero, NonZeroU32};
use std::collections::HashMap;
use lb_chain_service::Epoch;
use lb_core::{
block::{BlockNumber, genesis::GenesisBlock},
sdp::{MinStake, ServiceType},
block::genesis::GenesisBlock,
sdp::{MinStake, NumberOfEpochs, ServiceType},
};
use lb_cryptarchia_engine::{
Config as ConsensusConfig, average_slots_for_blocks, base_period_length, time::epoch_length,
@ -83,13 +84,11 @@ pub struct SdpConfig {
pub min_stake: MinStake,
}
// The same as `lb_core::sdp::ServiceParameters`, minus the
// `session_duration` values which are calculated from the other values
// provided.
// The same as `lb_core::sdp::ServiceParameters`.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ServiceParameters {
pub lock_period: u64,
pub inactivity_period: u64,
pub retention_period: u64,
pub timestamp: BlockNumber,
pub lock_period: NumberOfEpochs,
pub inactivity_period: NumberOfEpochs,
pub retention_period: NumberOfEpochs,
pub epoch: Epoch,
}

View File

@ -35,8 +35,6 @@ impl ServiceConfig {
lb_chain_network_service::ChainNetworkSettings<PeerId, LibP2pAdapterSettings>,
lb_chain_leader_service::LeaderSettings<(), Libp2pBroadcastSettings>,
) {
let blocks_per_session = self.deployment.blocks_per_epoch();
let ledger_config = lb_ledger::Config {
consensus_config: self.deployment.consensus_config(),
epoch_config: EpochConfig {
@ -62,11 +60,10 @@ impl ServiceConfig {
(
service_type,
ServiceParameters {
session_duration: blocks_per_session,
inactivity_period: service_params.inactivity_period,
lock_period: service_params.lock_period,
retention_period: service_params.retention_period,
timestamp: service_params.timestamp,
epoch: service_params.epoch,
},
)
})

View File

@ -8,7 +8,6 @@ blend:
scheduler:
cover:
message_frequency_per_round: 1.0
intervals_for_safety_buffer: 100
delayer:
maximum_release_delay_in_rounds: 1
minimum_messages_coefficient: 1
@ -34,7 +33,7 @@ cryptarchia:
lock_period: 10
inactivity_period: 1
retention_period: 1
timestamp: 0
epoch: 0
min_stake:
threshold: 1
timestamp: 0

View File

@ -1,10 +1,18 @@
use lb_blend::scheduling::message_blend::provers::{
core_and_leader::RealCoreAndLeaderProofsGenerator, leader::RealLeaderProofsGenerator,
use axum::async_trait;
use lb_blend::{
message::crypto::key_ext::Ed25519SecretKeyExt as _,
proofs::{
quota::{VerifiedProofOfQuota, inputs::prove::private::ProofOfLeadershipQuotaInputs},
selection::VerifiedProofOfSelection,
},
scheduling::message_blend::provers::{
BlendLayerProof, ProofsGeneratorSettings,
core_and_leader::RealCoreAndLeaderProofsGenerator,
leader::{LeaderProofsGenerator, RealLeaderProofsGenerator},
},
};
use lb_blend_service::{
RealProofsVerifier, core::kms::PreloadKMSBackendCorePoQGenerator, membership::service::Adapter,
};
use lb_chain_broadcast_service::BlockBroadcastService;
use lb_blend_service::{RealProofsVerifier, core::kms::PreloadKMSBackendCorePoQGenerator};
use lb_key_management_system_service::keys::UnsecuredEd25519Key;
use lb_time_service::backends::NtpTimeBackend;
use libp2p::PeerId;
@ -12,13 +20,10 @@ use crate::generic_services::{CryptarchiaService, SdpService, blend::pol::PolInf
pub(crate) mod pol;
pub type BlendMembershipAdapter<RuntimeServiceId> =
Adapter<BlockBroadcastService<RuntimeServiceId>, PeerId>;
pub type BlendCoreService<RuntimeServiceId> = lb_blend_service::core::BlendService<
lb_blend_service::core::backends::libp2p::Libp2pBlendBackend,
PeerId,
lb_blend_service::core::network::libp2p::Libp2pAdapter<RuntimeServiceId>,
BlendMembershipAdapter<RuntimeServiceId>,
SdpService<RuntimeServiceId>,
RealCoreAndLeaderProofsGenerator<PreloadKMSBackendCorePoQGenerator<RuntimeServiceId>>,
RealProofsVerifier,
@ -27,11 +32,32 @@ pub type BlendCoreService<RuntimeServiceId> = lb_blend_service::core::BlendServi
PolInfoProvider,
RuntimeServiceId,
>;
#[derive(Clone)]
pub struct MockLeaderProofsGenerator;
#[async_trait]
impl LeaderProofsGenerator for MockLeaderProofsGenerator {
fn new(
_settings: ProofsGeneratorSettings,
_private_inputs: ProofOfLeadershipQuotaInputs,
) -> Self {
Self
}
async fn get_next_proof(&mut self) -> BlendLayerProof {
BlendLayerProof {
proof_of_quota: VerifiedProofOfQuota::from_bytes_unchecked([0; _]),
proof_of_selection: VerifiedProofOfSelection::from_bytes_unchecked([0; _]),
ephemeral_signing_key: UnsecuredEd25519Key::generate_with_blake_rng(),
}
}
}
pub type BlendEdgeService<RuntimeServiceId> = lb_blend_service::edge::BlendService<
lb_blend_service::edge::backends::libp2p::Libp2pBlendBackend,
PeerId,
<lb_blend_service::core::network::libp2p::Libp2pAdapter<RuntimeServiceId> as lb_blend_service::core::network::NetworkAdapter<RuntimeServiceId>>::BroadcastSettings,
BlendMembershipAdapter<RuntimeServiceId>,
RealLeaderProofsGenerator,
NtpTimeBackend,
CryptarchiaService<RuntimeServiceId>,

View File

@ -58,47 +58,43 @@ where
Some(Box::new(
WatchStream::new(pol_winning_slot_receiver)
.filter_map(ready)
.scan(
None,
|processed_epoch, (leader_private, leader_public, epoch)| {
let should_yield_new_epoch =
processed_epoch.is_none_or(|processed_epoch| processed_epoch < epoch);
if !should_yield_new_epoch {
return ready(Some(None));
}
.scan(None, |processed_epoch, (leader_private, _, epoch)| {
let should_yield_new_epoch =
processed_epoch.is_none_or(|processed_epoch| processed_epoch < epoch);
if !should_yield_new_epoch {
return ready(Some(None));
}
*processed_epoch = Some(epoch);
let PolWitnessInputsData {
wallet:
PolWalletInputsData {
aged_path,
aged_selectors,
note_value,
output_number,
secret_key,
transaction_hash,
..
},
chain: PolChainInputsData { slot_number, .. },
} = leader_private.input();
let aged_path_and_selectors =
core::array::from_fn(|i| (aged_path[i], aged_selectors[i]));
ready(Some(Some(PolEpochInfo {
epoch,
poq_public_inputs: leader_public,
poq_private_inputs: ProofOfLeadershipQuotaInputs {
aged_path_and_selectors,
note_value: *note_value,
output_number: *output_number,
secret_key: *secret_key,
slot: *slot_number,
transaction_hash: *transaction_hash,
*processed_epoch = Some(epoch);
let PolWitnessInputsData {
wallet:
PolWalletInputsData {
aged_path,
aged_selectors,
note_value,
output_number,
secret_key,
transaction_hash,
..
},
})))
},
)
chain: PolChainInputsData { slot_number, .. },
} = leader_private.input();
let aged_path_and_selectors =
core::array::from_fn(|i| (aged_path[i], aged_selectors[i]));
ready(Some(Some(PolEpochInfo {
epoch,
poq_private_inputs: ProofOfLeadershipQuotaInputs {
aged_path_and_selectors,
note_value: *note_value,
output_number: *output_number,
secret_key: *secret_key,
slot: *slot_number,
transaction_hash: *transaction_hash,
},
})))
})
.filter_map(ready),
))
}

View File

@ -9,12 +9,9 @@ pub mod global_allocators;
use std::panic::set_hook;
use color_eyre::eyre::{Result, eyre};
pub use lb_blend_service::{
core::{
backends::libp2p::Libp2pBlendBackend as BlendBackend,
network::libp2p::Libp2pAdapter as BlendNetworkAdapter,
},
membership::service::Adapter as BlendMembershipAdapter,
pub use lb_blend_service::core::{
backends::libp2p::Libp2pBlendBackend as BlendBackend,
network::libp2p::Libp2pAdapter as BlendNetworkAdapter,
};
pub use lb_core::{
codec,

View File

@ -8,7 +8,6 @@ blend:
scheduler:
cover:
message_frequency_per_round: 1.0
intervals_for_safety_buffer: 100
delayer:
maximum_release_delay_in_rounds: 3
minimum_messages_coefficient: 1
@ -34,7 +33,7 @@ cryptarchia:
lock_period: 10
inactivity_period: 1
retention_period: 1
timestamp: 0
epoch: 0
min_stake:
threshold: 1
timestamp: 0

View File

@ -234,7 +234,8 @@ where
+ Sync
+ Display
+ AsServiceId<StorageService<StorageBackend, RuntimeServiceId>>
+ AsServiceId<ConsensusService>,
+ AsServiceId<ConsensusService>
+ 'static,
{
let processed_blocks_stream =
get_processed_blocks_event_stream::<Transaction, ConsensusService, RuntimeServiceId>(
@ -743,8 +744,11 @@ where
TryFrom<Block<Transaction>> + TryInto<Block<Transaction>>,
<StorageBackend as StorageChainApi>::Tx: From<Bytes> + AsRef<[u8]>,
<StorageBackend as StorageChainApi>::Events: TryFrom<Events> + TryInto<Events>,
RuntimeServiceId:
Debug + Sync + Display + AsServiceId<StorageService<StorageBackend, RuntimeServiceId>>,
RuntimeServiceId: Debug
+ Sync
+ Display
+ AsServiceId<StorageService<StorageBackend, RuntimeServiceId>>
+ 'static,
{
let header_ids = get_immutable_blocks_header_ids(handle, from_slot, to_slot).await?;
@ -795,8 +799,11 @@ where
TryFrom<Block<Transaction>> + TryInto<Block<Transaction>>,
<StorageBackend as StorageChainApi>::Tx: From<Bytes> + AsRef<[u8]>,
<StorageBackend as StorageChainApi>::Events: TryFrom<Events> + TryInto<Events>,
RuntimeServiceId:
Debug + Sync + Display + AsServiceId<StorageService<StorageBackend, RuntimeServiceId>>,
RuntimeServiceId: Debug
+ Sync
+ Display
+ AsServiceId<StorageService<StorageBackend, RuntimeServiceId>>
+ 'static,
{
let relay = handle.relay().await?;
let storage_adapter = StorageAdapter::<_, _, RuntimeServiceId>::new(relay).await;
@ -836,8 +843,11 @@ where
TryFrom<Block<Transaction>> + TryInto<Block<Transaction>>,
<StorageBackend as StorageChainApi>::Tx: From<Bytes> + AsRef<[u8]>,
<StorageBackend as StorageChainApi>::Events: TryFrom<Events> + TryInto<Events>,
RuntimeServiceId:
Debug + Sync + Display + AsServiceId<StorageService<StorageBackend, RuntimeServiceId>>,
RuntimeServiceId: Debug
+ Sync
+ Display
+ AsServiceId<StorageService<StorageBackend, RuntimeServiceId>>
+ 'static,
{
let relay = handle.relay().await?;
let storage_adapter = StorageAdapter::<_, _, RuntimeServiceId>::new(relay).await;
@ -875,8 +885,11 @@ where
TryFrom<Block<Transaction>> + TryInto<Block<Transaction>>,
<StorageBackend as StorageChainApi>::Tx: From<Bytes> + AsRef<[u8]>,
<StorageBackend as StorageChainApi>::Events: TryFrom<Events> + TryInto<Events>,
RuntimeServiceId:
Debug + Sync + Display + AsServiceId<StorageService<StorageBackend, RuntimeServiceId>>,
RuntimeServiceId: Debug
+ Sync
+ Display
+ AsServiceId<StorageService<StorageBackend, RuntimeServiceId>>
+ 'static,
{
let mut stream = get_transactions::<Transaction, StorageBackend, RuntimeServiceId>(
handle,
@ -892,8 +905,13 @@ pub async fn get_sdp_declarations<RuntimeServiceId>(
handle: &overwatch::overwatch::handle::OverwatchHandle<RuntimeServiceId>,
) -> Result<Vec<Declaration>, super::DynError>
where
RuntimeServiceId:
Debug + Send + Sync + Display + 'static + AsServiceId<Cryptarchia<RuntimeServiceId>>,
RuntimeServiceId: Debug
+ Send
+ Sync
+ Display
+ 'static
+ AsServiceId<Cryptarchia<RuntimeServiceId>>
+ 'static,
{
let relay = handle.relay::<Cryptarchia<RuntimeServiceId>>().await?;
let (sender, receiver) = oneshot::channel();

View File

@ -18,7 +18,6 @@ fork_stream = { workspace = true }
futures = { workspace = true }
hex = { workspace = true }
lb-blend = { workspace = true }
lb-chain-broadcast-service = { workspace = true }
lb-chain-service = { workspace = true }
lb-core = { workspace = true }
lb-cryptarchia-engine = { workspace = true }

View File

@ -1,4 +1,5 @@
use lb_blend::scheduling::membership::Membership;
use lb_chain_service::Epoch;
use lb_libp2p::NetworkBehaviour;
use libp2p::{PeerId, allow_block_list::BlockedPeers};
@ -21,7 +22,7 @@ where
{
pub fn new(
config: &BlendConfig<Libp2pBlendBackendSettings>,
current_membership_info: (Membership<PeerId>, u64),
current_membership_info: (Membership<PeerId>, Epoch),
) -> Self {
let observation_window_interval_provider =
ObservationWindowProvider::from((config, &current_membership_info.0));

View File

@ -8,6 +8,7 @@ use futures::{
use lb_blend::message::encap::validated::{
EncapsulatedMessageWithVerifiedPublicHeader, EncapsulatedMessageWithVerifiedSignature,
};
use lb_chain_service::Epoch;
use lb_log_targets::blend;
use libp2p::PeerId;
use overwatch::overwatch::handle::OverwatchHandle;
@ -18,7 +19,7 @@ use tokio_stream::wrappers::BroadcastStream;
use crate::{
core::{
backends::{
BlendBackend, PublicInfo, SessionInfo,
BackendEpochInfo, BlendBackend,
libp2p::{
swarm::{BlendSwarm, BlendSwarmMessage, SwarmParams},
tokio_provider::ObservationWindowTokioIntervalProvider,
@ -46,7 +47,7 @@ pub(crate) use self::tests::utils as core_swarm_test_utils;
pub struct Libp2pBlendBackend {
swarm_task_abort_handle: AbortHandle,
swarm_message_sender: mpsc::Sender<BlendSwarmMessage>,
incoming_message_sender: broadcast::Sender<(EncapsulatedMessageWithVerifiedSignature, u64)>,
incoming_message_sender: broadcast::Sender<(EncapsulatedMessageWithVerifiedSignature, Epoch)>,
}
const CHANNEL_SIZE: usize = 64;
@ -61,7 +62,7 @@ where
fn new(
config: BlendConfig<Self::Settings>,
overwatch_handle: OverwatchHandle<RuntimeServiceId>,
current_public_info: PublicInfo<PeerId>,
current_epoch_info: BackendEpochInfo<PeerId>,
rng: Rng,
) -> Self {
let (swarm_message_sender, swarm_message_receiver) = mpsc::channel(CHANNEL_SIZE);
@ -70,7 +71,7 @@ where
let swarm = BlendSwarm::<_, ObservationWindowTokioIntervalProvider>::new(SwarmParams {
config: &config,
current_public_info,
current_epoch_info,
incoming_message_sender: incoming_message_sender.clone(),
minimum_network_size,
rng,
@ -96,13 +97,13 @@ where
async fn publish(
&self,
msg: EncapsulatedMessageWithVerifiedPublicHeader,
intended_session: u64,
intended_epoch: Epoch,
) {
if let Err(e) = self
.swarm_message_sender
.send(BlendSwarmMessage::Publish {
message: Box::new(msg),
session: intended_session,
epoch: intended_epoch,
})
.await
{
@ -110,29 +111,29 @@ where
}
}
async fn rotate_session(&mut self, new_session_info: SessionInfo<PeerId>) {
async fn rotate_epoch(&mut self, new_epoch_info: BackendEpochInfo<PeerId>) {
if let Err(e) = self
.swarm_message_sender
.send(BlendSwarmMessage::StartNewSession(new_session_info))
.send(BlendSwarmMessage::StartNewEpoch(new_epoch_info))
.await
{
tracing::error!(target: LOG_TARGET, "Failed to send new public session info to BlendSwarm: {e}");
tracing::error!(target: LOG_TARGET, "Failed to send new public epoch info to BlendSwarm: {e}");
}
}
async fn complete_session_transition(&mut self) {
async fn complete_epoch_transition(&mut self) {
if let Err(e) = self
.swarm_message_sender
.send(BlendSwarmMessage::CompleteSessionTransition)
.send(BlendSwarmMessage::CompleteEpochTransition)
.await
{
tracing::error!(target: LOG_TARGET, "Failed to send session transition termination command to BlendSwarm: {e}");
tracing::error!(target: LOG_TARGET, "Failed to send epoch transition termination command to BlendSwarm: {e}");
}
}
fn listen_to_incoming_messages(
&mut self,
) -> Pin<Box<dyn Stream<Item = (EncapsulatedMessageWithVerifiedSignature, u64)> + Send>> {
) -> Pin<Box<dyn Stream<Item = (EncapsulatedMessageWithVerifiedSignature, Epoch)> + Send>> {
Box::pin(
BroadcastStream::new(self.incoming_message_sender.subscribe())
.filter_map(async |event| event.ok()),

View File

@ -26,6 +26,7 @@ use lb_blend::{
},
scheduling::membership::Membership,
};
use lb_chain_service::Epoch;
use lb_libp2p::{DialOpts, SwarmEvent};
use libp2p::{Multiaddr, PeerId, Swarm, SwarmBuilder, swarm::dial_opts::PeerCondition};
use rand::RngCore;
@ -34,7 +35,7 @@ use tokio::sync::{broadcast, mpsc, oneshot};
use crate::{
core::{
backends::{
PublicInfo, SessionInfo,
BackendEpochInfo,
libp2p::{
LOG_TARGET, Libp2pBlendBackendSettings,
behaviour::{BlendBehaviour, BlendBehaviourEvent},
@ -50,10 +51,10 @@ use crate::{
pub enum BlendSwarmMessage {
Publish {
message: Box<EncapsulatedMessageWithVerifiedPublicHeader>,
session: u64,
epoch: Epoch,
},
StartNewSession(SessionInfo<PeerId>),
CompleteSessionTransition,
StartNewEpoch(BackendEpochInfo<PeerId>),
CompleteEpochTransition,
GetNetworkInfo {
reply: oneshot::Sender<Option<NetworkInfo<PeerId>>>,
},
@ -70,11 +71,11 @@ pub struct DialAttempt {
failed_peers: HashSet<PeerId>,
}
/// [`DialAttempt`] with session information, i.e., whether the attempt was made
/// at this session or the previous one.
pub enum SessionDialAttempt {
OngoingSession(Option<DialAttempt>),
PreviousSession,
/// [`DialAttempt`] with epoch information, i.e., whether the attempt was made
/// at this epoch or the previous one.
pub enum EpochDialAttempt {
OngoingEpoch(Option<DialAttempt>),
PreviousEpoch,
}
#[cfg(test)]
@ -97,8 +98,8 @@ where
{
swarm: Swarm<BlendBehaviour<ObservationWindowProvider>>,
swarm_messages_receiver: mpsc::Receiver<BlendSwarmMessage>,
incoming_message_sender: broadcast::Sender<(EncapsulatedMessageWithVerifiedSignature, u64)>,
public_info: PublicInfo<PeerId>,
incoming_message_sender: broadcast::Sender<(EncapsulatedMessageWithVerifiedSignature, Epoch)>,
current_epoch_info: BackendEpochInfo<PeerId>,
rng: Rng,
max_dial_attempts_per_connection: NonZeroU64,
ongoing_dials: HashMap<PeerId, DialAttempt>,
@ -108,10 +109,11 @@ where
pub struct SwarmParams<'config, Rng> {
pub config: &'config BlendConfig<Libp2pBlendBackendSettings>,
pub current_public_info: PublicInfo<PeerId>,
pub current_epoch_info: BackendEpochInfo<PeerId>,
pub rng: Rng,
pub swarm_message_receiver: mpsc::Receiver<BlendSwarmMessage>,
pub incoming_message_sender: broadcast::Sender<(EncapsulatedMessageWithVerifiedSignature, u64)>,
pub incoming_message_sender:
broadcast::Sender<(EncapsulatedMessageWithVerifiedSignature, Epoch)>,
pub minimum_network_size: NonZeroUsize,
}
@ -127,7 +129,7 @@ where
pub(super) fn new(
SwarmParams {
config,
current_public_info,
current_epoch_info,
rng,
swarm_message_receiver: swarm_messages_receiver,
incoming_message_sender,
@ -140,15 +142,7 @@ where
.with_quic()
.with_dns()
.expect("DNS transport should be supported")
.with_behaviour(|_| {
BlendBehaviour::new(
config,
(
current_public_info.session.membership.clone(),
current_public_info.session.session_number,
),
)
})
.with_behaviour(|_| BlendBehaviour::new(config, current_epoch_info.clone()))
.expect("Blend Behaviour should be built")
.with_swarm_config(|cfg| {
// The idle timeout starts ticking once there are no active streams on a
@ -166,7 +160,7 @@ where
swarm,
swarm_messages_receiver,
incoming_message_sender,
public_info: current_public_info,
current_epoch_info,
rng,
max_dial_attempts_per_connection: config.backend.max_dial_attempts_per_peer,
ongoing_dials: HashMap::with_capacity(
@ -189,7 +183,7 @@ where
IntervalStreamProvider<IntervalStream: Unpin + Send, IntervalItem = RangeInclusive<u64>>,
{
/// Dial random peers from the membership list,
/// excluding the peers with a negotiated connection in the ongoing session,
/// excluding the peers with a negotiated connection in the ongoing epoch,
/// the peers that we are already trying to dial, the blocked peers, and
/// any extra peers specified in `except`.
fn dial_random_peers_except(&mut self, amount: usize, mut except: HashSet<PeerId>) {
@ -197,7 +191,7 @@ where
// We need to clone else we would not be able to call `self.dial` inside which
// requires access to `&mut self`.
let current_membership = self.public_info.session.membership.clone();
let current_membership = self.current_epoch_info.0.clone();
// Membership contains local node, so we need to exclude that from the count.
if except.len() == current_membership.size() - 1 {
tracing::debug!(target: LOG_TARGET, "All eligible peers have been tried. Clearing failed peers memory and retrying from scratch.");
@ -227,7 +221,7 @@ where
fn check_and_dial_new_peers_except(&mut self, except: HashSet<PeerId>) {
tracing::trace!(target: LOG_TARGET, ?except, "Checking if we need to dial new peers");
let membership_size = self.public_info.session.membership.size();
let membership_size = self.current_epoch_info.0.size();
if membership_size < self.minimum_network_size.get() {
tracing::warn!(target: LOG_TARGET, "Not dialing any peers because set of core nodes is smaller than the minimum network size. {membership_size} < {}", self.minimum_network_size.get());
return;
@ -253,17 +247,17 @@ where
fn collect_network_info(&self) -> NetworkInfo<PeerId> {
let core_behaviour = self.swarm.behaviour().blend.with_core();
let current_session_peers = core_behaviour
let current_epoch_peers = core_behaviour
.negotiated_peers()
.iter()
.map(|(peer_id, peer_state)| (*peer_id, peer_state.negotiated_state().is_healthy()))
.collect();
let old_session_peers = core_behaviour
.old_session_peer_ids()
let old_epoch_peers = core_behaviour
.old_epoch_peer_ids()
.map(|peers| peers.copied().collect());
let core_info = CoreInfo {
current_session_peers,
old_session_peers,
current_epoch_peers,
old_epoch_peers,
};
NetworkInfo {
node_id: *self.swarm.local_peer_id(),
@ -278,11 +272,11 @@ where
fn handle_blend_core_behaviour_event(&mut self, blend_event: CoreToCoreEvent) {
match blend_event {
lb_blend::network::core::with_core::behaviour::Event::Message { message, sender, session } => {
lb_blend::network::core::with_core::behaviour::Event::Message { message, sender, epoch } => {
// Forward message received from node to all other core nodes.
self.forward_received_core_message(&message, sender, session);
self.forward_received_core_message(&message, sender, epoch);
// Bubble up to service for decapsulation and delaying.
self.report_message_to_service(*message, session, metrics::InboundMessageType::Core);
self.report_message_to_service(*message, epoch, metrics::InboundMessageType::Core);
}
lb_blend::network::core::with_core::behaviour::Event::UnhealthyPeer(peer_id) => {
self.handle_unhealthy_peer(peer_id);
@ -299,8 +293,8 @@ where
lb_blend::network::core::with_core::behaviour::Event::OutboundConnectionUpgradeFailed { peer, reason } => {
match reason {
ConnectionUpgradeFailureReason::ConnectionFailure => {
// If we ran out of dial attempts, we try to connect to another random peer that we are not yet connected to, if the dial attempt was performed in the current session.
let SessionDialAttempt::OngoingSession(Some(dial_attempt)) = self.schedule_retry(peer) else {
// If we ran out of dial attempts, we try to connect to another random peer that we are not yet connected to, if the dial attempt was performed in the current epoch.
let EpochDialAttempt::OngoingEpoch(Some(dial_attempt)) = self.schedule_retry(peer) else {
return;
};
let failed_peers = {
@ -371,10 +365,10 @@ where
};
match self.schedule_retry(peer_id) {
SessionDialAttempt::PreviousSession => {
tracing::debug!(target: LOG_TARGET, "Received a dial error for peer {peer_id:?} that is not being tracked. This means that a new session has cleared the map of pending dials. No retry will be performed.");
EpochDialAttempt::PreviousEpoch => {
tracing::debug!(target: LOG_TARGET, "Received a dial error for peer {peer_id:?} that is not being tracked. This means that a new epoch has cleared the map of pending dials. No retry will be performed.");
}
SessionDialAttempt::OngoingSession(Some(dial_attempt)) => {
EpochDialAttempt::OngoingEpoch(Some(dial_attempt)) => {
let failed_peers = {
let mut failed_peers = dial_attempt.failed_peers;
failed_peers.insert(peer_id);
@ -383,7 +377,7 @@ where
self.check_and_dial_new_peers_except(failed_peers);
}
// Retry in progress.
SessionDialAttempt::OngoingSession(None) => {}
EpochDialAttempt::OngoingEpoch(None) => {}
}
}
_ => {
@ -394,21 +388,21 @@ where
fn handle_swarm_message(&mut self, msg: BlendSwarmMessage) {
match msg {
BlendSwarmMessage::Publish { message, session } => {
self.handle_publish_swarm_message(*message, session);
BlendSwarmMessage::Publish { message, epoch } => {
self.handle_publish_swarm_message(*message, epoch);
}
BlendSwarmMessage::StartNewSession(new_session_info) => {
self.public_info.session = new_session_info;
self.swarm.behaviour_mut().blend.start_new_session((
self.public_info.session.membership.clone(),
self.public_info.session.session_number,
));
BlendSwarmMessage::StartNewEpoch(new_epoch_info) => {
self.current_epoch_info = new_epoch_info;
self.swarm
.behaviour_mut()
.blend
.start_new_epoch(self.current_epoch_info.clone());
self.ongoing_dials.clear();
self.pending_retries.clear();
self.check_and_dial_new_peers_except(HashSet::new());
}
BlendSwarmMessage::CompleteSessionTransition => {
self.swarm.behaviour_mut().blend.finish_session_transition();
BlendSwarmMessage::CompleteEpochTransition => {
self.swarm.behaviour_mut().blend.finish_epoch_transition();
}
BlendSwarmMessage::GetNetworkInfo { reply } => {
let info = self.collect_network_info();
@ -494,7 +488,7 @@ where
DialOpts::peer_id(peer_id)
.addresses(vec![address])
// We use `Always` since we want to be able to dial a peer even if we already have
// an established connection with it that belongs to the previous session.
// an established connection with it that belongs to the previous epoch.
.condition(PeerCondition::Always)
.build(),
) {
@ -534,23 +528,23 @@ where
///
/// It returns:
///
/// * `SessionDialAttempt::PreviousSession` if the peer is not being tracked
/// in the map of ongoing dials, which means that a new session has been
/// * `EpochDialAttempt::PreviousEpoch` if the peer is not being tracked in
/// the map of ongoing dials, which means that a new epoch has been
/// started and the dial attempts have been reset;
/// * `SessionDialAttempt::OngoingSession(None)` if a retry has been
/// scheduled with exponential backoff;
/// * `SessionDialAttempt::OngoingSession(Some)` if the maximum attempts
/// have been reached and the peer has been removed from the map of
/// ongoing dials.
fn schedule_retry(&mut self, peer_id: PeerId) -> SessionDialAttempt {
/// * `EpochDialAttempt::OngoingEpoch(None)` if a retry has been scheduled
/// with exponential backoff;
/// * `EpochDialAttempt::OngoingEpoch(Some)` if the maximum attempts have
/// been reached and the peer has been removed from the map of ongoing
/// dials.
fn schedule_retry(&mut self, peer_id: PeerId) -> EpochDialAttempt {
let Some(dial_attempt) = self.ongoing_dials.remove(&peer_id) else {
tracing::debug!(target: LOG_TARGET, "Received a dial error for peer {peer_id:?} that is not being tracked. This means that a new session has cleared the map of pending dials.");
return SessionDialAttempt::PreviousSession;
tracing::debug!(target: LOG_TARGET, "Received a dial error for peer {peer_id:?} that is not being tracked. This means that a new epoch has cleared the map of pending dials.");
return EpochDialAttempt::PreviousEpoch;
};
let new_attempt_number = dial_attempt.attempt_number.checked_add(1).unwrap();
if new_attempt_number > self.max_dial_attempts_per_connection {
tracing::debug!(target: LOG_TARGET, "Maximum attempts ({}) reached for peer {peer_id:?}. Re-dialing stopped.", self.max_dial_attempts_per_connection);
return SessionDialAttempt::OngoingSession(Some(dial_attempt));
return EpochDialAttempt::OngoingEpoch(Some(dial_attempt));
}
let delay = Duration::from_secs(1 << (new_attempt_number.get() - 1));
tracing::debug!(
@ -568,7 +562,7 @@ where
},
)
}));
SessionDialAttempt::OngoingSession(None)
EpochDialAttempt::OngoingEpoch(None)
}
/// Called when a pending retry fires. Re-checks peering degree before
@ -608,7 +602,7 @@ where
.behaviour_mut()
.blend
.with_core_mut()
.publish_message_with_validated_signature_to_current_session(msg)
.publish_message_with_validated_signature_to_current_epoch(msg)
{
tracing::error!(target: LOG_TARGET, "Failed to publish message to blend network: {e:?}");
metrics::outbound_publish_err();
@ -621,14 +615,14 @@ where
&mut self,
msg: &EncapsulatedMessageWithVerifiedSignature,
except: PeerId,
session: u64,
epoch: Epoch,
) {
if let Err(e) = self
.swarm
.behaviour_mut()
.blend
.with_core_mut()
.forward_message_with_validated_signature(msg, except, session)
.forward_message_with_validated_signature(msg, except, epoch)
{
// If we have a single connection, then we will always hit the `NoPeers` error.
// In this case it's ok not to log such error, since this function is only
@ -646,14 +640,14 @@ where
fn report_message_to_service(
&self,
msg: EncapsulatedMessageWithVerifiedSignature,
session: u64,
epoch: Epoch,
message_type: metrics::InboundMessageType,
) {
tracing::trace!(
"Received message from a peer: {msg:?} from session {session:?} of type {message_type:?}."
"Received message from a peer: {msg:?} from epoch {epoch:?} of type {message_type:?}."
);
if self.incoming_message_sender.send((msg, session)).is_err() {
if self.incoming_message_sender.send((msg, epoch)).is_err() {
tracing::trace!(target: LOG_TARGET, "Failed to send incoming message to channel. No active listeners yet.");
metrics::inbound_message_err(message_type);
} else {
@ -693,7 +687,7 @@ where
// Bubble up to service for decapsulation and delaying.
self.report_message_to_service(
msg,
self.public_info.session.session_number,
self.current_epoch_info.1,
metrics::InboundMessageType::Edge,
);
}
@ -703,14 +697,14 @@ where
fn handle_publish_swarm_message(
&mut self,
msg: EncapsulatedMessageWithVerifiedPublicHeader,
intended_session: u64,
intended_epoch: Epoch,
) {
if let Err(e) = self
.swarm
.behaviour_mut()
.blend
.with_core_mut()
.publish_message_with_validated_header(msg, intended_session)
.publish_message_with_validated_header(msg, intended_epoch)
{
tracing::error!(target: LOG_TARGET, "Failed to publish message to blend network: {e:?}");
metrics::outbound_publish_err();
@ -732,8 +726,11 @@ where
identity: &libp2p::identity::Keypair,
behaviour_constructor: BehaviourConstructor,
swarm_messages_receiver: mpsc::Receiver<BlendSwarmMessage>,
incoming_message_sender: broadcast::Sender<(EncapsulatedMessageWithVerifiedSignature, u64)>,
current_public_info: PublicInfo<PeerId>,
incoming_message_sender: broadcast::Sender<(
EncapsulatedMessageWithVerifiedSignature,
Epoch,
)>,
current_epoch_info: BackendEpochInfo<PeerId>,
rng: Rng,
max_dial_attempts_per_connection: NonZeroU64,
minimum_network_size: NonZeroUsize,
@ -744,10 +741,10 @@ where
{
use crate::test_utils::memory_test_swarm;
let membership = current_public_info.session.membership.clone();
let membership = current_epoch_info.0.clone();
Self {
incoming_message_sender,
public_info: current_public_info,
current_epoch_info,
max_dial_attempts_per_connection,
ongoing_dials: HashMap::new(),
pending_retries: FuturesUnordered::new(),

View File

@ -51,7 +51,7 @@ async fn core_message_propagation() {
swarm_1_message_sender
.send(BlendSwarmMessage::Publish {
message: Box::new(message.clone()),
session: 1,
epoch: 1.into(),
})
.await
.unwrap();
@ -59,7 +59,7 @@ async fn core_message_propagation() {
// We test that swarm 1 publishes a message, sending it to swarm 2, the only
// swarm it is connected to. Then swarm 2 forwards it to swarm 3, which is not
// connected to swarm 1.
let (swarm_3_received_message, session) = swarm_3_message_receiver.recv().await.unwrap();
let (swarm_3_received_message, epoch) = swarm_3_message_receiver.recv().await.unwrap();
assert_eq!(swarm_3_received_message, message.into_inner().into());
assert_eq!(session, 1);
assert_eq!(epoch, 1);
}

View File

@ -15,7 +15,7 @@ use crate::{
test_utils::TestEncapsulatedMessage,
};
#[ignore = "TODO: enable this logic after investigating session/epoch transition issues. Test disabled because we don't let connections turn unhealthy because of too little messages now until we have proper observation window values."]
#[ignore = "TODO: enable this logic after investigating epoch transition issues. Test disabled because we don't let connections turn unhealthy because of too little messages now until we have proper observation window values."]
#[test(tokio::test)]
async fn on_unhealthy_peer() {
let (mut identities, mut nodes) = new_nodes_with_empty_address(3);
@ -93,7 +93,7 @@ async fn on_unhealthy_peer() {
assert_eq!(second_swarm_connection_details.role(), Endpoint::Listener);
}
#[ignore = "TODO: enable this logic after investigating session/epoch transition issues. Test disabled because we don't let connections turn spammy because of too many messages now until we have proper observation window values."]
#[ignore = "TODO: enable this logic after investigating epoch transition issues. Test disabled because we don't let connections turn spammy because of too many messages now until we have proper observation window values."]
#[test(tokio::test)]
async fn on_malicious_peer() {
let (mut identities, mut nodes) = new_nodes_with_empty_address(3);
@ -168,13 +168,13 @@ async fn on_malicious_peer() {
.behaviour_mut()
.blend
.with_core_mut()
.force_send_message_to_current_session_peer(&message_1, listening_swarm_peer_id)
.force_send_message_to_current_epoch_peer(&message_1, listening_swarm_peer_id)
.unwrap();
malicious_swarm
.behaviour_mut()
.blend
.with_core_mut()
.force_send_message_to_current_session_peer(&message_2, listening_swarm_peer_id)
.force_send_message_to_current_epoch_peer(&message_2, listening_swarm_peer_id)
.unwrap();
loop {

View File

@ -2,20 +2,15 @@ use core::time::Duration;
use std::collections::HashSet;
use lb_blend::scheduling::membership::Membership;
use lb_core::crypto::ZkHash;
use lb_groth16::Field as _;
use lb_libp2p::{Protocol, SwarmEvent};
use libp2p::{Multiaddr, PeerId};
use test_log::test;
use tokio::{select, time, time::sleep};
use crate::core::backends::{
SessionInfo,
libp2p::{
core_swarm_test_utils::{SwarmExt as _, new_nodes_with_empty_address, update_nodes},
swarm::BlendSwarmMessage,
tests::utils::{BlendBehaviourBuilder, SwarmBuilder, TestSwarm},
},
use crate::core::backends::libp2p::{
core_swarm_test_utils::{SwarmExt as _, new_nodes_with_empty_address, update_nodes},
swarm::BlendSwarmMessage,
tests::utils::{BlendBehaviourBuilder, SwarmBuilder, TestSwarm},
};
#[test(tokio::test)]
@ -289,10 +284,10 @@ async fn core_clears_failed_peers_memory_when_all_exhausted() {
);
}
/// When a new session rotation occurs, pending backoff retries should be
/// When a new epoch rotation occurs, pending backoff retries should be
/// discarded along with ongoing dials.
#[test(tokio::test)]
async fn core_session_rotation_clears_pending_retries() {
async fn core_epoch_rotation_clears_pending_retries() {
let (mut identities, peer_ids) = new_nodes_with_empty_address(1);
let TestSwarm {
swarm: mut dialing_swarm,
@ -315,22 +310,15 @@ async fn core_session_rotation_clears_pending_retries() {
.await;
assert_eq!(dialing_swarm.pending_retries_count(), 1);
// Trigger a new session via the swarm message channel.
let new_session_info = SessionInfo {
membership: Membership::new_without_local(&[]),
session_number: 2,
core_public_inputs: lb_blend::proofs::quota::inputs::prove::public::CoreInputs {
quota: 1,
zk_root: ZkHash::ZERO,
},
};
// Trigger a new epoch via the swarm message channel.
let new_epoch_info = (Membership::new_without_local(&[]), 2.into());
swarm_message_sender
.send(BlendSwarmMessage::StartNewSession(new_session_info))
.send(BlendSwarmMessage::StartNewEpoch(new_epoch_info))
.await
.unwrap();
dialing_swarm.poll_next().await;
// Session rotation should have cleared both ongoing dials and pending retries.
// Epoch rotation should have cleared both ongoing dials and pending retries.
assert!(dialing_swarm.ongoing_dials().is_empty());
assert_eq!(dialing_swarm.pending_retries_count(), 0);
}

View File

@ -15,6 +15,7 @@ use lb_blend::{
},
scheduling::membership::{Membership, Node},
};
use lb_chain_service::Epoch;
use lb_key_management_system_service::keys::UnsecuredEd25519Key;
use lb_libp2p::{Protocol, SwarmEvent};
use lb_utils::blake_rng::BlakeRng;
@ -32,7 +33,7 @@ use tokio_stream::wrappers::IntervalStream;
use crate::{
core::{
backends::{
PublicInfo,
BackendEpochInfo,
libp2p::{BlendSwarm, behaviour::BlendBehaviour, swarm::BlendSwarmMessage},
},
settings::StartingBlendConfig as BlendConfig,
@ -46,7 +47,7 @@ pub struct TestSwarm {
pub swarm: InnerSwarm,
pub swarm_message_sender: mpsc::Sender<BlendSwarmMessage>,
pub incoming_message_receiver:
broadcast::Receiver<(EncapsulatedMessageWithVerifiedSignature, u64)>,
broadcast::Receiver<(EncapsulatedMessageWithVerifiedSignature, Epoch)>,
}
/// Generates `count` nodes with randomly generated identities and empty
@ -92,13 +93,16 @@ pub fn build_membership(
pub struct SwarmBuilder {
identity: Keypair,
public_info: PublicInfo<PeerId>,
public_info: BackendEpochInfo<PeerId>,
max_dial_attempts: Option<NonZeroU64>,
}
impl SwarmBuilder {
pub fn new(identity: Keypair, membership: &[Node<PeerId>]) -> Self {
let public_info = build_membership(membership, Some(identity.public().into())).into();
let public_info = (
build_membership(membership, Some(identity.public().into())),
1.into(),
);
Self {
identity,
public_info,
@ -188,7 +192,7 @@ impl BlendBehaviourBuilder {
expected_message_range: observation_window_values.1,
interval: observation_window_values.0,
},
(self.membership, 1),
(self.membership, 1.into()),
self.peer_id,
PROTOCOL_NAME,
),

View File

@ -78,7 +78,7 @@ where
Self {
blending_ops_per_message: config.num_blend_layers.into(),
maximal_delay_rounds: config.scheduler.delayer.maximum_release_delay_in_rounds,
// TODO: Replace with a session stream: https://github.com/logos-blockchain/logos-blockchain/issues/1533
// TODO: Replace with an epoch stream: https://github.com/logos-blockchain/logos-blockchain/issues/1533
membership_size: NonZeroU64::try_from(membership.size() as u64)
.expect("Membership size cannot be zero."),
minimum_messages_coefficient: config.backend.minimum_messages_coefficient,

Some files were not shown because too many files have changed in this diff Show More