1#![doc = include_str!("../Documentation.md")]
2#![forbid(unsafe_code)]
3
4use kcode_k1_peering::K1Peering;
5use kcode_k1_txn_ordering::{K1TxnOrdering, Subsystem, SubsystemId, TxId};
6use semver::Version;
7use std::sync::{Arc, Mutex, MutexGuard, Weak};
8
9const SUBSYSTEM: &str = "k1-loom-bootstrap";
10const WIRE_VERSION: u8 = 2;
11
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct BeginRecord {
14 username: String,
15 full_name: String,
16 public_key: [u8; 32],
17}
18
19impl BeginRecord {
20 pub fn new(username: String, full_name: String, public_key: [u8; 32]) -> Self {
21 Self {
22 username,
23 full_name,
24 public_key,
25 }
26 }
27
28 pub fn username(&self) -> &str {
29 &self.username
30 }
31
32 pub fn full_name(&self) -> &str {
33 &self.full_name
34 }
35
36 pub const fn public_key(&self) -> [u8; 32] {
37 self.public_key
38 }
39}
40
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct ImportedPackage {
43 logical_name: String,
44 version: Version,
45}
46
47impl ImportedPackage {
48 pub fn new(logical_name: String, version: Version) -> Result<Self, String> {
49 if logical_name.is_empty() {
50 return Err("imported package logical name is empty".to_owned());
51 }
52 if !version.pre.is_empty() || !version.build.is_empty() {
53 return Err("imported package version must be stable".to_owned());
54 }
55 Ok(Self {
56 logical_name,
57 version,
58 })
59 }
60
61 pub fn logical_name(&self) -> &str {
62 &self.logical_name
63 }
64
65 pub fn version(&self) -> &Version {
66 &self.version
67 }
68}
69
70#[derive(Clone, Debug, Eq, PartialEq)]
71pub struct CompleteRecord {
72 begin: BeginRecord,
73 user_id: [u8; 12],
74 group_id: [u8; 12],
75 profile_id: [u8; 12],
76 root_id: [u8; 12],
77 packages: Vec<ImportedPackage>,
78}
79
80impl CompleteRecord {
81 pub fn new(
82 begin: BeginRecord,
83 user_id: [u8; 12],
84 group_id: [u8; 12],
85 profile_id: [u8; 12],
86 root_id: [u8; 12],
87 mut packages: Vec<ImportedPackage>,
88 ) -> Result<Self, String> {
89 packages.sort_by(|left, right| {
90 left.logical_name
91 .cmp(&right.logical_name)
92 .then_with(|| left.version.cmp(&right.version))
93 });
94 if packages.windows(2).any(|pair| {
95 pair[0].logical_name == pair[1].logical_name && pair[0].version == pair[1].version
96 }) {
97 return Err("duplicate imported package coordinate".to_owned());
98 }
99 Ok(Self {
100 begin,
101 user_id,
102 group_id,
103 profile_id,
104 root_id,
105 packages,
106 })
107 }
108
109 pub fn begin(&self) -> &BeginRecord {
110 &self.begin
111 }
112
113 pub const fn user_id(&self) -> [u8; 12] {
114 self.user_id
115 }
116
117 pub const fn group_id(&self) -> [u8; 12] {
118 self.group_id
119 }
120
121 pub const fn profile_id(&self) -> [u8; 12] {
122 self.profile_id
123 }
124
125 pub const fn root_id(&self) -> [u8; 12] {
126 self.root_id
127 }
128
129 pub fn packages(&self) -> &[ImportedPackage] {
130 &self.packages
131 }
132}
133
134#[derive(Clone, Debug, Eq, PartialEq)]
135pub enum BootstrapStatus {
136 Empty,
137 Begun {
138 record: BeginRecord,
139 transaction: TxId,
140 },
141 Complete {
142 record: CompleteRecord,
143 transaction: TxId,
144 },
145}
146
147#[derive(Clone)]
148pub struct K1BootstrapState {
149 inner: Arc<Inner>,
150}
151
152struct Inner {
153 peering: Arc<K1Peering>,
154 state: Mutex<State>,
155}
156
157struct State {
158 status: BootstrapStatus,
159 available: bool,
160}
161
162struct Handler(Weak<Inner>);
163
164#[derive(Debug)]
165enum Event {
166 Begin(BeginRecord),
167 Complete(CompleteRecord),
168}
169
170impl K1BootstrapState {
171 pub fn open(ordering: Arc<K1TxnOrdering>, peering: Arc<K1Peering>) -> Result<Self, String> {
172 let inner = Arc::new(Inner {
173 peering,
174 state: Mutex::new(State {
175 status: BootstrapStatus::Empty,
176 available: true,
177 }),
178 });
179 ordering.register_subsystem(
180 SubsystemId::from_str(SUBSYSTEM)?,
181 None,
182 Arc::new(Handler(Arc::downgrade(&inner))),
183 )?;
184 Ok(Self { inner })
185 }
186
187 pub fn status(&self) -> Result<BootstrapStatus, String> {
188 Ok(ready(&self.inner)?.status.clone())
189 }
190
191 pub fn begin(&self, record: BeginRecord) -> Result<TxId, String> {
192 match self.status()? {
193 BootstrapStatus::Begun {
194 record: existing,
195 transaction,
196 } if existing == record => {
197 return Ok(transaction);
198 }
199 BootstrapStatus::Complete {
200 record: existing,
201 transaction,
202 } if existing.begin == record => return Ok(transaction),
203 BootstrapStatus::Empty => {}
204 _ => return Err("bootstrap Begin conflicts with canonical state".to_owned()),
205 }
206 self.submit(Event::Begin(record.clone()))?;
207 match self.status()? {
208 BootstrapStatus::Begun {
209 record: existing,
210 transaction,
211 } if existing == record => Ok(transaction),
212 BootstrapStatus::Complete {
213 record: existing,
214 transaction,
215 } if existing.begin == record => Ok(transaction),
216 _ => Err("bootstrap Begin lost to conflicting canonical state".to_owned()),
217 }
218 }
219
220 pub fn complete(&self, record: CompleteRecord) -> Result<TxId, String> {
221 match self.status()? {
222 BootstrapStatus::Begun {
223 record: existing, ..
224 } if existing == record.begin => {}
225 BootstrapStatus::Complete {
226 record: existing,
227 transaction,
228 } if existing == record => {
229 return Ok(transaction);
230 }
231 BootstrapStatus::Empty => return Err("bootstrap has not begun".to_owned()),
232 _ => return Err("bootstrap Complete conflicts with canonical state".to_owned()),
233 }
234 self.submit(Event::Complete(record.clone()))?;
235 match self.status()? {
236 BootstrapStatus::Complete {
237 record: existing,
238 transaction,
239 } if existing == record => Ok(transaction),
240 _ => Err("bootstrap Complete lost to conflicting canonical state".to_owned()),
241 }
242 }
243
244 fn submit(&self, event: Event) -> Result<TxId, String> {
245 self.inner
246 .peering
247 .submit_txn(SubsystemId::from_str(SUBSYSTEM)?, &encode(&event)?)
248 }
249}
250
251impl Subsystem for Handler {
252 fn submit_txn(&self, id: TxId, payload: &[u8]) -> Result<(), String> {
253 let inner = self
254 .0
255 .upgrade()
256 .ok_or_else(|| "bootstrap state is unavailable".to_owned())?;
257 let event = decode(payload).map_err(|error| fail(&inner, error))?;
258 let mut state = ready(&inner)?;
259 match (&state.status, event) {
260 (BootstrapStatus::Empty, Event::Begin(record)) => {
261 state.status = BootstrapStatus::Begun {
262 record,
263 transaction: id,
264 };
265 }
266 (BootstrapStatus::Begun { record: begun, .. }, Event::Complete(record))
267 if *begun == record.begin =>
268 {
269 state.status = BootstrapStatus::Complete {
270 record,
271 transaction: id,
272 };
273 }
274 _ => {}
275 }
276 Ok(())
277 }
278
279 fn reorg(&self) -> Result<(), String> {
280 let inner = self
281 .0
282 .upgrade()
283 .ok_or_else(|| "bootstrap state is unavailable".to_owned())?;
284 lock(&inner)?.available = false;
285 Ok(())
286 }
287}
288
289fn lock(inner: &Inner) -> Result<MutexGuard<'_, State>, String> {
290 inner
291 .state
292 .lock()
293 .map_err(|_| "bootstrap state lock poisoned".to_owned())
294}
295
296fn ready(inner: &Inner) -> Result<MutexGuard<'_, State>, String> {
297 let state = lock(inner)?;
298 if state.available {
299 Ok(state)
300 } else {
301 Err("bootstrap state is unavailable".to_owned())
302 }
303}
304
305fn fail(inner: &Inner, error: String) -> String {
306 if let Ok(mut state) = lock(inner) {
307 state.available = false;
308 }
309 error
310}
311
312fn encode(event: &Event) -> Result<Vec<u8>, String> {
313 let mut out = vec![
314 WIRE_VERSION,
315 match event {
316 Event::Begin(_) => 1,
317 Event::Complete(_) => 2,
318 },
319 ];
320 match event {
321 Event::Begin(record) => put_begin(&mut out, record)?,
322 Event::Complete(record) => {
323 put_begin(&mut out, &record.begin)?;
324 out.extend_from_slice(&record.user_id);
325 out.extend_from_slice(&record.group_id);
326 out.extend_from_slice(&record.profile_id);
327 out.extend_from_slice(&record.root_id);
328 put_u64(&mut out, record.packages.len())?;
329 for package in &record.packages {
330 put_string(&mut out, &package.logical_name)?;
331 put_string(&mut out, &package.version.to_string())?;
332 }
333 }
334 }
335 Ok(out)
336}
337
338fn put_begin(out: &mut Vec<u8>, record: &BeginRecord) -> Result<(), String> {
339 put_string(out, &record.username)?;
340 put_string(out, &record.full_name)?;
341 out.extend_from_slice(&record.public_key);
342 Ok(())
343}
344
345fn put_string(out: &mut Vec<u8>, value: &str) -> Result<(), String> {
346 put_u64(out, value.len())?;
347 out.try_reserve(value.len())
348 .map_err(|_| "bootstrap encoding allocation failed".to_owned())?;
349 out.extend_from_slice(value.as_bytes());
350 Ok(())
351}
352
353fn put_u64(out: &mut Vec<u8>, value: usize) -> Result<(), String> {
354 out.extend_from_slice(
355 &u64::try_from(value)
356 .map_err(|_| "bootstrap encoding length overflow".to_owned())?
357 .to_le_bytes(),
358 );
359 Ok(())
360}
361
362fn decode(bytes: &[u8]) -> Result<Event, String> {
363 let mut input = Input { bytes, at: 0 };
364 if input.byte()? != WIRE_VERSION {
365 return Err("unsupported bootstrap state wire version".to_owned());
366 }
367 let kind = input.byte()?;
368 let begin = input.begin()?;
369 let event = match kind {
370 1 => Event::Begin(begin),
371 2 => {
372 let user_id = input.array()?;
373 let group_id = input.array()?;
374 let profile_id = input.array()?;
375 let root_id = input.array()?;
376 let count = input.length()?;
377 let mut packages = Vec::new();
378 packages
379 .try_reserve(count)
380 .map_err(|_| "bootstrap package allocation failed".to_owned())?;
381 for _ in 0..count {
382 packages.push(ImportedPackage::new(
383 input.string()?,
384 input
385 .string()?
386 .parse()
387 .map_err(|_| "invalid bootstrap package version".to_owned())?,
388 )?);
389 }
390 Event::Complete(CompleteRecord::new(
391 begin, user_id, group_id, profile_id, root_id, packages,
392 )?)
393 }
394 _ => return Err("unknown bootstrap state event kind".to_owned()),
395 };
396 if input.at != bytes.len() {
397 return Err("trailing bootstrap state bytes".to_owned());
398 }
399 Ok(event)
400}
401
402struct Input<'a> {
403 bytes: &'a [u8],
404 at: usize,
405}
406
407impl Input<'_> {
408 fn take(&mut self, count: usize) -> Result<&[u8], String> {
409 let end = self
410 .at
411 .checked_add(count)
412 .ok_or_else(|| "bootstrap state length overflow".to_owned())?;
413 let value = self
414 .bytes
415 .get(self.at..end)
416 .ok_or_else(|| "truncated bootstrap state event".to_owned())?;
417 self.at = end;
418 Ok(value)
419 }
420
421 fn byte(&mut self) -> Result<u8, String> {
422 Ok(self.take(1)?[0])
423 }
424
425 fn array<const N: usize>(&mut self) -> Result<[u8; N], String> {
426 self.take(N)?
427 .try_into()
428 .map_err(|_| "invalid bootstrap fixed field".to_owned())
429 }
430
431 fn length(&mut self) -> Result<usize, String> {
432 usize::try_from(u64::from_le_bytes(self.array()?))
433 .map_err(|_| "bootstrap state length overflow".to_owned())
434 }
435
436 fn string(&mut self) -> Result<String, String> {
437 let length = self.length()?;
438 String::from_utf8(self.take(length)?.to_vec())
439 .map_err(|_| "bootstrap state string is not UTF-8".to_owned())
440 }
441
442 fn begin(&mut self) -> Result<BeginRecord, String> {
443 Ok(BeginRecord::new(
444 self.string()?,
445 self.string()?,
446 self.array()?,
447 ))
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 #[test]
456 fn version_two_wire_round_trips_without_code_hashes() {
457 let begin = BeginRecord::new("admin".into(), "Admin".into(), [2; 32]);
458 let packages = vec![ImportedPackage::new("loom".into(), Version::new(0, 1, 0)).unwrap()];
459 let complete =
460 CompleteRecord::new(begin, [4; 12], [5; 12], [6; 12], [7; 12], packages).unwrap();
461 let encoded = encode(&Event::Complete(complete.clone())).unwrap();
462 assert_eq!(encoded[0], 2);
463 assert!(matches!(decode(&encoded).unwrap(), Event::Complete(found) if found == complete));
464 assert!(decode(&[1, 1]).unwrap_err().contains("wire version"));
465 }
466}