Skip to main content

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}