use ozzy_proto::{GroupId, NodeId};
use crate::{Configuration, Digest, QuorumPolicy, ReplicationError};
pub const CONFIGURATION_RECORD_BYTES: usize = 256;
const HASH_CONTEXT: &str = "ozzy fixed durable replica configuration v1";
const MAGIC: &[u8; 8] = b"OZYVOTER";
const DIGEST_START: usize = 192;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConfiguredVoter {
pub node_id: NodeId,
pub principal: Digest,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConfigurationRecord {
configuration: Configuration,
voters: [ConfiguredVoter; 3],
}
impl ConfigurationRecord {
pub fn new(
group_id: GroupId,
configuration_epoch: u64,
voters: [ConfiguredVoter; 3],
) -> Result<Self, ConfigurationRecordError> {
Self::with_policy(group_id, configuration_epoch, voters, QuorumPolicy::Durable)
}
pub fn with_policy(
group_id: GroupId,
configuration_epoch: u64,
voters: [ConfiguredVoter; 3],
policy: QuorumPolicy,
) -> Result<Self, ConfigurationRecordError> {
if configuration_epoch == 0
|| voters.iter().any(|voter| voter.principal == Digest::ZERO)
|| voters[0].principal == voters[1].principal
|| voters[0].principal == voters[2].principal
|| voters[1].principal == voters[2].principal
{
return Err(ConfigurationRecordError::InvalidConfiguration);
}
let bytes = unsigned_record(group_id, configuration_epoch, &voters, policy);
let mut configuration = Configuration::new(
group_id,
configuration_epoch,
record_digest(&bytes),
voters.map(|voter| voter.node_id),
)?;
configuration.policy = policy;
Ok(Self {
configuration,
voters,
})
}
pub const fn configuration(self) -> Configuration {
self.configuration
}
pub const fn voters(&self) -> &[ConfiguredVoter; 3] {
&self.voters
}
pub fn encode(self) -> [u8; CONFIGURATION_RECORD_BYTES] {
let scope = self.configuration.scope();
let mut bytes = unsigned_record(
scope.group_id,
scope.configuration_epoch,
&self.voters,
self.configuration.policy(),
);
bytes[DIGEST_START..DIGEST_START + 32]
.copy_from_slice(scope.configuration_digest.as_bytes());
bytes
}
pub fn decode(bytes: &[u8]) -> Result<Self, ConfigurationRecordError> {
if bytes.len() != CONFIGURATION_RECORD_BYTES {
return Err(ConfigurationRecordError::Length);
}
if &bytes[..8] != MAGIC
|| bytes[8..12] != [0, 2, 1, 0]
|| !matches!(bytes[12], 1 | 2)
|| bytes[13..16] != [1, ozzy_proto::VERSION, 1]
|| bytes[40] != 3
|| bytes[41..48].iter().any(|&byte| byte != 0)
|| bytes[224..].iter().any(|&byte| byte != 0)
{
return Err(ConfigurationRecordError::UnsupportedFields);
}
let stored = Digest::from_bytes(
bytes[DIGEST_START..DIGEST_START + 32]
.try_into()
.expect("fixed digest"),
);
if record_digest(bytes) != stored {
return Err(ConfigurationRecordError::DigestMismatch);
}
let group = GroupId::from_bytes(bytes[16..32].try_into().expect("fixed group"));
let epoch = u64::from_be_bytes(bytes[32..40].try_into().expect("fixed epoch"));
let voters = std::array::from_fn(|index| {
let start = 48 + index * 48;
ConfiguredVoter {
node_id: NodeId::from_bytes(
bytes[start..start + 16].try_into().expect("fixed voter"),
),
principal: Digest::from_bytes(
bytes[start + 16..start + 48]
.try_into()
.expect("fixed principal"),
),
}
});
let policy = if bytes[12] == 1 {
QuorumPolicy::Durable
} else {
QuorumPolicy::Replicated
};
let record = Self::with_policy(group, epoch, voters, policy)?;
debug_assert_eq!(record.configuration.scope().configuration_digest, stored);
Ok(record)
}
}
fn unsigned_record(
group: GroupId,
epoch: u64,
voters: &[ConfiguredVoter; 3],
policy: QuorumPolicy,
) -> [u8; CONFIGURATION_RECORD_BYTES] {
let mut bytes = [0; CONFIGURATION_RECORD_BYTES];
bytes[..8].copy_from_slice(MAGIC);
bytes[8..16].copy_from_slice(&[0, 2, 1, 0, policy as u8, 1, ozzy_proto::VERSION, 1]);
bytes[16..32].copy_from_slice(group.as_bytes());
bytes[32..40].copy_from_slice(&epoch.to_be_bytes());
bytes[40] = 3;
for (index, voter) in voters.iter().enumerate() {
let start = 48 + index * 48;
bytes[start..start + 16].copy_from_slice(voter.node_id.as_bytes());
bytes[start + 16..start + 48].copy_from_slice(voter.principal.as_bytes());
}
bytes
}
fn record_digest(bytes: &[u8]) -> Digest {
let mut hasher = ozzy_journal::integrity::IntegrityHasher::new(HASH_CONTEXT);
hasher.update(&bytes[..DIGEST_START]);
hasher.update(&[0; 32]);
hasher.update(&bytes[DIGEST_START + 32..]);
hasher.finish()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ConfigurationRecordError {
#[error("invalid replica configuration record length")]
Length,
#[error("unsupported replica configuration record fields")]
UnsupportedFields,
#[error("replica configuration record digest mismatch")]
DigestMismatch,
#[error("invalid replica configuration principals or epoch")]
InvalidConfiguration,
#[error(transparent)]
Replication(#[from] ReplicationError),
}