Skip to main content

heddle_thread_api/
hybrid.rs

1//! HYBRID transport checks. Structural proof closure is preparation only;
2//! authority is resolved independently at each serialized durable mutation.
3use 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
24/// Integration routes support HYBRID. Keep this single switch OFF until the
25/// coordinated Sync mandatory cutover in api#307; optional capable paths still
26/// require exact protocol negotiation before accepting any authority bundle.
27pub const SYNC_MANDATORY_GATE: bool = false;
28
29/// All ordinary Sync constructors use the same coordinated cutover switch.
30pub fn sync_protocol() -> Option<ProtocolCompatibility> {
31    SYNC_MANDATORY_GATE.then(protocol)
32}
33
34/// A creator binding opts StartThread into HYBRID before request PoP and I/O.
35pub 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}
70/// Complete structural closure, never signature trust, enrollment or admission.
71pub 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}
103/// Bind support to both ends of this exact stream, including its first Ready.
104pub 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    // Presence alone rejects: an empty-but-present bundle or protocol is still
185    // a claim this peer cannot honour, so the defaults below are deliberate.
186    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}