Skip to main content

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}