coven_protocol/circle_control/
semantic_path.rs1use 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}