Skip to main content

heddle_object_model/object/thread_replication/
ownership_resolution.rs

1//! Owner adjudication of conflicting explicit ownership claims over one Thread.
2//! The surviving claim and the complete accepted source frontier are signed by
3//! the adjudicating owner authority. Signature and account capability admission
4//! are separate verification layers, exactly as for the ownership claim.
5use std::collections::BTreeSet;
6
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use super::{GenesisOwner, SourceAuthor, ThreadGenesis, bounded, invalid};
11use crate::{error::Result, object::ContentHash};
12
13pub const FORMAT: &str = "heddle-thread-ownership-resolution-v1";
14pub const METHOD: &str = "/heddle.api.v1alpha2.ThreadService/ResolveOwnershipConflict";
15
16#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct ThreadOwnershipResolution {
19    pub version: u16,
20    pub spool: Uuid,
21    /// Thread identity being adjudicated.
22    pub thread: ContentHash,
23    /// Claim id of the surviving claim; must be a member of `conflicting_claims`.
24    pub winning_claim: ContentHash,
25    /// The full stored conflicting claim-id set, including the winner (>= 2).
26    pub conflicting_claims: BTreeSet<ContentHash>,
27    /// Complete accepted source frontier observed at resolution time.
28    pub frontier: BTreeSet<ContentHash>,
29    /// Immutable original local owner who chooses the surviving claim.
30    pub local_owner: [u8; 32],
31    /// Fresh recipient publisher accepting the chosen account ownership.
32    pub accepting_publisher: [u8; 32],
33    pub acceptance: SourceAuthor,
34    pub occurred_at_ms: i64,
35}
36impl ThreadOwnershipResolution {
37    pub fn encode(&self) -> Result<Vec<u8>> {
38        self.acceptance.validate()?;
39        if self.version != 1
40            || self.local_owner == [0; 32]
41            || self.accepting_publisher == [0; 32]
42            || self.spool.is_nil()
43            || self.occurred_at_ms <= 0
44            || self.conflicting_claims.len() < 2
45            || self.conflicting_claims.len() > 128
46            || self.frontier.len() > 128
47            || !self.conflicting_claims.contains(&self.winning_claim)
48            || !matches!(&self.acceptance, SourceAuthor::Account { spool, .. } if *spool == self.spool)
49        {
50            return Err(invalid("invalid Thread ownership resolution"));
51        }
52        let bytes = rmp_serde::to_vec_named(self)?;
53        bounded(&bytes)?;
54        Ok(bytes)
55    }
56    pub fn decode(bytes: &[u8]) -> Result<Self> {
57        bounded(bytes)?;
58        let value: Self = rmp_serde::from_slice(bytes)?;
59        if value.encode()? != bytes {
60            return Err(invalid("noncanonical Thread ownership resolution"));
61        }
62        Ok(value)
63    }
64    pub fn id(&self) -> Result<ContentHash> {
65        Ok(ContentHash::compute_typed(FORMAT, &self.encode()?))
66    }
67    pub fn validate_genesis(&self, genesis: &ThreadGenesis) -> Result<()> {
68        self.encode()?;
69        if self.thread != genesis.id()?
70            || genesis.spool != self.spool.to_string()
71            || genesis.owner != GenesisOwner::LocalKey(self.local_owner)
72        {
73            return Err(invalid("resolution differs from immutable local ownership"));
74        }
75        Ok(())
76    }
77    pub fn account(&self) -> Result<uuid::Uuid> {
78        let SourceAuthor::Account { actor, .. } = &self.acceptance else {
79            return Err(invalid("resolution requires account acceptance"));
80        };
81        Ok(actor.principal_id)
82    }
83}