ozzy_replication/
configuration_record.rs1use ozzy_proto::{GroupId, NodeId};
2
3use crate::{Configuration, Digest, QuorumPolicy, ReplicationError};
4
5pub const CONFIGURATION_RECORD_BYTES: usize = 256;
7const HASH_CONTEXT: &str = "ozzy fixed durable replica configuration v1";
8const MAGIC: &[u8; 8] = b"OZYVOTER";
9const DIGEST_START: usize = 192;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct ConfiguredVoter {
14 pub node_id: NodeId,
16 pub principal: Digest,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct ConfigurationRecord {
30 configuration: Configuration,
31 voters: [ConfiguredVoter; 3],
32}
33
34impl ConfigurationRecord {
35 pub fn new(
38 group_id: GroupId,
39 configuration_epoch: u64,
40 voters: [ConfiguredVoter; 3],
41 ) -> Result<Self, ConfigurationRecordError> {
42 Self::with_policy(group_id, configuration_epoch, voters, QuorumPolicy::Durable)
43 }
44
45 pub fn with_policy(
47 group_id: GroupId,
48 configuration_epoch: u64,
49 voters: [ConfiguredVoter; 3],
50 policy: QuorumPolicy,
51 ) -> Result<Self, ConfigurationRecordError> {
52 if configuration_epoch == 0
53 || voters.iter().any(|voter| voter.principal == Digest::ZERO)
54 || voters[0].principal == voters[1].principal
55 || voters[0].principal == voters[2].principal
56 || voters[1].principal == voters[2].principal
57 {
58 return Err(ConfigurationRecordError::InvalidConfiguration);
59 }
60 let bytes = unsigned_record(group_id, configuration_epoch, &voters, policy);
61 let mut configuration = Configuration::new(
62 group_id,
63 configuration_epoch,
64 record_digest(&bytes),
65 voters.map(|voter| voter.node_id),
66 )?;
67 configuration.policy = policy;
68 Ok(Self {
69 configuration,
70 voters,
71 })
72 }
73
74 pub const fn configuration(self) -> Configuration {
76 self.configuration
77 }
78
79 pub const fn voters(&self) -> &[ConfiguredVoter; 3] {
81 &self.voters
82 }
83
84 pub fn encode(self) -> [u8; CONFIGURATION_RECORD_BYTES] {
86 let scope = self.configuration.scope();
87 let mut bytes = unsigned_record(
88 scope.group_id,
89 scope.configuration_epoch,
90 &self.voters,
91 self.configuration.policy(),
92 );
93 bytes[DIGEST_START..DIGEST_START + 32]
94 .copy_from_slice(scope.configuration_digest.as_bytes());
95 bytes
96 }
97
98 pub fn decode(bytes: &[u8]) -> Result<Self, ConfigurationRecordError> {
101 if bytes.len() != CONFIGURATION_RECORD_BYTES {
102 return Err(ConfigurationRecordError::Length);
103 }
104 if &bytes[..8] != MAGIC
105 || bytes[8..12] != [0, 2, 1, 0]
106 || !matches!(bytes[12], 1 | 2)
107 || bytes[13..16] != [1, ozzy_proto::VERSION, 1]
108 || bytes[40] != 3
109 || bytes[41..48].iter().any(|&byte| byte != 0)
110 || bytes[224..].iter().any(|&byte| byte != 0)
111 {
112 return Err(ConfigurationRecordError::UnsupportedFields);
113 }
114 let stored = Digest::from_bytes(
115 bytes[DIGEST_START..DIGEST_START + 32]
116 .try_into()
117 .expect("fixed digest"),
118 );
119 if record_digest(bytes) != stored {
120 return Err(ConfigurationRecordError::DigestMismatch);
121 }
122 let group = GroupId::from_bytes(bytes[16..32].try_into().expect("fixed group"));
123 let epoch = u64::from_be_bytes(bytes[32..40].try_into().expect("fixed epoch"));
124 let voters = std::array::from_fn(|index| {
125 let start = 48 + index * 48;
126 ConfiguredVoter {
127 node_id: NodeId::from_bytes(
128 bytes[start..start + 16].try_into().expect("fixed voter"),
129 ),
130 principal: Digest::from_bytes(
131 bytes[start + 16..start + 48]
132 .try_into()
133 .expect("fixed principal"),
134 ),
135 }
136 });
137 let policy = if bytes[12] == 1 {
138 QuorumPolicy::Durable
139 } else {
140 QuorumPolicy::Replicated
141 };
142 let record = Self::with_policy(group, epoch, voters, policy)?;
143 debug_assert_eq!(record.configuration.scope().configuration_digest, stored);
144 Ok(record)
145 }
146}
147
148fn unsigned_record(
149 group: GroupId,
150 epoch: u64,
151 voters: &[ConfiguredVoter; 3],
152 policy: QuorumPolicy,
153) -> [u8; CONFIGURATION_RECORD_BYTES] {
154 let mut bytes = [0; CONFIGURATION_RECORD_BYTES];
155 bytes[..8].copy_from_slice(MAGIC);
156 bytes[8..16].copy_from_slice(&[0, 2, 1, 0, policy as u8, 1, ozzy_proto::VERSION, 1]);
159 bytes[16..32].copy_from_slice(group.as_bytes());
160 bytes[32..40].copy_from_slice(&epoch.to_be_bytes());
161 bytes[40] = 3;
162 for (index, voter) in voters.iter().enumerate() {
163 let start = 48 + index * 48;
164 bytes[start..start + 16].copy_from_slice(voter.node_id.as_bytes());
165 bytes[start + 16..start + 48].copy_from_slice(voter.principal.as_bytes());
166 }
167 bytes
168}
169
170fn record_digest(bytes: &[u8]) -> Digest {
171 let mut hasher = ozzy_journal::integrity::IntegrityHasher::new(HASH_CONTEXT);
172 hasher.update(&bytes[..DIGEST_START]);
173 hasher.update(&[0; 32]);
174 hasher.update(&bytes[DIGEST_START + 32..]);
175 hasher.finish()
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
180pub enum ConfigurationRecordError {
181 #[error("invalid replica configuration record length")]
183 Length,
184 #[error("unsupported replica configuration record fields")]
186 UnsupportedFields,
187 #[error("replica configuration record digest mismatch")]
189 DigestMismatch,
190 #[error("invalid replica configuration principals or epoch")]
192 InvalidConfiguration,
193 #[error(transparent)]
195 Replication(#[from] ReplicationError),
196}