1use api::heddle::api::common::ProtocolCompatibility;
4
5use crate::contract::{
6 FetchOpen, ImportPublicProofBundleV1, NativePublicProofBundleV1, PublicationReceipt,
7 PublishContentOpen, ReplicationOpen, ReplicationOperations, ReplicationReady, StreamOpen,
8 TransferReady,
9};
10
11#[cfg(feature = "native")]
12pub mod authority;
13#[cfg(all(test, feature = "native"))]
14mod foreign_tests;
15pub mod history;
16#[cfg(test)]
17mod history_tests;
18#[cfg(all(test, feature = "native"))]
19mod native_tests;
20#[cfg(test)]
21mod protocol_tests;
22pub type Rejection = &'static str;
23
24pub const SYNC_MANDATORY_GATE: bool = false;
28
29pub fn sync_protocol() -> Option<ProtocolCompatibility> {
31 SYNC_MANDATORY_GATE.then(protocol)
32}
33
34pub fn native_start_thread(method_path: &str, encoded: &[u8]) -> Result<bool, Rejection> {
36 if method_path.trim_start_matches('/') != "heddle.api.v1alpha2.ThreadService/StartThread" {
37 return Ok(false);
38 }
39 use prost::Message;
40 let request = crate::contract::StartThreadRequest::decode(encoded)
41 .map_err(|_| "invalid StartThread request")?;
42 let Some(binding) = request.native_genesis_authority.as_ref() else {
43 return Ok(false);
44 };
45 api::native_witness::verify_genesis_authority(
46 binding,
47 request
48 .thread_genesis
49 .as_ref()
50 .ok_or("native genesis absent")?,
51 &request.creator_authority,
52 )
53 .map_err(|_| "invalid native creator binding")?;
54 Ok(true)
55}
56
57pub fn protocol() -> ProtocolCompatibility {
58 ProtocolCompatibility {
59 protocol_version: 2,
60 mandatory_features: vec![1],
61 }
62}
63fn check_protocol(value: Option<&ProtocolCompatibility>) -> Result<(), Rejection> {
64 if SYNC_MANDATORY_GATE || value.is_some() {
65 api::import_authority::require_hybrid_peer(value)
66 .map_err(|_| "incompatible HYBRID protocol (api#307 cutover)")?;
67 }
68 Ok(())
69}
70pub fn bundle(value: Option<&ImportPublicProofBundleV1>) -> Result<(), Rejection> {
72 if let Some(value) = value {
73 api::import_authority::validate_public_bundle(value)
74 .map_err(|_| "incomplete HYBRID import authority (api#307 cutover)")?;
75 }
76 Ok(())
77}
78pub fn bundles(
79 imported: Option<&ImportPublicProofBundleV1>,
80 native: Option<&NativePublicProofBundleV1>,
81) -> Result<(), Rejection> {
82 api::native_witness::validate_carriers(imported, native).map_err(|_| {
83 if native.is_some() {
84 "incomplete or conflicting HYBRID native authority (api#307 cutover)"
85 } else {
86 "incomplete HYBRID import authority (api#307 cutover)"
87 }
88 })
89}
90fn carrier(
91 protocol: Option<&ProtocolCompatibility>,
92 value: Option<&ImportPublicProofBundleV1>,
93 native: Option<&NativePublicProofBundleV1>,
94) -> Result<(), Rejection> {
95 check_protocol(protocol)?;
96 if value.is_some() || native.is_some() {
97 api::import_authority::require_hybrid_peer(protocol).map_err(
98 |_| "import authority requires negotiated HYBRID protocol (api#307 cutover)",
99 )?;
100 }
101 bundles(value, native)
102}
103pub fn negotiated(
105 open: Option<&ProtocolCompatibility>,
106 ready: Option<&ProtocolCompatibility>,
107) -> Result<(), Rejection> {
108 check_protocol(open)?;
109 check_protocol(ready)?;
110 if open != ready {
111 return Err("HYBRID protocol differs from stream opening");
112 }
113 Ok(())
114}
115pub fn operations(batch: &ReplicationOperations) -> Result<(), Rejection> {
116 bundles(
117 batch.import_authority.as_ref(),
118 batch.native_authority.as_ref(),
119 )
120}
121pub fn replication_open(open: &ReplicationOpen) -> Result<(), Rejection> {
122 carrier(
123 open.protocol.as_ref(),
124 open.import_authority.as_ref(),
125 open.native_authority.as_ref(),
126 )
127}
128pub fn replication_ready(ready: &ReplicationReady) -> Result<(), Rejection> {
129 carrier(
130 ready.protocol.as_ref(),
131 ready.import_authority.as_ref(),
132 ready.native_authority.as_ref(),
133 )
134}
135pub fn fetch_open(open: &FetchOpen) -> Result<(), Rejection> {
136 check_protocol(open.protocol.as_ref())
137}
138pub fn transfer_ready(ready: &TransferReady) -> Result<(), Rejection> {
139 carrier(
140 ready.protocol.as_ref(),
141 ready.import_authority.as_ref(),
142 ready.native_authority.as_ref(),
143 )
144}
145pub fn publish_open(open: &PublishContentOpen) -> Result<(), Rejection> {
146 carrier(
147 open.protocol.as_ref(),
148 open.import_authority.as_ref(),
149 open.native_authority.as_ref(),
150 )
151}
152pub fn publication_receipt(receipt: &PublicationReceipt) -> Result<(), Rejection> {
153 bundles(
154 receipt.import_authority.as_ref(),
155 receipt.native_authority.as_ref(),
156 )
157}
158pub fn stream_open(open: &StreamOpen) -> Result<(), Rejection> {
159 check_protocol(open.protocol.as_ref())?;
160 if let Some(set) = &open.witness_set {
161 use prost::Message;
162 api::import_authority::require_hybrid_peer(open.protocol.as_ref())
163 .map_err(|_| "witness set requires HYBRID protocol (api#307 cutover)")?;
164 if set.encoded_len() > api::witness_trust::MAX_SET_BYTES || set.body.is_none() {
165 return Err("invalid HYBRID witness set");
166 }
167 }
168 Ok(())
169}
170
171#[cfg(test)]
172mod tests {
173 use api::heddle::api::common::{ProtocolCompatibility, SignedHostedWitnessSetV1};
174
175 use super::*;
176 use crate::contract::ImportPublicProofBundleV1;
177
178 fn rejected(result: Result<(), Rejection>, field: &str) {
179 let message = result.expect_err(field);
180 assert!(message.contains(field), "{message}");
181 assert!(message.contains("api#307"), "{message}");
182 }
183
184 fn bundle() -> Option<ImportPublicProofBundleV1> {
187 Some(ImportPublicProofBundleV1::default())
188 }
189 fn protocol() -> Option<ProtocolCompatibility> {
190 Some(ProtocolCompatibility::default())
191 }
192
193 #[test]
194 fn absent_hybrid_fields_are_accepted() {
195 operations(&ReplicationOperations::default()).expect("operations");
196 replication_open(&ReplicationOpen::default()).expect("replication open");
197 replication_ready(&ReplicationReady::default()).expect("replication ready");
198 fetch_open(&FetchOpen::default()).expect("fetch open");
199 transfer_ready(&TransferReady::default()).expect("transfer ready");
200 publish_open(&PublishContentOpen::default()).expect("publish open");
201 publication_receipt(&PublicationReceipt::default()).expect("receipt");
202 stream_open(&StreamOpen::default()).expect("stream open");
203 }
204
205 #[test]
206 fn incomplete_import_authority_is_rejected_never_ignored() {
207 rejected(
208 operations(&ReplicationOperations {
209 import_authority: bundle(),
210 ..Default::default()
211 }),
212 "import authority",
213 );
214 rejected(
215 replication_open(&ReplicationOpen {
216 import_authority: bundle(),
217 ..Default::default()
218 }),
219 "import authority",
220 );
221 rejected(
222 replication_ready(&ReplicationReady {
223 import_authority: bundle(),
224 ..Default::default()
225 }),
226 "import authority",
227 );
228 rejected(
229 transfer_ready(&TransferReady {
230 import_authority: bundle(),
231 ..Default::default()
232 }),
233 "import authority",
234 );
235 rejected(
236 publish_open(&PublishContentOpen {
237 import_authority: bundle(),
238 ..Default::default()
239 }),
240 "import authority",
241 );
242 rejected(
243 publication_receipt(&PublicationReceipt {
244 import_authority: bundle(),
245 ..Default::default()
246 }),
247 "import authority",
248 );
249 }
250
251 #[test]
252 fn unsupported_protocol_and_incomplete_witness_sets_are_rejected() {
253 rejected(
254 replication_open(&ReplicationOpen {
255 protocol: protocol(),
256 ..Default::default()
257 }),
258 "protocol",
259 );
260 rejected(
261 replication_ready(&ReplicationReady {
262 protocol: protocol(),
263 ..Default::default()
264 }),
265 "protocol",
266 );
267 rejected(
268 fetch_open(&FetchOpen {
269 protocol: protocol(),
270 ..Default::default()
271 }),
272 "protocol",
273 );
274 rejected(
275 transfer_ready(&TransferReady {
276 protocol: protocol(),
277 ..Default::default()
278 }),
279 "protocol",
280 );
281 rejected(
282 publish_open(&PublishContentOpen {
283 protocol: protocol(),
284 ..Default::default()
285 }),
286 "protocol",
287 );
288 rejected(
289 stream_open(&StreamOpen {
290 protocol: protocol(),
291 ..Default::default()
292 }),
293 "protocol",
294 );
295 rejected(
296 stream_open(&StreamOpen {
297 witness_set: Some(SignedHostedWitnessSetV1::default()),
298 ..Default::default()
299 }),
300 "witness set",
301 );
302 }
303}