use crate::stateful::{
db::{Anchor, DatabaseSet},
Application,
};
use commonware_codec::{EncodeSize, Error, FixedSize, Read, ReadExt, Write};
use commonware_consensus::{
marshal::{
core::{CommitmentFallback, Mailbox as MarshalMailbox, Variant},
Identifier,
},
simplex::types::Finalization,
types::Height,
CertifiableBlock, Heightable, Roundable,
};
use commonware_cryptography::{certificate::Scheme, Digest, Digestible};
use commonware_runtime::{Buf, BufMut, Clock, Metrics, Spawner};
use commonware_storage::{
metadata::{self, Metadata},
Context,
};
use commonware_utils::{fixed_bytes, sequence::FixedBytes};
use rand_core::Rng;
mod actor;
pub(crate) use actor::{Config, Syncer};
mod mailbox;
pub(crate) use mailbox::Mailbox;
mod plan;
pub use plan::SyncPlan;
const SYNC_METADATA_SUFFIX: &str = "state_sync_metadata";
const SYNC_STATE_KEY: FixedBytes<1> = fixed_bytes!("C0");
type BlockDigest<A, E> = <<A as Application<E>>::Block as Digestible>::Digest;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum SyncState<S, C>
where
S: Scheme,
C: Digest,
{
InProgress(Finalization<S, C>),
Complete(Height),
}
impl<S, C> SyncState<S, C>
where
S: Scheme,
C: Digest,
{
pub(crate) const fn sync_height(&self) -> Option<Height> {
match self {
Self::InProgress(_) => None,
Self::Complete(height) => Some(*height),
}
}
}
impl<S, C> Write for SyncState<S, C>
where
S: Scheme,
C: Digest,
{
fn write(&self, writer: &mut impl BufMut) {
match self {
Self::InProgress(floor) => {
0u8.write(writer);
floor.write(writer);
}
Self::Complete(height) => {
1u8.write(writer);
height.write(writer);
}
}
}
}
impl<S, C> EncodeSize for SyncState<S, C>
where
S: Scheme,
C: Digest,
{
fn encode_size(&self) -> usize {
u8::SIZE
+ match self {
Self::InProgress(floor) => floor.encode_size(),
Self::Complete(height) => height.encode_size(),
}
}
}
impl<S, C> Read for SyncState<S, C>
where
S: Scheme,
C: Digest,
{
type Cfg = <S::Certificate as Read>::Cfg;
fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, Error> {
match u8::read(reader)? {
0 => Ok(Self::InProgress(Finalization::read_cfg(reader, cfg)?)),
1 => Ok(Self::Complete(Height::read(reader)?)),
n => Err(Error::InvalidEnum(n)),
}
}
}
#[cfg(feature = "arbitrary")]
impl<'a, S, C> arbitrary::Arbitrary<'a> for SyncState<S, C>
where
S: Scheme,
S::Certificate: for<'b> arbitrary::Arbitrary<'b>,
C: Digest + for<'b> arbitrary::Arbitrary<'b>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
Ok(if u.arbitrary::<bool>()? {
Self::InProgress(u.arbitrary()?)
} else {
Self::Complete(u.arbitrary()?)
})
}
}
pub struct SyncResult<E, A>
where
E: Rng + Spawner + Metrics + Clock,
A: Application<E>,
{
pub databases: A::Databases,
pub anchor: Anchor<BlockDigest<A, E>>,
}
impl<E, A> Clone for SyncResult<E, A>
where
E: Rng + Spawner + Metrics + Clock,
A: Application<E>,
{
fn clone(&self) -> Self {
Self {
databases: self.databases.clone(),
anchor: self.anchor,
}
}
}
pub(crate) struct ResolvedFloor<E, A>
where
E: Rng + Spawner + Metrics + Clock,
A: Application<E>,
{
pub anchor: Anchor<BlockDigest<A, E>>,
pub targets: <A::Databases as DatabaseSet<E>>::SyncTargets,
}
pub(crate) struct StateSyncMetadata<E, S, C>
where
E: Context,
S: Scheme,
C: Digest,
{
partition_prefix: String,
metadata: Metadata<E, FixedBytes<1>, SyncState<S, C>>,
}
impl<E, S, C> StateSyncMetadata<E, S, C>
where
E: Context,
S: Scheme,
C: Digest,
{
pub(crate) async fn init(context: &E, partition_prefix: impl AsRef<str>) -> Self {
let partition_prefix = partition_prefix.as_ref().to_string();
let metadata = Metadata::init(
context.child("metadata"),
metadata::Config {
partition: format!("{partition_prefix}{SYNC_METADATA_SUFFIX}"),
codec_config: S::certificate_codec_config_unbounded(),
},
)
.await
.expect("failed to load sync metadata");
Self {
partition_prefix,
metadata,
}
}
pub(crate) const fn partition_prefix(&self) -> &str {
self.partition_prefix.as_str()
}
pub(crate) fn sync_height(&self) -> Option<Height> {
self.metadata
.get(&SYNC_STATE_KEY)
.map(SyncState::sync_height)
.unwrap_or_default()
}
pub(crate) fn in_progress(&self) -> bool {
matches!(
self.metadata.get(&SYNC_STATE_KEY),
Some(SyncState::InProgress(_))
)
}
pub(crate) fn in_progress_floor(&self) -> Option<&Finalization<S, C>> {
match self.metadata.get(&SYNC_STATE_KEY) {
Some(SyncState::InProgress(floor)) => Some(floor),
_ => None,
}
}
pub(crate) async fn begin_sync(&mut self, floor: Finalization<S, C>) {
match self.metadata.get(&SYNC_STATE_KEY) {
Some(SyncState::InProgress(existing)) => {
assert!(
floor.round() >= existing.round(),
"selected state sync floor cannot move behind the persisted in-progress floor",
);
if floor.round() == existing.round() {
assert!(
floor.proposal.payload == existing.proposal.payload,
"selected state sync floor conflicts with the persisted in-progress round",
);
}
}
Some(SyncState::Complete(_)) => {
panic!("completed state sync cannot be marked in-progress");
}
None => {}
}
self.metadata
.put_sync(SYNC_STATE_KEY, SyncState::InProgress(floor))
.await
.expect("failed to set state sync state to in-progress");
}
pub(crate) async fn set_complete(&mut self, height: Height) {
if let Some(SyncState::Complete(existing)) = self.metadata.get(&SYNC_STATE_KEY) {
assert!(
height >= *existing,
"completed state sync height cannot move backward",
);
}
self.metadata
.put_sync(SYNC_STATE_KEY, SyncState::Complete(height))
.await
.expect("failed to set state sync state to complete");
}
}
pub(crate) async fn resolve_state_sync_floor<E, A, S, V>(
marshal: &MarshalMailbox<S, V>,
finalization: &Finalization<S, V::Commitment>,
) -> ResolvedFloor<E, A>
where
E: Rng + Spawner + Metrics + Clock,
A: Application<E>,
S: Scheme,
V: Variant<ApplicationBlock = A::Block>,
{
let floor = {
let block = marshal
.subscribe_by_commitment(finalization.proposal.payload, CommitmentFallback::Wait)
.await
.expect("marshal must yield floor block");
V::into_inner_shared(block)
};
ResolvedFloor {
anchor: Anchor::from(floor.as_ref()),
targets: A::sync_targets(floor.as_ref()),
}
}
pub(crate) struct StartupResult<E, A>
where
E: Rng + Spawner + Metrics + Clock,
A: Application<E>,
{
pub sync: SyncResult<E, A>,
pub skip_finalized_until: Option<Height>,
}
pub(crate) async fn init_databases_from_marshal<E, A, S, V>(
context: &E,
marshal: &MarshalMailbox<S, V>,
db_config: <A::Databases as DatabaseSet<E>>::Config,
mut sync_metadata: StateSyncMetadata<E, S, V::Commitment>,
) -> StartupResult<E, A>
where
E: Rng + Spawner + Context,
A: Application<E>,
S: Scheme,
V: Variant<ApplicationBlock = A::Block>,
{
let sync_height = sync_metadata.sync_height();
let processed_height = marshal.get_processed_height().await;
let skip_finalized_until = match (sync_height, processed_height) {
(Some(sync_height), Some(processed_height)) if processed_height < sync_height => {
Some(sync_height)
}
(Some(sync_height), None) => Some(sync_height),
_ => None,
};
let marshal_floor = sync_height
.into_iter()
.chain(processed_height)
.max()
.unwrap_or_else(Height::zero);
let floor_block = {
let marshal_block = marshal
.get_block(Identifier::Height(marshal_floor))
.await
.expect("marshal must return floor block");
V::into_inner(marshal_block)
};
let databases = A::Databases::init(context.child("db_set"), db_config).await;
let processed_targets = A::sync_targets(&floor_block);
if databases.committed_targets().await != processed_targets {
databases.rewind_to_targets(processed_targets.clone()).await;
let rewound_targets = databases.committed_targets().await;
assert!(
rewound_targets == processed_targets,
"databases must be consistent with marshal floor after rewind"
);
}
sync_metadata.set_complete(floor_block.height()).await;
let anchor = Anchor {
height: floor_block.height(),
round: floor_block.context().round(),
digest: floor_block.digest(),
};
StartupResult {
sync: SyncResult { databases, anchor },
skip_finalized_until,
}
}
#[cfg(all(test, feature = "arbitrary"))]
mod tests {
mod conformance {
use crate::stateful::{actor::syncer::SyncState, tests::mocks::TestScheme};
use commonware_codec::conformance::CodecConformance;
use commonware_cryptography::sha256::Digest as Sha256Digest;
commonware_conformance::conformance_tests! {
CodecConformance<SyncState<TestScheme, Sha256Digest>>,
}
}
}