Skip to main content

dig_peer_protocol/
opcodes.rs

1//! The complete DIG opcode namespace — the `200..=226` band, in one place.
2//!
3//! DIG extends Chia's `ProtocolMessageTypes` (which stops at `RespondCostInfo = 107`) with a
4//! band that starts at **200**, leaving a 100-value gap for future upstream additions. The band
5//! has two halves:
6//!
7//! | Range | Half | Where it is defined |
8//! |---|---|---|
9//! | `200..=219` | **consensus** — the DIG L2 gossip opcodes | [`DigMessageType`], one variant each |
10//! | `220..=` | **free** — directed / broadcast application protocols | the constants below |
11//!
12//! The consensus half is an enum because each opcode has a fixed body type and a gossip
13//! strategy; the free half is plain constants because each opcode's body is owned by the
14//! application protocol that defines it, not by this crate.
15//!
16//! These values are **canonical**: a second implementation must match them byte for byte, and
17//! no assigned value ever moves (§5.1, additive only).
18
19use crate::DigMessageType;
20
21/// First opcode of the DIG band. Everything below this belongs to Chia.
22pub const DIG_BAND_START: u8 = 200;
23
24/// First opcode of the **free** half of the DIG band — application protocols, not L2 consensus.
25pub const FREE_BAND_START: u8 = 220;
26
27/// Wire opcode for a directed **dig-message** envelope (WU6, epic #796).
28///
29/// Carries a `dig-message` directed envelope as OPAQUE bytes in [`DigMessage::data`]. The
30/// transport (dig-gossip) never seals, opens, or parses it; end-to-end sealing to the
31/// recipient's DID key is `dig-message`'s job.
32///
33/// [`DigMessage::data`]: crate::DigMessage::data
34pub const DIG_MESSAGE: u8 = 220;
35
36/// Wire opcode for a **store-melted** broadcast (epic #1316).
37///
38/// Announces that a dig-store's on-chain coin has been melted, so peers stop hosting its `.dig`
39/// content. A public all-peers flood: signed and mTLS-authenticated, but NOT recipient-sealed —
40/// store deletion is addressed to everyone (the §5.4 public-broadcast carve-out).
41pub const STORE_MELTED: u8 = 221;
42
43/// Wire opcode for a **holdings-announce** broadcast (#1428, spec #1394).
44///
45/// Announces a batch of signed holdings add/remove deltas so peers learn which content a
46/// provider holds; this feeds dig-dht's holder set. Public flood, same carve-out as
47/// [`STORE_MELTED`].
48pub const HOLDINGS_ANNOUNCE: u8 = 222;
49
50/// Wire opcode for a **profile-root announce** broadcast (epic #3008).
51///
52/// Body is exactly 64 bytes: `store_id ‖ root`, two 32-byte hashes with no framing, announcing
53/// the sender's current profile-SMT root for that store. A public all-peers flood, same §5.4
54/// carve-out as [`STORE_MELTED`].
55///
56/// **Deliberately unsigned.** The authority for a profile root is the on-chain root, not the
57/// announcing peer, so the receiver compares any announced root against chain before trusting
58/// it. A forged announce therefore costs an attacker one wasted [`PROFILE_BODY_REQUEST`] that
59/// then fails that compare — signing would buy no additional guarantee while adding a signature
60/// verification to every message of the highest-volume broadcast in the band.
61pub const PROFILE_ROOT_ANNOUNCE: u8 = 223;
62
63/// Wire opcode for a directed **profile-body request** (epic #3008).
64///
65/// Body is exactly 64 bytes: `store_id ‖ root`, asking one peer for the profile body behind a
66/// root learned from a [`PROFILE_ROOT_ANNOUNCE`]. Answered with [`PROFILE_BODY`].
67pub const PROFILE_BODY_REQUEST: u8 = 224;
68
69/// Wire opcode for a directed **profile-body** response (epic #3008).
70///
71/// Body is `store_id ‖ root ‖ len:u32be ‖ body` — the two 32-byte hashes the request named,
72/// then a big-endian length prefix and that many bytes of profile body. The receiver rehashes
73/// the body and compares against `root`, which is why the announce that started the exchange
74/// needs no signature.
75pub const PROFILE_BODY: u8 = 225;
76
77/// Wire opcode for a **distributor-announce** broadcast (DIG-Network/dig_ecosystem#3252).
78///
79/// A public all-peers flood carrying untrusted reward-distributor discovery hints: a
80/// `store_id` plus the launcher ids of distributors the sender knows of for that store. Same
81/// §5.4 public-broadcast carve-out as [`STORE_MELTED`] and [`HOLDINGS_ANNOUNCE`].
82///
83/// Body layout (encoded downstream in dig-gossip, not this crate): `store_id(32) ‖
84/// launcher_id_count(u16 BE) ‖ launcher_ids(32 each, ≤ 32)`.
85///
86/// **Deliberately unsigned**, for the same reason [`PROFILE_ROOT_ANNOUNCE`] is: the authority
87/// for a distributor is the on-chain coin, not the announcing peer, so a receiver re-derives
88/// every property from chain before the launcher id becomes a candidate. Unlike
89/// [`HOLDINGS_ANNOUNCE`], the frame carries no addresses, so there is no address-rewriting or
90/// DHT-poisoning threat for a signature to close — a forged announce costs a receiver one
91/// wasted chain lookup that then fails that compare.
92///
93/// **A frame asserts membership only, and never completeness.** The launcher ids present are
94/// ids the sender claims to know of for that `store_id`. The absence of an id from a frame
95/// asserts nothing at all — not that the sender doesn't know of it, not that it has been
96/// evicted, nothing.
97///
98/// A receiver **MUST union** a received frame into whatever set it already holds for that
99/// store; it **MUST NOT** replace its per-store set from a frame, and it **MUST NOT** diff two
100/// frames against each other to infer a removal. This is what makes eviction
101/// **unrepresentable** on this wire: because absence carries no meaning, `old_set \ new_set` is
102/// never a signal, so a receiver that unions can never manufacture a false eviction
103/// (dig_ecosystem §12.5 clause 7). Age out an entry locally on your own retention policy, never
104/// on a peer's frame.
105///
106/// A sender who knows of more than `MAX_LAUNCHER_IDS_PER_ANNOUNCE` (32) distributors for one
107/// store sends **any subset of at most 32** and MAY rotate which subset it sends across frames.
108/// Because a frame is never a completeness claim, sending a partial subset is honest by
109/// construction — it is not truncation, and it cannot forge an eviction the way silently
110/// dropping ids from a claimed-complete snapshot would.
111///
112/// An **empty** launcher-id list is a distinct, positive statement in its own right: "my known
113/// set for this store is empty" — not "I have nothing to say." It is how this wire says "I know
114/// of none" instead of saying nothing, and a receiver MUST NOT read it as a request to clear
115/// what it already holds (see the union rule above).
116///
117/// A received announce is a **hint only**: it must not admit an entry, rank a candidate, or be
118/// a claim's authority. A peer that never hears one MUST still find and claim via §13.1 —
119/// this opcode is a latency shortcut, never a dependency (dig_ecosystem §13.2 clause 2).
120pub const DISTRIBUTOR_ANNOUNCE: u8 = 226;
121
122/// Every opcode DIG has assigned, ascending — the 20 consensus opcodes plus the 7 free-band ones.
123///
124/// This is the list a peer link dispatches on and the list a conformance test checks against
125/// Chia's namespace for collisions.
126pub const ALL_DIG_OPCODES: [u8; 27] = [
127    DigMessageType::NewAttestation as u8,
128    DigMessageType::NewCheckpointProposal as u8,
129    DigMessageType::NewCheckpointSignature as u8,
130    DigMessageType::RequestCheckpointSignatures as u8,
131    DigMessageType::RespondCheckpointSignatures as u8,
132    DigMessageType::RequestStatus as u8,
133    DigMessageType::RespondStatus as u8,
134    DigMessageType::NewCheckpointSubmission as u8,
135    DigMessageType::ValidatorAnnounce as u8,
136    DigMessageType::RequestBlockTransactions as u8,
137    DigMessageType::RespondBlockTransactions as u8,
138    DigMessageType::ReconciliationSketch as u8,
139    DigMessageType::ReconciliationResponse as u8,
140    DigMessageType::StemTransaction as u8,
141    DigMessageType::PlumtreeLazyAnnounce as u8,
142    DigMessageType::PlumtreePrune as u8,
143    DigMessageType::PlumtreeGraft as u8,
144    DigMessageType::PlumtreeRequestByHash as u8,
145    DigMessageType::RegisterPeer as u8,
146    DigMessageType::RegisterAck as u8,
147    DIG_MESSAGE,
148    STORE_MELTED,
149    HOLDINGS_ANNOUNCE,
150    PROFILE_ROOT_ANNOUNCE,
151    PROFILE_BODY_REQUEST,
152    PROFILE_BODY,
153    DISTRIBUTOR_ANNOUNCE,
154];
155
156/// Whether `opcode` belongs to the DIG band rather than Chia's namespace.
157///
158/// This is a *band* test, not an *assigned* test: an unassigned value such as `250` is still
159/// DIG's to allocate, and a link must route it to DIG handling (where it is rejected as unknown)
160/// rather than to a Chia decoder that would reject the whole connection.
161#[must_use]
162pub const fn is_dig_opcode(opcode: u8) -> bool {
163    opcode >= DIG_BAND_START
164}
165
166#[cfg(test)]
167mod tests {
168    use super::{
169        is_dig_opcode, ALL_DIG_OPCODES, DIG_BAND_START, DIG_MESSAGE, DISTRIBUTOR_ANNOUNCE,
170        FREE_BAND_START, HOLDINGS_ANNOUNCE, PROFILE_BODY, PROFILE_BODY_REQUEST,
171        PROFILE_ROOT_ANNOUNCE, STORE_MELTED,
172    };
173    use chia_protocol::ProtocolMessageTypes;
174    use chia_traits::Streamable;
175
176    /// No DIG opcode may ever collide with one Chia accepts — probed against the real decoder
177    /// over the whole `u8` space rather than against a transcribed copy of Chia's enum, so an
178    /// upstream addition that reached into the band would fail this test instead of silently
179    /// producing two meanings for one byte.
180    #[test]
181    fn dig_opcodes_are_disjoint_from_the_chia_namespace() {
182        for opcode in ALL_DIG_OPCODES {
183            assert!(
184                ProtocolMessageTypes::from_bytes(&[opcode]).is_err(),
185                "opcode {opcode} is claimed by both DIG and Chia"
186            );
187        }
188    }
189
190    /// The band is contiguous from 200 with no gaps and no duplicates: a gap would mean an
191    /// opcode was silently dropped from the list, a duplicate that two protocols share a byte.
192    #[test]
193    fn the_assigned_band_is_contiguous_from_200() {
194        let expected: Vec<u8> = (DIG_BAND_START..=DISTRIBUTOR_ANNOUNCE).collect();
195        assert_eq!(ALL_DIG_OPCODES.to_vec(), expected);
196    }
197
198    /// The free band starts exactly where the consensus band ends, and every assigned value is
199    /// pinned — these are cross-repo canonical constants that must not drift.
200    #[test]
201    fn free_band_constants_are_pinned() {
202        assert_eq!(FREE_BAND_START, 220);
203        assert_eq!(DIG_MESSAGE, 220);
204        assert_eq!(STORE_MELTED, 221);
205        assert_eq!(HOLDINGS_ANNOUNCE, 222);
206        assert_eq!(PROFILE_ROOT_ANNOUNCE, 223);
207        assert_eq!(PROFILE_BODY_REQUEST, 224);
208        assert_eq!(PROFILE_BODY, 225);
209        assert_eq!(DISTRIBUTOR_ANNOUNCE, 226);
210    }
211
212    /// `DISTRIBUTOR_ANNOUNCE` is a canonical constant (DIG-Network/dig_ecosystem#3252): a
213    /// second implementation must match this value byte for byte, so it is pinned as a literal
214    /// the same way the other free-band opcodes are.
215    #[test]
216    fn distributor_announce_is_pinned_to_226() {
217        assert_eq!(DISTRIBUTOR_ANNOUNCE, 226);
218    }
219
220    /// `ALL_DIG_OPCODES` must enumerate `DISTRIBUTOR_ANNOUNCE` — this is the only mechanism
221    /// that gives a 220-band opcode a rate limit in dig-gossip's exhaustiveness test
222    /// (`inbound_limits.rs`); an opcode missing from this array silently falls through to the
223    /// loose default rate-limit settings (the #1720/#1760-D fail-open bug).
224    #[test]
225    fn all_dig_opcodes_contains_distributor_announce() {
226        assert!(ALL_DIG_OPCODES.contains(&DISTRIBUTOR_ANNOUNCE));
227    }
228
229    /// `ALL_DIG_OPCODES` has no duplicates and is strictly ascending — a duplicate would mean
230    /// two protocols silently sharing one byte, a non-ascending entry would mean a copy/paste
231    /// error when a new opcode was appended.
232    #[test]
233    fn all_dig_opcodes_is_deduped_and_strictly_ascending() {
234        for window in ALL_DIG_OPCODES.windows(2) {
235            assert!(
236                window[0] < window[1],
237                "not strictly ascending at {window:?}"
238            );
239        }
240    }
241
242    /// `ALL_DIG_OPCODES` has grown to 27 entries: 20 consensus opcodes plus 7 free-band ones
243    /// now that `DISTRIBUTOR_ANNOUNCE` is assigned.
244    #[test]
245    fn all_dig_opcodes_has_27_entries() {
246        assert_eq!(ALL_DIG_OPCODES.len(), 27);
247    }
248
249    /// `is_dig_opcode` is a band test, so it must still recognize the newly assigned value.
250    #[test]
251    fn is_dig_opcode_recognizes_distributor_announce() {
252        assert!(is_dig_opcode(DISTRIBUTOR_ANNOUNCE));
253    }
254
255    /// The three profile-SMT opcodes are a single indivisible allocation: each one must be
256    /// present in the dispatch list, in order, or the sync protocol is only half-routable. A
257    /// list missing just the middle value still passes a naive "highest value is 225" check,
258    /// so this asserts the exact contiguous triple as a slice.
259    #[test]
260    fn the_profile_sync_triple_is_assigned_together() {
261        let start = ALL_DIG_OPCODES
262            .iter()
263            .position(|&op| op == PROFILE_ROOT_ANNOUNCE)
264            .expect("PROFILE_ROOT_ANNOUNCE must be present");
265        let triple = &ALL_DIG_OPCODES[start..start + 3];
266        assert_eq!(
267            triple,
268            [PROFILE_ROOT_ANNOUNCE, PROFILE_BODY_REQUEST, PROFILE_BODY]
269        );
270    }
271
272    /// The band predicate is pinned from BOTH sides: 199 is Chia's, 200 is DIG's.
273    #[test]
274    fn band_predicate_is_pinned_from_both_sides() {
275        assert!(!is_dig_opcode(DIG_BAND_START - 1));
276        assert!(is_dig_opcode(DIG_BAND_START));
277        assert!(is_dig_opcode(u8::MAX));
278    }
279}