mod bootstrap;
mod session;
use super::{
NnsCertifiedRegistryDeltaBatchReport, NnsCertifiedRegistryDeltaBatchRequest,
NnsCertifiedRegistryMutation, NnsCertifiedRegistryMutationKind, NnsRegistryHostError,
validate_nns_certified_registry_delta_batch,
};
use std::collections::BTreeMap;
use thiserror::Error as ThisError;
pub use bootstrap::{
NnsCertifiedRegistryBootstrapProbeOutcome, NnsCertifiedRegistryBootstrapProbeStatus,
NnsCertifiedRegistryBootstrapRequest, bootstrap_nns_certified_registry_async,
bootstrap_nns_certified_registry_with_source_async, probe_nns_certified_registry_async,
probe_nns_certified_registry_with_source_async,
};
pub use session::{NnsRegistryReplaySession, NnsRegistryReplaySessionLimits};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct NnsRegistryReplayLimits {
pub max_entries: usize,
pub max_content_bytes: usize,
}
impl NnsRegistryReplayLimits {
#[must_use]
pub const fn new(max_entries: usize, max_content_bytes: usize) -> Self {
Self {
max_entries,
max_content_bytes,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NnsRegistryReplayValue {
value: Vec<u8>,
last_mutation_version: u64,
timestamp_nanoseconds: u64,
}
impl NnsRegistryReplayValue {
#[must_use]
pub fn value(&self) -> &[u8] {
&self.value
}
#[must_use]
pub const fn last_mutation_version(&self) -> u64 {
self.last_mutation_version
}
#[must_use]
pub const fn timestamp_nanoseconds(&self) -> u64 {
self.timestamp_nanoseconds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct NnsRegistryReplayState {
through_version: u64,
content_bytes: usize,
entries: BTreeMap<Vec<u8>, NnsRegistryReplayValue>,
}
impl NnsRegistryReplayState {
#[must_use]
pub const fn new() -> Self {
Self {
through_version: 0,
content_bytes: 0,
entries: BTreeMap::new(),
}
}
#[must_use]
pub const fn through_version(&self) -> u64 {
self.through_version
}
#[must_use]
pub fn entry_count(&self) -> usize {
self.entries.len()
}
#[must_use]
pub const fn content_bytes(&self) -> usize {
self.content_bytes
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn get(&self, key: &[u8]) -> Option<&NnsRegistryReplayValue> {
self.entries.get(key)
}
pub fn entries(&self) -> impl Iterator<Item = (&[u8], &NnsRegistryReplayValue)> {
self.entries
.iter()
.map(|(key, value)| (key.as_slice(), value))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NnsRegistryReplayProgress {
pub previous_version: u64,
pub through_version: u64,
pub applied_version_count: usize,
pub applied_mutation_count: usize,
pub entry_count: usize,
pub content_bytes: usize,
pub complete_at_certified_latest_version: bool,
}
#[derive(Debug, ThisError)]
pub enum NnsRegistryReplayError {
#[error(transparent)]
InvalidBatch(#[from] NnsRegistryHostError),
#[error(
"Registry replay state is at version {state_version}, but the batch starts after version {requested_version}"
)]
VersionMismatch {
state_version: u64,
requested_version: u64,
},
#[error("Registry replay session already reached selected version {selected_version}")]
SessionComplete {
selected_version: u64,
},
#[error(
"Registry replay selected version {selected_version}, but a later batch certifies only version {certified_latest_version}"
)]
CertifiedVersionRegressed {
selected_version: u64,
certified_latest_version: u64,
},
#[error(
"Registry replay root-key digest changed from {expected_root_key_digest} to {actual_root_key_digest}"
)]
RootKeyDigestMismatch {
expected_root_key_digest: String,
actual_root_key_digest: String,
},
#[error("Registry replay session {field} would be {actual}; caller maximum is {maximum}")]
SessionLimitExceeded {
field: &'static str,
maximum: u64,
actual: u64,
},
#[error("Registry replay {field} is {actual}; caller maximum is {maximum}")]
LimitExceeded {
field: &'static str,
maximum: usize,
actual: usize,
},
#[error("validated Registry replay {field} is not lowercase hexadecimal")]
InvalidHex {
field: &'static str,
},
#[error("Registry replay byte accounting overflowed or became inconsistent")]
Accounting,
}
pub fn apply_nns_certified_registry_delta_batch(
state: &mut NnsRegistryReplayState,
request: &NnsCertifiedRegistryDeltaBatchRequest,
report: &NnsCertifiedRegistryDeltaBatchReport,
limits: NnsRegistryReplayLimits,
) -> Result<NnsRegistryReplayProgress, NnsRegistryReplayError> {
validate_nns_certified_registry_delta_batch(request, report)?;
apply_validated_batch_through(
state,
report,
report.last_version.unwrap_or(report.requested_version),
limits,
)
}
pub(super) fn apply_validated_batch_through(
state: &mut NnsRegistryReplayState,
report: &NnsCertifiedRegistryDeltaBatchReport,
selected_version: u64,
limits: NnsRegistryReplayLimits,
) -> Result<NnsRegistryReplayProgress, NnsRegistryReplayError> {
if state.through_version != report.requested_version {
return Err(NnsRegistryReplayError::VersionMismatch {
state_version: state.through_version,
requested_version: report.requested_version,
});
}
enforce_limits(state, limits)?;
let (applied_version_count, applied_mutation_count) =
validated_batch_prefix_counts(report, selected_version)?;
let mut journal = ReplayJournal::new(state);
let application = (|| {
for version in &report.versions[..applied_version_count] {
for mutation in &version.mutations {
apply_committed_mutation(
state,
&mut journal,
version.version,
version.timestamp_nanoseconds,
mutation,
limits,
)?;
}
state.through_version = version.version;
}
enforce_limits(state, limits)
})();
if let Err(error) = application {
journal.rollback(state);
return Err(error);
}
let progress = NnsRegistryReplayProgress {
previous_version: journal.previous_version,
through_version: state.through_version,
applied_version_count,
applied_mutation_count,
entry_count: state.entries.len(),
content_bytes: state.content_bytes,
complete_at_certified_latest_version: state.through_version
== report.certified_latest_version,
};
Ok(progress)
}
pub(super) fn validated_batch_prefix_counts(
report: &NnsCertifiedRegistryDeltaBatchReport,
selected_version: u64,
) -> Result<(usize, usize), NnsRegistryReplayError> {
let version_count = report
.versions
.partition_point(|version| version.version <= selected_version);
let mutation_count =
report.versions[..version_count]
.iter()
.try_fold(0usize, |total, version| {
total
.checked_add(version.mutations.len())
.ok_or(NnsRegistryReplayError::Accounting)
})?;
Ok((version_count, mutation_count))
}
fn apply_committed_mutation(
state: &mut NnsRegistryReplayState,
journal: &mut ReplayJournal,
version: u64,
timestamp_nanoseconds: u64,
mutation: &NnsCertifiedRegistryMutation,
limits: NnsRegistryReplayLimits,
) -> Result<(), NnsRegistryReplayError> {
let key = decode_hex("mutation key", &mutation.key_hex)?;
if mutation.mutation_kind == NnsCertifiedRegistryMutationKind::Delete {
let prior_content_bytes = journal.remove_current(state, &key)?;
state.content_bytes = state
.content_bytes
.checked_sub(prior_content_bytes)
.ok_or(NnsRegistryReplayError::Accounting)?;
return Ok(());
}
let value_hex = mutation
.value_hex
.as_deref()
.ok_or(NnsRegistryReplayError::InvalidHex {
field: "mutation value",
})?;
let value_bytes = value_hex.len() / 2;
let prior_content_bytes = journal.remove_current(state, &key)?;
let candidate_content_bytes = state
.content_bytes
.checked_sub(prior_content_bytes)
.and_then(|bytes| bytes.checked_add(key.len()))
.and_then(|bytes| bytes.checked_add(value_bytes))
.ok_or(NnsRegistryReplayError::Accounting)?;
enforce_limit(
"content bytes",
candidate_content_bytes,
limits.max_content_bytes,
)?;
let candidate_entry_count = state
.entries
.len()
.checked_add(1)
.ok_or(NnsRegistryReplayError::Accounting)?;
enforce_limit("entry count", candidate_entry_count, limits.max_entries)?;
state.entries.insert(
key,
NnsRegistryReplayValue {
value: decode_hex("mutation value", value_hex)?,
last_mutation_version: version,
timestamp_nanoseconds,
},
);
state.content_bytes = candidate_content_bytes;
Ok(())
}
struct ReplayJournal {
previous_version: u64,
previous_content_bytes: usize,
entries: BTreeMap<Vec<u8>, Option<NnsRegistryReplayValue>>,
}
impl ReplayJournal {
const fn new(state: &NnsRegistryReplayState) -> Self {
Self {
previous_version: state.through_version,
previous_content_bytes: state.content_bytes,
entries: BTreeMap::new(),
}
}
fn remove_current(
&mut self,
state: &mut NnsRegistryReplayState,
key: &[u8],
) -> Result<usize, NnsRegistryReplayError> {
let current = state.entries.remove(key);
let content_bytes = current.as_ref().map_or(Ok(0), |value| {
key.len()
.checked_add(value.value.len())
.ok_or(NnsRegistryReplayError::Accounting)
})?;
self.entries.entry(key.to_vec()).or_insert(current);
Ok(content_bytes)
}
fn rollback(self, state: &mut NnsRegistryReplayState) {
for (key, original) in self.entries {
state.entries.remove(&key);
if let Some(value) = original {
state.entries.insert(key, value);
}
}
state.through_version = self.previous_version;
state.content_bytes = self.previous_content_bytes;
}
}
fn enforce_limits(
state: &NnsRegistryReplayState,
limits: NnsRegistryReplayLimits,
) -> Result<(), NnsRegistryReplayError> {
enforce_limit("entry count", state.entries.len(), limits.max_entries)?;
enforce_limit(
"content bytes",
state.content_bytes,
limits.max_content_bytes,
)
}
const fn enforce_limit(
field: &'static str,
actual: usize,
maximum: usize,
) -> Result<(), NnsRegistryReplayError> {
if actual > maximum {
Err(NnsRegistryReplayError::LimitExceeded {
field,
maximum,
actual,
})
} else {
Ok(())
}
}
fn decode_hex(field: &'static str, value: &str) -> Result<Vec<u8>, NnsRegistryReplayError> {
if !value.len().is_multiple_of(2) || !crate::hex::is_lowercase_hex(value) {
return Err(NnsRegistryReplayError::InvalidHex { field });
}
(0..value.len())
.step_by(2)
.map(|index| {
u8::from_str_radix(&value[index..index + 2], 16)
.map_err(|_| NnsRegistryReplayError::InvalidHex { field })
})
.collect()
}
#[cfg(test)]
mod tests;