Skip to main content

heddle_object_model/object/thread_replication/
ownership_claim.rs

1//! Explicit transition from immutable local-key genesis ownership to an account.
2//! Signatures and account capability admission are separate verification layers.
3use std::collections::BTreeSet;
4
5use serde::{Deserialize, Serialize};
6
7use super::{GenesisOwner, SourceAuthor, ThreadGenesis, bounded, invalid};
8use crate::{error::Result, object::ContentHash};
9
10pub const FORMAT: &str = "heddle-thread-ownership-claim-v1";
11pub const METHOD: &str = "/heddle.api.v1alpha2.ThreadService/ClaimThreadOwnership";
12
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct ThreadOwnershipClaim {
16    pub version: u16,
17    pub thread: ContentHash,
18    pub prior_local_key: [u8; 32],
19    pub accepting_publisher: [u8; 32],
20    pub acceptance: SourceAuthor,
21    /// Complete observed source frontier, signed as the former-key authority cutoff.
22    pub source_frontier: BTreeSet<ContentHash>,
23}
24impl ThreadOwnershipClaim {
25    pub fn encode(&self) -> Result<Vec<u8>> {
26        self.acceptance.validate()?;
27        if self.version != 1
28            || self.prior_local_key == [0; 32]
29            || self.accepting_publisher == [0; 32]
30            || self.source_frontier.len() > 128
31            || !matches!(self.acceptance, SourceAuthor::Account { .. })
32        {
33            return Err(invalid("invalid explicit Thread ownership claim"));
34        }
35        let bytes = rmp_serde::to_vec_named(self)?;
36        bounded(&bytes)?;
37        Ok(bytes)
38    }
39    pub fn decode(bytes: &[u8]) -> Result<Self> {
40        bounded(bytes)?;
41        let value: Self = rmp_serde::from_slice(bytes)?;
42        if value.encode()? != bytes {
43            return Err(invalid("noncanonical Thread ownership claim"));
44        }
45        Ok(value)
46    }
47    pub fn id(&self) -> Result<ContentHash> {
48        Ok(ContentHash::compute_typed(FORMAT, &self.encode()?))
49    }
50    pub fn validate_genesis(&self, genesis: &ThreadGenesis) -> Result<()> {
51        self.encode()?;
52        let SourceAuthor::Account { spool, .. } = &self.acceptance else {
53            return Err(invalid("Thread claim requires account acceptance"));
54        };
55        if self.thread != genesis.id()?
56            || genesis.spool != spool.to_string()
57            || genesis.owner != GenesisOwner::LocalKey(self.prior_local_key)
58        {
59            return Err(invalid(
60                "Thread claim differs from immutable local ownership",
61            ));
62        }
63        Ok(())
64    }
65    pub fn account(&self) -> Result<uuid::Uuid> {
66        let SourceAuthor::Account { actor, .. } = &self.acceptance else {
67            return Err(invalid("Thread claim requires account acceptance"));
68        };
69        Ok(actor.principal_id)
70    }
71}