dig_rpc_types/errors.rs
1//! Stable JSON-RPC error codes.
2//!
3//! # Contract
4//!
5//! Once a variant is assigned a numeric value, that value **never** changes.
6//! New variants append to the enum at the next unused integer. The enum is
7//! `#[non_exhaustive]` so clients built against an older version don't
8//! panic on a newer server's new code.
9//!
10//! # Ranges
11//!
12//! - `-32768..=-32000` — reserved by JSON-RPC 2.0. We use the spec-defined
13//! pre-defined values (`ParseError`, `InvalidRequest`, `MethodNotFound`,
14//! `InvalidParams`, `InternalError`).
15//! - `10000..` — DIG-reserved. Sub-allocated by concern:
16//! - `10001..=10009` — sync / auth / permission
17//! - `10010..=10019` — cryptographic checks
18//! - `10020..=10029` — L1 (Chia) issues
19//! - `10030..=10039` — wallet state
20//! - `10040..=10049` — lifecycle
21//! - `10050..=10059` — protocol / version
22//!
23//! # References
24//!
25//! - [JSON-RPC 2.0 spec §5.1](https://www.jsonrpc.org/specification#error_object)
26//! - [serde_repr](https://docs.rs/serde_repr) — the crate providing the
27//! `Serialize_repr` / `Deserialize_repr` derive macros used here.
28
29use serde_repr::{Deserialize_repr, Serialize_repr};
30
31/// Stable JSON-RPC error code.
32///
33/// Serialises as a bare integer on the wire (via `serde_repr`) so values
34/// are JSON-RPC-spec-compliant. The enum is `#[non_exhaustive]` so that
35/// adding variants in minor releases is backwards-compatible — clients
36/// built against an older `dig-rpc-types` must use `_ => ...` when
37/// matching.
38#[repr(i32)]
39#[non_exhaustive]
40#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq, Hash)]
41pub enum ErrorCode {
42 // ---------- JSON-RPC 2.0 reserved range ----------
43 /// Invalid JSON was received by the server.
44 ParseError = -32700,
45 /// The JSON sent is not a valid Request object.
46 InvalidRequest = -32600,
47 /// The method does not exist / is not available.
48 MethodNotFound = -32601,
49 /// Invalid method parameter(s).
50 InvalidParams = -32602,
51 /// Internal JSON-RPC error.
52 InternalError = -32603,
53
54 // ---------- DIG reserved 1xxxx range ----------
55 /// The fullnode is not yet synced to chain tip; the requested data
56 /// may be stale or unavailable.
57 NotSynced = 10001,
58 /// The peer's certificate does not resolve to any known role (internal
59 /// server only).
60 PeerUnauthorized = 10002,
61 /// The peer's resolved role is below the method's `min_role`.
62 PermissionDenied = 10003,
63 /// The peer exceeded its per-opcode token bucket.
64 RateLimited = 10004,
65 /// The requested block / coin / validator was not found.
66 ResourceNotFound = 10005,
67
68 /// The validator's local slashing-protection DB would veto this
69 /// signature (e.g., proposed-slot watermark, attested-target watermark).
70 SlashingGuardBlocked = 10010,
71 /// A provided BLS signature failed verification.
72 InvalidSignature = 10011,
73 /// A provided Groth16 or other ZK proof failed verification.
74 InvalidProof = 10012,
75
76 /// Cannot reach Chia L1 (peer pool exhausted, no coinset fallback).
77 L1Unavailable = 10020,
78
79 /// The wallet is locked; unlock before calling signing methods.
80 WalletLocked = 10030,
81
82 /// Server is shutting down; retry against another peer.
83 ShutdownPending = 10040,
84
85 /// The client's advertised network id differs from the server's.
86 NetworkMismatch = 10050,
87 /// The client's advertised schema major version differs from the server's.
88 VersionMismatch = 10051,
89}
90
91impl ErrorCode {
92 /// Return the raw integer code.
93 ///
94 /// Equivalent to `self as i32`; provided as an explicit method so
95 /// callers don't rely on repr casting.
96 pub fn code(self) -> i32 {
97 self as i32
98 }
99
100 /// Whether this variant is in the JSON-RPC-reserved range.
101 pub fn is_jsonrpc_reserved(self) -> bool {
102 let c = self.code();
103 (-32768..=-32000).contains(&c)
104 }
105
106 /// Whether this variant is DIG-specific (1xxxx range).
107 pub fn is_dig_specific(self) -> bool {
108 self.code() >= 10000
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 /// **Proves:** every variant's numeric value is pinned to the value
117 /// declared in the enum — that is, [`ErrorCode::MethodNotFound as i32`]
118 /// equals `-32601`.
119 ///
120 /// **Why it matters:** Once published, error codes are a permanent part
121 /// of the wire contract. Reordering the enum variants or switching
122 /// `#[repr]` types would change the on-wire integers and break every
123 /// deployed client.
124 ///
125 /// **Catches:** a mass rename-or-reorder of the enum that accidentally
126 /// renumbers variants; removing `#[repr(i32)]`; switching to a
127 /// different representation.
128 #[test]
129 fn numeric_values_pinned() {
130 assert_eq!(ErrorCode::ParseError as i32, -32700);
131 assert_eq!(ErrorCode::InvalidRequest as i32, -32600);
132 assert_eq!(ErrorCode::MethodNotFound as i32, -32601);
133 assert_eq!(ErrorCode::InvalidParams as i32, -32602);
134 assert_eq!(ErrorCode::InternalError as i32, -32603);
135 assert_eq!(ErrorCode::NotSynced as i32, 10001);
136 assert_eq!(ErrorCode::PeerUnauthorized as i32, 10002);
137 assert_eq!(ErrorCode::PermissionDenied as i32, 10003);
138 assert_eq!(ErrorCode::RateLimited as i32, 10004);
139 assert_eq!(ErrorCode::ResourceNotFound as i32, 10005);
140 assert_eq!(ErrorCode::SlashingGuardBlocked as i32, 10010);
141 assert_eq!(ErrorCode::InvalidSignature as i32, 10011);
142 assert_eq!(ErrorCode::InvalidProof as i32, 10012);
143 assert_eq!(ErrorCode::L1Unavailable as i32, 10020);
144 assert_eq!(ErrorCode::WalletLocked as i32, 10030);
145 assert_eq!(ErrorCode::ShutdownPending as i32, 10040);
146 assert_eq!(ErrorCode::NetworkMismatch as i32, 10050);
147 assert_eq!(ErrorCode::VersionMismatch as i32, 10051);
148 }
149
150 /// **Proves:** serialisation produces a bare integer (not a tagged enum
151 /// object).
152 ///
153 /// **Why it matters:** JSON-RPC 2.0 mandates that `error.code` is a
154 /// number. A tagged form (`{"MethodNotFound": null}`) would violate the
155 /// spec and break every standards-compliant client library.
156 ///
157 /// **Catches:** a regression where `Serialize_repr` is replaced with
158 /// plain `Serialize` (which would produce `"MethodNotFound"` string).
159 #[test]
160 fn serialises_as_integer() {
161 let s = serde_json::to_string(&ErrorCode::MethodNotFound).unwrap();
162 assert_eq!(s, "-32601");
163
164 let s = serde_json::to_string(&ErrorCode::NotSynced).unwrap();
165 assert_eq!(s, "10001");
166 }
167
168 /// **Proves:** `is_jsonrpc_reserved` / `is_dig_specific` correctly
169 /// classify variants by their numeric range.
170 ///
171 /// **Why it matters:** Middleware may want to treat the two classes
172 /// differently (log DIG-specific errors with more context). Pinning
173 /// the classification prevents subtle bugs where a new variant lands
174 /// in the wrong range.
175 ///
176 /// **Catches:** adding a new variant in the wrong range (e.g.,
177 /// `-32500` — outside the reserved JSON-RPC range and also outside
178 /// the DIG range).
179 #[test]
180 fn range_classifiers_work() {
181 assert!(ErrorCode::MethodNotFound.is_jsonrpc_reserved());
182 assert!(!ErrorCode::MethodNotFound.is_dig_specific());
183
184 assert!(ErrorCode::NotSynced.is_dig_specific());
185 assert!(!ErrorCode::NotSynced.is_jsonrpc_reserved());
186 }
187}