Skip to main content

heddle_object_model/object/thread_replication/
initial_base.rs

1//! Canonical synthetic empty base for a new native Spool.
2use super::{ThreadGenesis, invalid};
3use crate::{error::Result, object::State};
4
5/// Stable synthetic pre-history for every new Spool. This is system
6/// initialization, never an assertion of human capture authorship.
7pub 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    // Restore the derived cached ID after fixing the canonical creation time.
17    State::decode_current_msgpack(&state.encode_current_msgpack()?)
18}
19
20/// Only the exact deterministic empty seed can bootstrap without an original
21/// source operation. A random empty State, even with Heddle attribution, is
22/// authored content and requires ordinary source provenance.
23pub 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}