heddle_object_model/object/thread_replication/
initial_base.rs1use super::{ThreadGenesis, invalid};
3use crate::{error::Result, object::State};
4
5pub fn synthetic_initial_base() -> Result<State> {
8 use crate::object::{Attribution, ChangeId, Principal, Tree};
9 let mut state = State::new_refresh_of(
10 Tree::new().hash(),
11 Vec::new(),
12 Attribution::human(Principal::new("Heddle", "init@heddle")),
13 ChangeId::from_bytes(*b"heddle-seed-v2!!"),
14 );
15 state.created_at = chrono::DateTime::UNIX_EPOCH;
16 State::decode_current_msgpack(&state.encode_current_msgpack()?)
18}
19
20pub fn initial_base_state(genesis: &ThreadGenesis, bytes: &[u8]) -> Result<State> {
24 if bytes.is_empty() || bytes.len() > 4096 {
25 return Err(invalid("initial Thread base exceeds bootstrap bound"));
26 }
27 let state = State::decode_current_msgpack(bytes)?;
28 let expected = synthetic_initial_base()?;
29 if state.id() != genesis.base || expected.encode_current_msgpack()? != bytes {
30 return Err(invalid(
31 "initial Thread base differs from signed empty seed",
32 ));
33 }
34 Ok(state)
35}
36
37#[cfg(test)]
38mod tests {
39 use uuid::Uuid;
40
41 use super::*;
42 use crate::object::{Attribution, ContentHash, Principal, Tree};
43 #[test]
44 fn import_initial_base_is_exact_bounded_empty_seed() {
45 let seed = synthetic_initial_base().expect("known system seed");
46 let mut genesis = ThreadGenesis {
47 version: 1,
48 spool: Uuid::from_u128(1).to_string(),
49 parent: None,
50 base: seed.id(),
51 name: "main".into(),
52 intent: String::new(),
53 owner: super::super::GenesisOwner::LocalKey([1; 32]),
54 creator: [1; 32],
55 nonce: vec![],
56 };
57 let bytes = seed.encode_current_msgpack().expect("seed");
58 genesis.base = seed.id();
59 assert_eq!(
60 initial_base_state(&genesis, &bytes)
61 .expect("one-call bootstrap")
62 .id(),
63 seed.id()
64 );
65 let random_seed = State::new_snapshot(
66 Tree::new().hash(),
67 vec![],
68 Attribution::human(Principal::new("Heddle", "init@heddle")),
69 );
70 genesis.base = random_seed.id();
71 assert!(
72 initial_base_state(
73 &genesis,
74 &random_seed.encode_current_msgpack().expect("random seed")
75 )
76 .is_err(),
77 "the old random empty seed shape is not a bootstrap exception"
78 );
79 genesis.base = seed.id();
80 let mut changed = seed.clone();
81 changed.tree = ContentHash::from_bytes([88; 32]);
82 genesis.base = changed.id();
83 assert!(
84 initial_base_state(
85 &genesis,
86 &changed.encode_current_msgpack().expect("changed")
87 )
88 .is_err(),
89 "nonempty source needs authorized closure transfer"
90 );
91 changed = seed.clone();
92 changed.provenance = Some(ContentHash::from_bytes([89; 32]));
93 genesis.base = changed.id();
94 assert!(
95 initial_base_state(
96 &genesis,
97 &changed.encode_current_msgpack().expect("changed")
98 )
99 .is_err(),
100 "seed cannot introduce another reference"
101 );
102 assert!(
103 initial_base_state(&genesis, &bytes).is_err(),
104 "base must match signed identity"
105 );
106 }
107 #[test]
108 fn synthetic_initial_base_is_stable_across_rust_and_browser() {
109 let state = synthetic_initial_base().expect("synthetic seed");
110 let bytes = state.encode_current_msgpack().expect("canonical seed");
111 let expected = include_str!("../../../tests/fixtures/synthetic-initial-base-v2.txt");
112 assert_eq!(
113 format!(
114 "canonical={}\nid={}\n",
115 hex::encode(&bytes),
116 hex::encode(state.id().as_bytes())
117 ),
118 expected
119 );
120 assert_eq!(
121 bytes,
122 synthetic_initial_base()
123 .expect("repeat")
124 .encode_current_msgpack()
125 .expect("repeat bytes")
126 );
127 let mut genesis = ThreadGenesis {
128 version: 1,
129 spool: Uuid::from_u128(1).to_string(),
130 parent: None,
131 base: state.id(),
132 name: "main".into(),
133 intent: String::new(),
134 owner: super::super::GenesisOwner::LocalKey([1; 32]),
135 creator: [1; 32],
136 nonce: vec![],
137 };
138 genesis.base = state.id();
139 initial_base_state(&genesis, &bytes).expect("accepted seed shape");
140 }
141}