use super::StateSyncMetadata;
use commonware_consensus::{
marshal::{core::Variant, Start},
simplex::types::Finalization,
types::Height,
};
use commonware_cryptography::certificate::Scheme;
use commonware_storage::Context;
use tracing::warn;
pub struct SyncPlan<E, S, V>
where
E: Context,
S: Scheme,
V: Variant,
{
sync_metadata: StateSyncMetadata<E, S, V::Commitment>,
floor: Option<Finalization<S, V::Commitment>>,
}
impl<E, S, V> SyncPlan<E, S, V>
where
E: Context,
S: Scheme,
V: Variant,
{
pub async fn init(context: &E, partition_prefix: impl AsRef<str>) -> Self {
let sync_metadata =
StateSyncMetadata::<E, S, V::Commitment>::init(context, partition_prefix).await;
let floor = sync_metadata.in_progress_floor().cloned();
Self {
sync_metadata,
floor,
}
}
pub fn may_state_sync(&self) -> bool {
self.sync_metadata.sync_height().is_none()
}
pub fn sync_height(&self) -> Option<Height> {
self.sync_metadata.sync_height()
}
pub const fn partition_prefix(&self) -> &str {
self.sync_metadata.partition_prefix()
}
pub const fn floor(&self) -> Option<&Finalization<S, V::Commitment>> {
self.floor.as_ref()
}
#[must_use]
pub fn with_floor(mut self, floor: Finalization<S, V::Commitment>) -> Self {
if !self.may_state_sync() {
return self;
}
if let Some(selected) = &self.floor {
if floor.round() <= selected.round() {
warn!(
candidate = ?floor.round(),
selected = ?selected.round(),
"state sync floor not updated, candidate is not newer",
);
return self;
}
}
self.floor = Some(floor);
self
}
pub fn marshal_start<B>(&self, genesis: B) -> Start<S, V::Commitment, B> {
self.floor
.as_ref()
.cloned()
.map_or_else(|| Start::Genesis(genesis), Start::Floor)
}
pub fn requires_state_sync_floor(&self) -> bool {
self.sync_metadata.in_progress()
}
pub fn should_state_sync(&self, requested: bool) -> bool {
self.may_state_sync() && (requested || self.requires_state_sync_floor())
}
pub(crate) fn into_sync_metadata(self) -> StateSyncMetadata<E, S, V::Commitment> {
self.sync_metadata
}
}
#[cfg(test)]
mod tests {
use super::SyncPlan;
use crate::stateful::{
actor::syncer::StateSyncMetadata,
tests::mocks::{TestScheme, TestVariant},
};
use commonware_consensus::{
marshal::Start,
simplex::{
mocks::scheme as scheme_mocks,
types::{Finalization, Finalize, Proposal},
},
types::{Epoch, Height, Round, View},
};
use commonware_cryptography::sha256::{Digest as Sha256Digest, Sha256};
use commonware_parallel::Sequential;
use commonware_runtime::{deterministic, Runner as _};
fn finalization(
schemes: &[TestScheme],
view: u64,
digest_byte: u8,
) -> Finalization<TestScheme, Sha256Digest> {
let proposal = Proposal {
round: Round::new(Epoch::zero(), View::new(view)),
parent: View::new(view.saturating_sub(1)),
payload: Sha256::fill(digest_byte),
};
let finalizes = schemes
.iter()
.map(|scheme| Finalize::sign(scheme, proposal.clone()).expect("sign finalize"))
.collect::<Vec<_>>();
Finalization::from_finalizes(&schemes[0], &finalizes, &Sequential)
.expect("recover finalization")
}
#[test]
fn stored_sync_height_disables_state_sync() {
deterministic::Runner::default().start(|context| async move {
let partition_prefix = "stored_sync_height";
let plan =
SyncPlan::<_, TestScheme, TestVariant>::init(&context, partition_prefix).await;
assert!(plan.may_state_sync());
assert!(plan.should_state_sync(true));
assert!(!plan.should_state_sync(false));
assert_eq!(plan.sync_height(), None);
drop(plan);
let mut metadata =
StateSyncMetadata::<_, TestScheme, Sha256Digest>::init(&context, partition_prefix)
.await;
metadata.set_complete(Height::new(7)).await;
drop(metadata);
let plan =
SyncPlan::<_, TestScheme, TestVariant>::init(&context, partition_prefix).await;
assert!(!plan.may_state_sync());
assert!(!plan.should_state_sync(true));
assert_eq!(plan.sync_height(), Some(Height::new(7)));
assert!(plan.floor().is_none());
});
}
#[test]
#[should_panic(expected = "completed state sync cannot be marked in-progress")]
fn completed_sync_cannot_be_marked_in_progress() {
deterministic::Runner::default().start(|mut context| async move {
let partition_prefix = "completed_sync_cannot_be_marked_in_progress";
let fixture = scheme_mocks::fixture(&mut context, b"_COMMONWARE_GLUE_SYNC_PLAN", 1);
let mut metadata =
StateSyncMetadata::<_, TestScheme, Sha256Digest>::init(&context, partition_prefix)
.await;
metadata.set_complete(Height::new(7)).await;
metadata
.begin_sync(finalization(&fixture.schemes, 8, 8))
.await;
});
}
#[test]
#[should_panic(expected = "completed state sync height cannot move backward")]
fn complete_height_cannot_move_backward() {
deterministic::Runner::default().start(|context| async move {
let partition_prefix = "complete_height_cannot_move_backward";
let mut metadata =
StateSyncMetadata::<_, TestScheme, Sha256Digest>::init(&context, partition_prefix)
.await;
metadata.set_complete(Height::new(7)).await;
metadata.set_complete(Height::new(6)).await;
});
}
#[test]
fn in_progress_sync_requires_compatible_floor() {
deterministic::Runner::default().start(|mut context| async move {
let partition_prefix = "in_progress_sync_requires_compatible_floor";
let fixture = scheme_mocks::fixture(&mut context, b"_COMMONWARE_GLUE_SYNC_PLAN", 1);
let stored = finalization(&fixture.schemes, 7, 7);
let mut metadata =
StateSyncMetadata::<_, TestScheme, Sha256Digest>::init(&context, partition_prefix)
.await;
metadata.begin_sync(stored.clone()).await;
drop(metadata);
let mut plan =
SyncPlan::<_, TestScheme, TestVariant>::init(&context, partition_prefix).await;
assert!(plan.may_state_sync());
assert!(plan.requires_state_sync_floor());
assert!(plan.should_state_sync(false));
plan.sync_metadata.begin_sync(stored).await;
plan.sync_metadata
.begin_sync(finalization(&fixture.schemes, 9, 9))
.await;
});
}
#[test]
fn interrupted_sync_reuses_persisted_floor_when_probe_lags() {
deterministic::Runner::default().start(|mut context| async move {
let partition_prefix = "interrupted_sync_reuses_persisted_floor";
let fixture = scheme_mocks::fixture(&mut context, b"_COMMONWARE_GLUE_SYNC_PLAN", 1);
let stored = finalization(&fixture.schemes, 7, 7);
let mut metadata =
StateSyncMetadata::<_, TestScheme, Sha256Digest>::init(&context, partition_prefix)
.await;
metadata.begin_sync(stored.clone()).await;
drop(metadata);
let plan =
SyncPlan::<_, TestScheme, TestVariant>::init(&context, partition_prefix).await;
assert!(plan.should_state_sync(false));
assert_eq!(
plan.floor().expect("interrupted sync must have a floor"),
&stored,
);
assert!(matches!(
plan.marshal_start(()),
Start::Floor(ref floor) if floor == &stored
));
let plan =
SyncPlan::<_, TestScheme, TestVariant>::init(&context, partition_prefix).await;
let plan = plan.with_floor(finalization(&fixture.schemes, 6, 6));
assert_eq!(
plan.floor().expect("interrupted sync must have a floor"),
&stored,
"a lagging probe must not replace the persisted in-progress floor",
);
let newer = finalization(&fixture.schemes, 9, 9);
let plan =
SyncPlan::<_, TestScheme, TestVariant>::init(&context, partition_prefix).await;
let plan = plan.with_floor(newer.clone());
assert_eq!(plan.floor(), Some(&newer));
});
}
#[test]
fn with_floor_does_not_replace_newer_selection() {
deterministic::Runner::default().start(|mut context| async move {
let fixture = scheme_mocks::fixture(&mut context, b"_COMMONWARE_GLUE_SYNC_PLAN", 1);
let newer = finalization(&fixture.schemes, 9, 9);
let plan = SyncPlan::<_, TestScheme, TestVariant>::init(
&context,
"with_floor_does_not_replace_newer_selection",
)
.await;
let plan =
plan.with_floor(newer.clone())
.with_floor(finalization(&fixture.schemes, 8, 8));
assert_eq!(plan.floor(), Some(&newer));
});
}
#[test]
#[should_panic(
expected = "selected state sync floor cannot move behind the persisted in-progress floor"
)]
fn in_progress_sync_panics_for_backward_floor() {
deterministic::Runner::default().start(|mut context| async move {
let fixture = scheme_mocks::fixture(&mut context, b"_COMMONWARE_GLUE_SYNC_PLAN", 1);
let mut metadata = StateSyncMetadata::<_, TestScheme, Sha256Digest>::init(
&context,
"in_progress_sync_panics_for_backward_floor",
)
.await;
metadata
.begin_sync(finalization(&fixture.schemes, 7, 7))
.await;
metadata
.begin_sync(finalization(&fixture.schemes, 6, 6))
.await;
});
}
#[test]
#[should_panic(
expected = "selected state sync floor conflicts with the persisted in-progress round"
)]
fn in_progress_sync_panics_for_conflicting_round() {
deterministic::Runner::default().start(|mut context| async move {
let fixture = scheme_mocks::fixture(&mut context, b"_COMMONWARE_GLUE_SYNC_PLAN", 1);
let mut metadata = StateSyncMetadata::<_, TestScheme, Sha256Digest>::init(
&context,
"in_progress_sync_panics_for_conflicting_round",
)
.await;
metadata
.begin_sync(finalization(&fixture.schemes, 7, 7))
.await;
metadata
.begin_sync(finalization(&fixture.schemes, 7, 8))
.await;
});
}
}