Skip to main content

coven_protocol/circle_control/
semantic_path.rs

1use super::*;
2
3#[derive(Debug, Clone, Copy)]
4pub enum CircleSemanticSlot<'a> {
5    Control {
6        circle_id: CircleId,
7        control: &'a CircleControlCoord,
8    },
9    ControlHead {
10        circle_id: CircleId,
11        control: &'a CircleControlCoord,
12    },
13    RosterEntry {
14        circle_id: CircleId,
15        coord: &'a crate::circle_roster::CircleRosterCoord,
16    },
17    RosterHead {
18        circle_id: CircleId,
19        head: &'a CircleRosterHeadRef,
20    },
21    RosterResolution {
22        circle_id: CircleId,
23        resolution: &'a crate::circle_roster::CircleRosterConflictResolutionRef,
24    },
25    MetadataEntry {
26        circle_id: CircleId,
27        coord: &'a CircleMetadataCoord,
28    },
29    MetadataHead {
30        circle_id: CircleId,
31        head: &'a CircleMetadataHeadRef,
32    },
33}
34
35pub fn circle_semantic_prefix(slot: CircleSemanticSlot<'_>) -> String {
36    match slot {
37        CircleSemanticSlot::Control { circle_id, control } => format!(
38            "circle-control/{}/merge/entries/{author_pubkey}/{device_id}/{author_owner_grant}/{stream_id}/{seq}/{control_hash}",
39            circle_id,
40            author_pubkey = control.author_pubkey,
41            device_id = control.device_id,
42            author_owner_grant = control.author_owner_grant,
43            stream_id = control.stream_id,
44            seq = control.seq,
45            control_hash = control.control_hash,
46        ),
47        CircleSemanticSlot::ControlHead { circle_id, control } => {
48            circle_control_head_prefix(
49                circle_id,
50                &CircleAuthorStreamKey {
51                    author_pubkey: control.author_pubkey.clone(),
52                    device_id: control.device_id.clone(),
53                    stream_id: control.stream_id,
54                    author_owner_grant: control.author_owner_grant.clone(),
55                },
56                control.seq,
57            )
58        }
59        CircleSemanticSlot::RosterEntry { circle_id, coord } => format!(
60            "circles/{circle_id}/roster/entries/{}/{}/{}/{}/{}/{}",
61            coord.author_pubkey,
62            coord.device_id,
63            coord.author_owner_grant,
64            coord.stream_id,
65            coord.seq,
66            coord.entry_hash
67        ),
68        CircleSemanticSlot::RosterHead { circle_id, head } => {
69            circle_roster_head_prefix(circle_id, &head.coord.stream_key(), head.coord.seq)
70        }
71        CircleSemanticSlot::RosterResolution {
72            circle_id,
73            resolution,
74        } => format!(
75            "circles/{circle_id}/roster/resolutions/{}/{}/{}",
76            resolution.conflict_hash,
77            resolution.resolver_pubkey,
78            resolution.resolution_hash
79        ),
80        CircleSemanticSlot::MetadataEntry { circle_id, coord } => format!(
81            "circles/{circle_id}/metadata/entries/{}/{}/{}/{}/{}/{}",
82            coord.author_pubkey,
83            coord.device_id,
84            coord.author_owner_grant,
85            coord.stream_id,
86            coord.seq,
87            coord.metadata_hash
88        ),
89        CircleSemanticSlot::MetadataHead { circle_id, head } => {
90            circle_metadata_head_prefix(circle_id, &head.coord.stream_key(), head.coord.seq)
91        }
92    }
93}
94
95pub fn circle_control_head_prefix(
96    circle_id: CircleId,
97    stream: &CircleAuthorStreamKey,
98    seq: u64,
99) -> String {
100    format!(
101        "circle-control/{circle_id}/merge/heads/{}/{}/{}/{}/{seq}",
102        stream.author_pubkey, stream.device_id, stream.author_owner_grant, stream.stream_id
103    )
104}
105
106pub fn circle_roster_head_prefix(
107    circle_id: CircleId,
108    stream: &CircleAuthorStreamKey,
109    seq: u64,
110) -> String {
111    format!(
112        "circles/{circle_id}/roster/heads/{}/{}/{}/{}/{seq}",
113        stream.author_pubkey, stream.device_id, stream.author_owner_grant, stream.stream_id
114    )
115}
116
117pub fn circle_metadata_head_prefix(
118    circle_id: CircleId,
119    stream: &CircleAuthorStreamKey,
120    seq: u64,
121) -> String {
122    format!(
123        "circles/{circle_id}/metadata/heads/{}/{}/{}/{}/{seq}",
124        stream.author_pubkey, stream.device_id, stream.author_owner_grant, stream.stream_id
125    )
126}
127
128pub fn circle_epoch_close_outcome_semantic_prefix(
129    circle_id: CircleId,
130    close_id: CircleEpochCloseId,
131) -> String {
132    format!("circles/{circle_id}/epoch-close/{close_id}/outcome")
133}
134
135pub fn circle_epoch_close_intent_semantic_prefix(
136    circle_id: CircleId,
137    close_id: CircleEpochCloseId,
138    intent_hash: ObjectHash,
139) -> String {
140    format!("circles/{circle_id}/epoch-close/{close_id}/intent/{intent_hash}")
141}
142
143pub fn circle_epoch_close_response_semantic_prefix(
144    circle_id: CircleId,
145    close_id: CircleEpochCloseId,
146    device_id: crate::store_commit::StoreDeviceId,
147) -> String {
148    format!("circles/{circle_id}/epoch-close/{close_id}/responses/{device_id}")
149}
150
151pub fn verify_circle_semantic_prefix(
152    actual: &str,
153    slot: CircleSemanticSlot<'_>,
154) -> Result<(), CircleSemanticPathError> {
155    let expected = circle_semantic_prefix(slot);
156    if actual == expected {
157        Ok(())
158    } else {
159        Err(CircleSemanticPathError {
160            expected,
161            actual: actual.to_string(),
162        })
163    }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
167#[error("Circle object path {actual:?} does not match signed coordinate path {expected:?}")]
168pub struct CircleSemanticPathError {
169    pub expected: String,
170    pub actual: String,
171}
172
173pub fn recipient_slot(
174    owner: &dyn coven_keys::keys::IdentityKeyAuthority,
175    recipient_pubkey: &str,
176    circle_id: CircleId,
177) -> Result<String, CircleTransitionError> {
178    recipient_slot_with_peer(owner, recipient_pubkey, circle_id)
179}
180
181pub fn recipient_slot_with_peer(
182    local_identity: &dyn coven_keys::keys::IdentityKeyAuthority,
183    peer_pubkey: &str,
184    circle_id: CircleId,
185) -> Result<String, CircleTransitionError> {
186    let peer_x25519 = keys::ed25519_hex_to_x25519_public_key(peer_pubkey)
187        .map_err(|_| CircleTransitionError::InvalidRecipient(peer_pubkey.to_string()))?;
188    let shared = keys::x25519_shared_secret(local_identity.to_x25519_secret_key(), peer_x25519)
189        .map_err(|_| CircleTransitionError::InvalidRecipient(peer_pubkey.to_string()))?;
190    let mut mac = Hmac::<Sha256>::new_from_slice(&shared).expect("HMAC accepts X25519 output");
191    mac.update(RECIPIENT_SLOT_DOMAIN);
192    mac.update(circle_id.as_bytes());
193    Ok(hex::encode(mac.finalize().into_bytes()))
194}