Skip to main content

coven_protocol/
membership_mutation.rs

1//! Prepared membership mutations: the exact entry, head, and objects one
2//! membership publication or transition binds, validated as a unit before
3//! anything durable records them.
4
5use crate::membership::{
6    self, AuthorHead, MembershipEntry, MembershipEntryRef, MembershipHeadRef,
7    MergeMembershipHeadTransition,
8};
9use crate::objects::{ExactObjectRef, PreparedExactObject};
10use crate::store_commit::ObjectHash;
11
12/// One value prepared for upload: its canonical bytes under the exact object its
13/// reference names.
14///
15/// The bytes are not carried beside the value — they are what the value
16/// serializes to — so both the validators and the upload paths re-derive them
17/// here. [`PreparedExactObject::new`] checks them against the reference, so a
18/// value that does not serialize to what its reference names fails at this call
19/// rather than reaching storage.
20pub fn prepare_exact_object(
21    object: &ExactObjectRef,
22    value: &impl serde::Serialize,
23) -> Result<PreparedExactObject, MembershipPreparationError> {
24    let bytes = serde_json::to_vec(value).map_err(MembershipPreparationError::Json)?;
25    PreparedExactObject::new(object.clone(), bytes).map_err(MembershipPreparationError::ExactObject)
26}
27
28fn binds_exact_object(object: &ExactObjectRef, value: &impl serde::Serialize) -> bool {
29    prepare_exact_object(object, value).is_ok()
30}
31
32/// A prepared membership mutation whose parts do not bind one exact entry and
33/// head. Workflow errors wrap it at the operation boundary.
34#[derive(Debug, thiserror::Error)]
35pub enum MembershipPreparationError {
36    #[error("invalid prepared membership mutation: {0}")]
37    Invariant(String),
38    #[error("serialize prepared membership mutation: {0}")]
39    Json(#[source] serde_json::Error),
40    #[error("prepared membership exact object: {0}")]
41    ExactObject(#[source] crate::objects::StorageError),
42}
43
44/// One membership entry and the head that publishes it, each named by its exact
45/// reference.
46///
47/// The entry and head values are here, and `entry_ref.object` / `head_ref.object`
48/// name the objects they serialize to, so nothing carries their bytes a second
49/// time: the upload rebuilds them from the value and the reference re-checks
50/// them on the way out.
51#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct PreparedMembershipPublication {
54    pub entry: MembershipEntry,
55    pub entry_ref: MembershipEntryRef,
56    pub head: AuthorHead,
57    pub head_ref: MembershipHeadRef,
58}
59
60impl PreparedMembershipPublication {
61    pub fn validate(&self) -> Result<(), MembershipPreparationError> {
62        PreparedMembershipTransition {
63            entry: self.entry.clone(),
64            entry_ref: self.entry_ref.clone(),
65            transition: membership::MergeMembershipHeadTransition {
66                body: self.head.body.clone(),
67                head_slot: self.head_ref.object.slot().clone(),
68            },
69        }
70        .validate()?;
71        let coord = self.entry.coord();
72        if self.entry_ref.coord != coord
73            || self.head.body.entry != self.entry_ref
74            || self.head.entry_coord() != coord
75            || self.head_ref.coord != coord
76            || self.head_ref.head_hash != self.head.head_hash()
77            || !binds_exact_object(&self.head_ref.object, &self.head)
78        {
79            return Err(MembershipPreparationError::Invariant(
80                "prepared membership publication does not bind one exact entry and head"
81                    .to_string(),
82            ));
83        }
84        Ok(())
85    }
86
87    /// The entry object this publication uploads.
88    pub fn prepared_entry(&self) -> Result<PreparedExactObject, MembershipPreparationError> {
89        prepare_exact_object(&self.entry_ref.object, &self.entry)
90    }
91
92    /// The head object this publication uploads.
93    pub fn prepared_head(&self) -> Result<PreparedExactObject, MembershipPreparationError> {
94        prepare_exact_object(&self.head_ref.object, &self.head)
95    }
96}
97
98#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
99#[serde(deny_unknown_fields)]
100pub struct PreparedMembershipTransition {
101    pub entry: MembershipEntry,
102    pub entry_ref: MembershipEntryRef,
103    pub transition: MergeMembershipHeadTransition,
104}
105
106impl PreparedMembershipTransition {
107    pub fn validate(&self) -> Result<(), MembershipPreparationError> {
108        let coord = self.entry.coord();
109        let next_sequence = coord.seq.checked_add(1).ok_or_else(|| {
110            MembershipPreparationError::Invariant("membership sequence is exhausted".to_string())
111        })?;
112        let entry_key = format!(
113            "{}.json",
114            crate::store_commit::membership_entry_semantic_prefix(
115                &coord.author_pubkey,
116                &coord.author_owner_grant,
117                coord.stream_id,
118                coord.seq,
119                coord.entry_hash,
120            )
121        );
122        let head_key = format!(
123            "{}.json",
124            crate::store_commit::membership_head_slot_prefix(
125                &coord.author_pubkey,
126                &coord.author_owner_grant,
127                coord.stream_id,
128                coord.seq,
129            )
130        );
131        let successor_key = format!(
132            "{}.json",
133            crate::store_commit::membership_head_slot_prefix(
134                &coord.author_pubkey,
135                &coord.author_owner_grant,
136                coord.stream_id,
137                next_sequence,
138            )
139        );
140        if self.entry_ref.coord != self.entry.coord()
141            || !binds_exact_object(&self.entry_ref.object, &self.entry)
142            || self.entry_ref.object.slot().logical_key() != entry_key
143            || self.transition.body.entry != self.entry_ref
144            || self.transition.body.resolutions != self.entry.resolution_dependencies
145            || self.transition.head_slot.logical_key() != head_key
146            || self.transition.body.successor.next_slot.logical_key() != successor_key
147        {
148            return Err(MembershipPreparationError::Invariant(
149                "prepared membership transition does not bind its exact entry".to_string(),
150            ));
151        }
152        Ok(())
153    }
154
155    /// The entry object this transition uploads.
156    pub fn prepared_entry(&self) -> Result<PreparedExactObject, MembershipPreparationError> {
157        prepare_exact_object(&self.entry_ref.object, &self.entry)
158    }
159}
160
161pub enum StoreMembershipJournalCompletion {
162    Mutation {
163        intent_hash: ObjectHash,
164        progress_bytes: Vec<u8>,
165        remote_objects: Vec<crate::remote_object::RemoteObjectRecord>,
166    },
167    RotationMutation {
168        intent_hash: ObjectHash,
169        progress_bytes: Vec<u8>,
170        generation: u64,
171        remote_objects: Vec<crate::remote_object::RemoteObjectRecord>,
172    },
173    OwnerPromotion {
174        transition: crate::owner_promotion_journal::OwnerPromotionJournalTransition,
175        remote_objects: Vec<crate::remote_object::RemoteObjectRecord>,
176    },
177}
178
179impl StoreMembershipJournalCompletion {
180    pub fn object_refs(&self) -> Vec<ExactObjectRef> {
181        let remote_objects = match self {
182            Self::Mutation { remote_objects, .. }
183            | Self::RotationMutation { remote_objects, .. }
184            | Self::OwnerPromotion { remote_objects, .. } => remote_objects,
185        };
186        remote_objects
187            .iter()
188            .map(|remote| remote.object().clone())
189            .collect()
190    }
191
192    pub fn remote_object(
193        &self,
194        object: &ExactObjectRef,
195    ) -> Result<crate::remote_object::RemoteObjectRecord, MembershipPreparationError> {
196        let remote_objects = match self {
197            Self::Mutation { remote_objects, .. }
198            | Self::RotationMutation { remote_objects, .. }
199            | Self::OwnerPromotion { remote_objects, .. } => remote_objects,
200        };
201        remote_objects
202            .iter()
203            .find(|remote| remote.object() == object)
204            .cloned()
205            .ok_or_else(|| {
206                MembershipPreparationError::Invariant(
207                    "membership completion omits an exact activated object".to_string(),
208                )
209            })
210    }
211}