dig_peer_protocol/opcodes.rs
1//! The complete DIG opcode namespace — the `200..=225` 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/// Every opcode DIG has assigned, ascending — the 20 consensus opcodes plus the 6 free-band ones.
78///
79/// This is the list a peer link dispatches on and the list a conformance test checks against
80/// Chia's namespace for collisions.
81pub const ALL_DIG_OPCODES: [u8; 26] = [
82 DigMessageType::NewAttestation as u8,
83 DigMessageType::NewCheckpointProposal as u8,
84 DigMessageType::NewCheckpointSignature as u8,
85 DigMessageType::RequestCheckpointSignatures as u8,
86 DigMessageType::RespondCheckpointSignatures as u8,
87 DigMessageType::RequestStatus as u8,
88 DigMessageType::RespondStatus as u8,
89 DigMessageType::NewCheckpointSubmission as u8,
90 DigMessageType::ValidatorAnnounce as u8,
91 DigMessageType::RequestBlockTransactions as u8,
92 DigMessageType::RespondBlockTransactions as u8,
93 DigMessageType::ReconciliationSketch as u8,
94 DigMessageType::ReconciliationResponse as u8,
95 DigMessageType::StemTransaction as u8,
96 DigMessageType::PlumtreeLazyAnnounce as u8,
97 DigMessageType::PlumtreePrune as u8,
98 DigMessageType::PlumtreeGraft as u8,
99 DigMessageType::PlumtreeRequestByHash as u8,
100 DigMessageType::RegisterPeer as u8,
101 DigMessageType::RegisterAck as u8,
102 DIG_MESSAGE,
103 STORE_MELTED,
104 HOLDINGS_ANNOUNCE,
105 PROFILE_ROOT_ANNOUNCE,
106 PROFILE_BODY_REQUEST,
107 PROFILE_BODY,
108];
109
110/// Whether `opcode` belongs to the DIG band rather than Chia's namespace.
111///
112/// This is a *band* test, not an *assigned* test: an unassigned value such as `250` is still
113/// DIG's to allocate, and a link must route it to DIG handling (where it is rejected as unknown)
114/// rather than to a Chia decoder that would reject the whole connection.
115#[must_use]
116pub const fn is_dig_opcode(opcode: u8) -> bool {
117 opcode >= DIG_BAND_START
118}
119
120#[cfg(test)]
121mod tests {
122 use super::{
123 is_dig_opcode, ALL_DIG_OPCODES, DIG_BAND_START, DIG_MESSAGE, FREE_BAND_START,
124 HOLDINGS_ANNOUNCE, PROFILE_BODY, PROFILE_BODY_REQUEST, PROFILE_ROOT_ANNOUNCE, STORE_MELTED,
125 };
126 use chia_protocol::ProtocolMessageTypes;
127 use chia_traits::Streamable;
128
129 /// No DIG opcode may ever collide with one Chia accepts — probed against the real decoder
130 /// over the whole `u8` space rather than against a transcribed copy of Chia's enum, so an
131 /// upstream addition that reached into the band would fail this test instead of silently
132 /// producing two meanings for one byte.
133 #[test]
134 fn dig_opcodes_are_disjoint_from_the_chia_namespace() {
135 for opcode in ALL_DIG_OPCODES {
136 assert!(
137 ProtocolMessageTypes::from_bytes(&[opcode]).is_err(),
138 "opcode {opcode} is claimed by both DIG and Chia"
139 );
140 }
141 }
142
143 /// The band is contiguous from 200 with no gaps and no duplicates: a gap would mean an
144 /// opcode was silently dropped from the list, a duplicate that two protocols share a byte.
145 #[test]
146 fn the_assigned_band_is_contiguous_from_200() {
147 let expected: Vec<u8> = (DIG_BAND_START..=PROFILE_BODY).collect();
148 assert_eq!(ALL_DIG_OPCODES.to_vec(), expected);
149 }
150
151 /// The free band starts exactly where the consensus band ends, and every assigned value is
152 /// pinned — these are cross-repo canonical constants that must not drift.
153 #[test]
154 fn free_band_constants_are_pinned() {
155 assert_eq!(FREE_BAND_START, 220);
156 assert_eq!(DIG_MESSAGE, 220);
157 assert_eq!(STORE_MELTED, 221);
158 assert_eq!(HOLDINGS_ANNOUNCE, 222);
159 assert_eq!(PROFILE_ROOT_ANNOUNCE, 223);
160 assert_eq!(PROFILE_BODY_REQUEST, 224);
161 assert_eq!(PROFILE_BODY, 225);
162 }
163
164 /// The three profile-SMT opcodes are a single indivisible allocation: each one must be
165 /// present in the dispatch list, in order, or the sync protocol is only half-routable. A
166 /// list missing just the middle value still passes a naive "highest value is 225" check,
167 /// so this asserts the exact contiguous triple as a slice.
168 #[test]
169 fn the_profile_sync_triple_is_assigned_together() {
170 let tail = &ALL_DIG_OPCODES[ALL_DIG_OPCODES.len() - 3..];
171 assert_eq!(
172 tail,
173 [PROFILE_ROOT_ANNOUNCE, PROFILE_BODY_REQUEST, PROFILE_BODY]
174 );
175 }
176
177 /// The band predicate is pinned from BOTH sides: 199 is Chia's, 200 is DIG's.
178 #[test]
179 fn band_predicate_is_pinned_from_both_sides() {
180 assert!(!is_dig_opcode(DIG_BAND_START - 1));
181 assert!(is_dig_opcode(DIG_BAND_START));
182 assert!(is_dig_opcode(u8::MAX));
183 }
184}