sui_rpc/light_client/error.rs
1use sui_sdk_types::Address;
2use sui_sdk_types::ObjectReference;
3use sui_sdk_types::framework::ApplyStreamError;
4use sui_sdk_types::framework::EventStreamHead;
5use sui_sdk_types::proof::ProofError;
6
7use crate::proto::TryFromProtoError;
8
9/// Errors returned by the [`crate::light_client`] APIs.
10#[derive(Debug)]
11#[non_exhaustive]
12pub enum LightClientError {
13 /// A gRPC call returned a non-success status.
14 Rpc(tonic::Status),
15
16 /// A protobuf message returned by the server was malformed.
17 Proto(TryFromProtoError),
18
19 /// BCS decoding of a wire-format payload (e.g. the response's
20 /// `checkpoint_summary` bytes) failed.
21 Bcs(bcs::Error),
22
23 /// The BLS aggregate signature on a checkpoint summary failed to
24 /// verify against the cached validator committee.
25 InvalidSignature(sui_crypto::SignatureError),
26
27 /// The OCS proof failed to verify against the trusted checkpoint
28 /// summary.
29 InvalidProof(ProofError),
30
31 /// The cache was asked to apply a ratchet update with a new committee
32 /// whose `epoch` was not exactly one greater than the current epoch.
33 InvalidEpochAdvance {
34 /// The current epoch held by the cache.
35 current: u64,
36 /// The advertised epoch of the new committee.
37 provided: u64,
38 },
39
40 /// A checkpoint summary advertised as the last of an epoch had no
41 /// `end_of_epoch_data` payload — the next epoch's committee cannot
42 /// be extracted.
43 MissingEndOfEpochData {
44 /// The checkpoint sequence number whose summary was missing the
45 /// payload.
46 checkpoint: u64,
47 },
48
49 /// The server returned a proof anchored at a different checkpoint
50 /// than the one the caller requested.
51 CheckpointMismatch {
52 /// The checkpoint sequence number the caller asked for.
53 requested: u64,
54 /// The checkpoint sequence number on the returned summary.
55 returned: u64,
56 },
57
58 /// After ratcheting forward, the cache still did not have a
59 /// committee on file for the relevant epoch. This should not
60 /// happen in practice and indicates either a bug or a malicious
61 /// server response.
62 NoCommitteeForEpoch {
63 /// The epoch number that has no committee on file.
64 epoch: u64,
65 },
66
67 /// The ratchet driver tried to advance through more epochs than
68 /// the configured cap (see
69 /// [`crate::light_client::RatchetConfig::max_ratchet_gap`]) allows
70 /// in a single call. This guard exists to bound the work the
71 /// client will do in response to a server that keeps reporting
72 /// `last_checkpoint < target_seq` for every epoch, never letting
73 /// the discovery walk terminate.
74 RatchetGapTooLarge {
75 /// The cache's current epoch when the ratchet began.
76 current: u64,
77 /// The epoch the discovery walk had reached when the cap was
78 /// hit. The actual target epoch may be even further out.
79 target: u64,
80 /// The configured maximum gap.
81 max: u64,
82 },
83
84 /// The fullnode reported `NotFound` for an epoch the ratchet
85 /// needed to advance through. Distinguished from a generic
86 /// `Rpc(Status)` so callers can differentiate "the network does
87 /// not know about this epoch" (likely a bug or a request for an
88 /// epoch past the chain's tip) from a transport-level failure.
89 EpochNotFound {
90 /// The epoch number that the fullnode could not produce.
91 epoch: u64,
92 },
93
94 /// The server returned an inclusion proof for a different object
95 /// id than the caller asked about.
96 ObjectIdMismatch {
97 /// The object id the caller asked about.
98 requested: Address,
99 /// The object id on the returned object reference.
100 returned: Address,
101 },
102
103 /// The `object_data` bytes the server returned did not BCS-decode to
104 /// an [`Object`] whose `(id, version, digest)` match the
105 /// authenticated `ObjectReference`. Without this check, a server
106 /// could pass the proof verification step while still returning
107 /// arbitrary bytes for the object's contents.
108 ///
109 /// Boxed because `ObjectReference` carries a 32-byte digest and a
110 /// 32-byte address; keeping two of them inline would inflate every
111 /// `Result<_, LightClientError>` slot by ~144 bytes for a case
112 /// that's exceedingly rare in practice.
113 ///
114 /// [`Object`]: sui_sdk_types::Object
115 ObjectDataMismatch(Box<ObjectDataMismatch>),
116
117 /// The locally-replayed [`EventStreamHead`] disagreed with the
118 /// authenticated head fetched from chain at the given checkpoint.
119 /// This is the streaming client's terminal failure mode — once the
120 /// MMRs diverge, every subsequent event would compound the
121 /// divergence, so the stream is aborted with this error.
122 ///
123 /// Boxed because `EventStreamHead` carries a `Vec<U256>` (each
124 /// `U256` is 32 bytes) and grows unbounded with checkpoint count;
125 /// keeping two of them inline would inflate every
126 /// `Result<_, LightClientError>` slot returned from the streaming
127 /// task.
128 MmrMismatch(Box<MmrMismatch>),
129
130 /// Folding a batch of received events into the local MMR violated
131 /// the [`apply_stream_updates`] contract — empty batch, mismatched
132 /// `checkpoint_seq` within a batch, or non-monotonic batch
133 /// ordering. Surfaces a malformed server response that slipped
134 /// past the per-event decode.
135 ///
136 /// [`apply_stream_updates`]: sui_sdk_types::framework::apply_stream_updates
137 InvalidEventBatch(ApplyStreamError),
138
139 /// An object returned by the OCS inclusion proof flow did not
140 /// match the shape the caller expected — for example, an
141 /// `EventStreamHead` fetch returned a package instead of a Move
142 /// struct, or the dynamic-field contents were too short to
143 /// contain the expected value.
144 UnexpectedObjectShape {
145 /// A short, human-readable description of what the caller was
146 /// expecting versus what the object actually was.
147 reason: &'static str,
148 },
149}
150
151/// Payload for [`LightClientError::ObjectDataMismatch`].
152#[derive(Debug)]
153pub struct ObjectDataMismatch {
154 /// The authenticated reference from the verified inclusion proof.
155 pub expected: ObjectReference,
156 /// The reference reconstructed from the returned `object_data`
157 /// bytes.
158 pub returned: ObjectReference,
159}
160
161/// Payload for [`LightClientError::MmrMismatch`].
162#[derive(Debug)]
163pub struct MmrMismatch {
164 /// The checkpoint at which the reconciliation was attempted.
165 pub checkpoint: u64,
166 /// The head fetched from chain via an OCS inclusion proof — the
167 /// truth against which the local replay was compared.
168 pub expected: EventStreamHead,
169 /// The head the streaming client computed by folding received
170 /// events into its local MMR.
171 pub actual: EventStreamHead,
172}
173
174impl std::fmt::Display for LightClientError {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 match self {
177 Self::Rpc(status) => write!(f, "gRPC error: {status}"),
178 Self::Proto(e) => write!(f, "malformed protobuf message: {e}"),
179 Self::Bcs(e) => write!(f, "BCS decoding failed: {e}"),
180 Self::InvalidSignature(e) => {
181 write!(f, "checkpoint summary signature did not verify: {e}")
182 }
183 Self::InvalidProof(e) => write!(f, "OCS proof did not verify: {e}"),
184 Self::InvalidEpochAdvance { current, provided } => write!(
185 f,
186 "cannot advance epoch cache: current epoch is {current}, but provided committee is for epoch {provided}"
187 ),
188 Self::MissingEndOfEpochData { checkpoint } => write!(
189 f,
190 "checkpoint {checkpoint} was advertised as end-of-epoch but its summary has no `end_of_epoch_data` payload"
191 ),
192 Self::CheckpointMismatch {
193 requested,
194 returned,
195 } => write!(
196 f,
197 "proof was returned for checkpoint {returned} but caller requested checkpoint {requested}"
198 ),
199 Self::NoCommitteeForEpoch { epoch } => write!(
200 f,
201 "no validator committee on file for epoch {epoch} after ratchet"
202 ),
203 Self::RatchetGapTooLarge {
204 current,
205 target,
206 max,
207 } => write!(
208 f,
209 "ratchet would advance from epoch {current} past epoch {target}, exceeding configured max gap of {max}"
210 ),
211 Self::EpochNotFound { epoch } => {
212 write!(f, "fullnode reported NotFound for epoch {epoch}")
213 }
214 Self::ObjectIdMismatch {
215 requested,
216 returned,
217 } => write!(
218 f,
219 "proof was returned for object {returned} but caller requested object {requested}"
220 ),
221 Self::ObjectDataMismatch(boxed) => {
222 let ObjectDataMismatch { expected, returned } = boxed.as_ref();
223 write!(
224 f,
225 "object_data bytes hash to {} version {} but the verified leaf attests to {} version {}",
226 returned.digest(),
227 returned.version(),
228 expected.digest(),
229 expected.version(),
230 )
231 }
232 Self::MmrMismatch(boxed) => {
233 let MmrMismatch {
234 checkpoint,
235 expected,
236 actual,
237 } = boxed.as_ref();
238 write!(
239 f,
240 "local MMR diverged from on-chain EventStreamHead at checkpoint {checkpoint}: \
241 locally replayed {} events to checkpoint {}, chain attests to {} events at \
242 checkpoint {}",
243 actual.num_events,
244 actual.checkpoint_seq,
245 expected.num_events,
246 expected.checkpoint_seq,
247 )
248 }
249 Self::InvalidEventBatch(e) => write!(f, "invalid event batch: {e}"),
250 Self::UnexpectedObjectShape { reason } => {
251 write!(f, "unexpected object shape: {reason}")
252 }
253 }
254 }
255}
256
257impl std::error::Error for LightClientError {
258 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
259 match self {
260 Self::Rpc(e) => Some(e),
261 Self::Proto(e) => Some(e),
262 Self::Bcs(e) => Some(e),
263 Self::InvalidSignature(e) => Some(e),
264 Self::InvalidProof(e) => Some(e),
265 Self::InvalidEventBatch(e) => Some(e),
266 _ => None,
267 }
268 }
269}
270
271impl From<tonic::Status> for LightClientError {
272 fn from(value: tonic::Status) -> Self {
273 Self::Rpc(value)
274 }
275}
276
277impl From<TryFromProtoError> for LightClientError {
278 fn from(value: TryFromProtoError) -> Self {
279 Self::Proto(value)
280 }
281}
282
283impl From<bcs::Error> for LightClientError {
284 fn from(value: bcs::Error) -> Self {
285 Self::Bcs(value)
286 }
287}
288
289impl From<sui_crypto::SignatureError> for LightClientError {
290 fn from(value: sui_crypto::SignatureError) -> Self {
291 Self::InvalidSignature(value)
292 }
293}
294
295impl From<ProofError> for LightClientError {
296 fn from(value: ProofError) -> Self {
297 Self::InvalidProof(value)
298 }
299}
300
301impl From<ApplyStreamError> for LightClientError {
302 fn from(value: ApplyStreamError) -> Self {
303 Self::InvalidEventBatch(value)
304 }
305}