Skip to main content

sui_rpc/proto/sui/rpc/v2alpha/
proof_service.rs

1//! Conversions between the v2alpha `ProofService` proto messages and the
2//! in-memory proof / Merkle types in `sui_sdk_types`.
3//!
4//! The proto wire format encodes Merkle proofs natively — each node is a
5//! `oneof { Empty empty, bytes digest }` — rather than carrying a single
6//! BCS-encoded blob. The conversions in this module unpack that proto
7//! representation into the SDK's in-memory `Node` and `MerkleProof`
8//! types, validating that every digest is exactly 32 bytes and that
9//! every `MerkleNode` has its `oneof` populated.
10//!
11//! Conversions for the higher-level `OcsInclusionProof` and
12//! `OcsNonInclusionProof` envelopes layer on top of those primitives and
13//! the v2 `ObjectReference` conversions.
14
15use super::*;
16use crate::proto::TryFromProtoError;
17
18use sui_sdk_types::Digest;
19use sui_sdk_types::ObjectReference;
20use sui_sdk_types::merkle::MerkleNonInclusionProof as SdkMerkleNonInclusionProof;
21use sui_sdk_types::merkle::Node;
22use sui_sdk_types::proof::OcsInclusionProof as SdkOcsInclusionProof;
23use sui_sdk_types::proof::OcsNonInclusionProof as SdkOcsNonInclusionProof;
24
25const DIGEST_LEN: usize = 32;
26
27/// Decode a 32-byte field from a `Bytes` blob into a `Digest`, with a
28/// `TryFromProtoError` pointing at the named field on failure.
29fn try_digest_from_bytes(
30    bytes: &prost::bytes::Bytes,
31    field: &'static str,
32) -> Result<Digest, TryFromProtoError> {
33    let len = bytes.len();
34    let arr: [u8; DIGEST_LEN] = bytes.as_ref().try_into().map_err(|_| {
35        TryFromProtoError::invalid(field, format!("expected {DIGEST_LEN} bytes, got {len}"))
36    })?;
37    Ok(Digest::new(arr))
38}
39
40impl From<Node> for MerkleNode {
41    fn from(value: Node) -> Self {
42        let node = match value {
43            Node::Empty => merkle_node::Node::Empty(()),
44            Node::Digest(digest) => {
45                merkle_node::Node::Digest(prost::bytes::Bytes::copy_from_slice(&digest))
46            }
47        };
48        Self { node: Some(node) }
49    }
50}
51
52impl From<&Node> for MerkleNode {
53    fn from(value: &Node) -> Self {
54        (*value).into()
55    }
56}
57
58impl TryFrom<&MerkleNode> for Node {
59    type Error = TryFromProtoError;
60
61    fn try_from(value: &MerkleNode) -> Result<Self, Self::Error> {
62        match value.node.as_ref() {
63            Some(merkle_node::Node::Empty(_)) => Ok(Node::Empty),
64            Some(merkle_node::Node::Digest(bytes)) => {
65                let len = bytes.len();
66                let digest: [u8; DIGEST_LEN] = bytes.as_ref().try_into().map_err(|_| {
67                    TryFromProtoError::invalid(
68                        MerkleNode::DIGEST_FIELD.name,
69                        format!("expected {DIGEST_LEN} bytes, got {len}"),
70                    )
71                })?;
72                Ok(Node::Digest(digest))
73            }
74            None => Err(TryFromProtoError::missing("node")),
75        }
76    }
77}
78
79impl From<&sui_sdk_types::merkle::MerkleProof> for MerkleProof {
80    fn from(value: &sui_sdk_types::merkle::MerkleProof) -> Self {
81        Self {
82            path: value.path().iter().copied().map(MerkleNode::from).collect(),
83        }
84    }
85}
86
87impl From<sui_sdk_types::merkle::MerkleProof> for MerkleProof {
88    fn from(value: sui_sdk_types::merkle::MerkleProof) -> Self {
89        (&value).into()
90    }
91}
92
93impl TryFrom<&MerkleProof> for sui_sdk_types::merkle::MerkleProof {
94    type Error = TryFromProtoError;
95
96    fn try_from(value: &MerkleProof) -> Result<Self, Self::Error> {
97        let path = value
98            .path
99            .iter()
100            .enumerate()
101            .map(|(i, node)| {
102                Node::try_from(node).map_err(|e| e.nested_at(MerkleProof::PATH_FIELD.name, i))
103            })
104            .collect::<Result<Vec<_>, _>>()?;
105        Ok(sui_sdk_types::merkle::MerkleProof::new(path))
106    }
107}
108
109impl From<&(ObjectReference, sui_sdk_types::merkle::MerkleProof)> for MerkleNeighbourLeaf {
110    fn from(value: &(ObjectReference, sui_sdk_types::merkle::MerkleProof)) -> Self {
111        let (leaf, proof) = value;
112        Self {
113            leaf: Some(leaf.clone().into()),
114            merkle_proof: Some(proof.into()),
115        }
116    }
117}
118
119impl TryFrom<&MerkleNeighbourLeaf> for (ObjectReference, sui_sdk_types::merkle::MerkleProof) {
120    type Error = TryFromProtoError;
121
122    fn try_from(value: &MerkleNeighbourLeaf) -> Result<Self, Self::Error> {
123        let leaf_proto = value
124            .leaf
125            .as_ref()
126            .ok_or_else(|| TryFromProtoError::missing(MerkleNeighbourLeaf::LEAF_FIELD.name))?;
127        let leaf: ObjectReference = leaf_proto
128            .try_into()
129            .map_err(|e: TryFromProtoError| e.nested(MerkleNeighbourLeaf::LEAF_FIELD.name))?;
130        let proof_proto = value.merkle_proof.as_ref().ok_or_else(|| {
131            TryFromProtoError::missing(MerkleNeighbourLeaf::MERKLE_PROOF_FIELD.name)
132        })?;
133        let proof = sui_sdk_types::merkle::MerkleProof::try_from(proof_proto)
134            .map_err(|e| e.nested(MerkleNeighbourLeaf::MERKLE_PROOF_FIELD.name))?;
135        Ok((leaf, proof))
136    }
137}
138
139impl From<&SdkMerkleNonInclusionProof<ObjectReference>> for MerkleNonInclusionProof {
140    fn from(value: &SdkMerkleNonInclusionProof<ObjectReference>) -> Self {
141        Self {
142            index: Some(value.index() as u64),
143            left_leaf: value.left_leaf().map(MerkleNeighbourLeaf::from),
144            right_leaf: value.right_leaf().map(MerkleNeighbourLeaf::from),
145        }
146    }
147}
148
149impl From<SdkMerkleNonInclusionProof<ObjectReference>> for MerkleNonInclusionProof {
150    fn from(value: SdkMerkleNonInclusionProof<ObjectReference>) -> Self {
151        (&value).into()
152    }
153}
154
155impl TryFrom<&MerkleNonInclusionProof> for SdkMerkleNonInclusionProof<ObjectReference> {
156    type Error = TryFromProtoError;
157
158    fn try_from(value: &MerkleNonInclusionProof) -> Result<Self, Self::Error> {
159        let index_u64 = value
160            .index
161            .ok_or_else(|| TryFromProtoError::missing(MerkleNonInclusionProof::INDEX_FIELD.name))?;
162        let index = usize::try_from(index_u64).map_err(|e| {
163            TryFromProtoError::invalid(MerkleNonInclusionProof::INDEX_FIELD.name, e)
164        })?;
165        let left_leaf = value
166            .left_leaf
167            .as_ref()
168            .map(|l| {
169                <(ObjectReference, sui_sdk_types::merkle::MerkleProof)>::try_from(l)
170                    .map_err(|e| e.nested(MerkleNonInclusionProof::LEFT_LEAF_FIELD.name))
171            })
172            .transpose()?;
173        let right_leaf = value
174            .right_leaf
175            .as_ref()
176            .map(|l| {
177                <(ObjectReference, sui_sdk_types::merkle::MerkleProof)>::try_from(l)
178                    .map_err(|e| e.nested(MerkleNonInclusionProof::RIGHT_LEAF_FIELD.name))
179            })
180            .transpose()?;
181        Ok(SdkMerkleNonInclusionProof::new(
182            index, left_leaf, right_leaf,
183        ))
184    }
185}
186
187impl From<&SdkOcsInclusionProof> for OcsInclusionProof {
188    fn from(value: &SdkOcsInclusionProof) -> Self {
189        Self {
190            // `object_ref` and `object_data` are carried alongside the
191            // proof on the wire (so the server can hand the SDK an
192            // authenticated reference and the corresponding object
193            // bytes in a single round trip), but they're populated by
194            // the `LightClient` glue from the response fields rather
195            // than by `OcsInclusionProof` itself. This conversion only
196            // touches the cryptographic-proof fields.
197            object_ref: None,
198            merkle_proof: Some((&value.merkle_proof).into()),
199            leaf_index: Some(value.leaf_index),
200            tree_root: Some(prost::bytes::Bytes::copy_from_slice(
201                value.tree_root.inner(),
202            )),
203            object_data: None,
204        }
205    }
206}
207
208impl From<SdkOcsInclusionProof> for OcsInclusionProof {
209    fn from(value: SdkOcsInclusionProof) -> Self {
210        (&value).into()
211    }
212}
213
214impl TryFrom<&OcsInclusionProof> for SdkOcsInclusionProof {
215    type Error = TryFromProtoError;
216
217    fn try_from(value: &OcsInclusionProof) -> Result<Self, Self::Error> {
218        let merkle_proof_proto = value.merkle_proof.as_ref().ok_or_else(|| {
219            TryFromProtoError::missing(OcsInclusionProof::MERKLE_PROOF_FIELD.name)
220        })?;
221        let merkle_proof = sui_sdk_types::merkle::MerkleProof::try_from(merkle_proof_proto)
222            .map_err(|e| e.nested(OcsInclusionProof::MERKLE_PROOF_FIELD.name))?;
223        let leaf_index = value
224            .leaf_index
225            .ok_or_else(|| TryFromProtoError::missing(OcsInclusionProof::LEAF_INDEX_FIELD.name))?;
226        let tree_root_bytes = value
227            .tree_root
228            .as_ref()
229            .ok_or_else(|| TryFromProtoError::missing(OcsInclusionProof::TREE_ROOT_FIELD.name))?;
230        let tree_root =
231            try_digest_from_bytes(tree_root_bytes, OcsInclusionProof::TREE_ROOT_FIELD.name)?;
232        Ok(SdkOcsInclusionProof {
233            merkle_proof,
234            leaf_index,
235            tree_root,
236        })
237    }
238}
239
240impl From<&SdkOcsNonInclusionProof> for OcsNonInclusionProof {
241    fn from(value: &SdkOcsNonInclusionProof) -> Self {
242        Self {
243            non_inclusion_proof: Some((&value.non_inclusion_proof).into()),
244            tree_root: Some(prost::bytes::Bytes::copy_from_slice(
245                value.tree_root.inner(),
246            )),
247        }
248    }
249}
250
251impl From<SdkOcsNonInclusionProof> for OcsNonInclusionProof {
252    fn from(value: SdkOcsNonInclusionProof) -> Self {
253        (&value).into()
254    }
255}
256
257impl TryFrom<&OcsNonInclusionProof> for SdkOcsNonInclusionProof {
258    type Error = TryFromProtoError;
259
260    fn try_from(value: &OcsNonInclusionProof) -> Result<Self, Self::Error> {
261        let inner_proto = value.non_inclusion_proof.as_ref().ok_or_else(|| {
262            TryFromProtoError::missing(OcsNonInclusionProof::NON_INCLUSION_PROOF_FIELD.name)
263        })?;
264        let non_inclusion_proof =
265            SdkMerkleNonInclusionProof::<ObjectReference>::try_from(inner_proto)
266                .map_err(|e| e.nested(OcsNonInclusionProof::NON_INCLUSION_PROOF_FIELD.name))?;
267        let tree_root_bytes = value.tree_root.as_ref().ok_or_else(|| {
268            TryFromProtoError::missing(OcsNonInclusionProof::TREE_ROOT_FIELD.name)
269        })?;
270        let tree_root =
271            try_digest_from_bytes(tree_root_bytes, OcsNonInclusionProof::TREE_ROOT_FIELD.name)?;
272        Ok(SdkOcsNonInclusionProof {
273            non_inclusion_proof,
274            tree_root,
275        })
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    use prost::bytes::Bytes;
284    use sui_sdk_types::merkle::MerkleTree;
285
286    /// A round-trip through the proto types preserves the proof's path
287    /// node-by-node and the recovered proof verifies against the same tree
288    /// root.
289    #[test]
290    fn merkle_proof_round_trip_via_proto() {
291        const LEAVES: [&[u8]; 9] = [
292            b"foo", b"bar", b"fizz", b"baz", b"buzz", b"fizz", b"foobar", b"walrus", b"fizz",
293        ];
294
295        let tree = MerkleTree::build_from_serialized(LEAVES);
296        for (index, leaf) in LEAVES.iter().enumerate() {
297            let original = tree.get_proof(index).unwrap();
298
299            let proto: MerkleProof = (&original).into();
300            let round_tripped: sui_sdk_types::merkle::MerkleProof = (&proto).try_into().unwrap();
301
302            assert_eq!(round_tripped, original);
303            round_tripped
304                .verify_proof_with_leaf_bytes(&tree.root(), leaf, index)
305                .unwrap();
306        }
307    }
308
309    #[test]
310    fn merkle_node_empty_round_trip() {
311        let proto: MerkleNode = Node::Empty.into();
312        assert!(matches!(proto.node, Some(merkle_node::Node::Empty(_))));
313        let back: Node = (&proto).try_into().unwrap();
314        assert_eq!(back, Node::Empty);
315    }
316
317    #[test]
318    fn merkle_node_digest_round_trip() {
319        let raw = [0xab; DIGEST_LEN];
320        let proto: MerkleNode = Node::Digest(raw).into();
321        match &proto.node {
322            Some(merkle_node::Node::Digest(bytes)) => assert_eq!(bytes.as_ref(), raw),
323            other => panic!("expected Digest variant, got {other:?}"),
324        }
325        let back: Node = (&proto).try_into().unwrap();
326        assert_eq!(back, Node::Digest(raw));
327    }
328
329    #[test]
330    fn merkle_node_missing_oneof_rejected() {
331        let proto = MerkleNode { node: None };
332        let err = Node::try_from(&proto).unwrap_err();
333        assert_eq!(err.field_violation().field, "node");
334    }
335
336    #[test]
337    fn merkle_node_short_digest_rejected() {
338        let proto = MerkleNode {
339            node: Some(merkle_node::Node::Digest(Bytes::from_static(&[0u8; 16]))),
340        };
341        let err = Node::try_from(&proto).unwrap_err();
342        assert_eq!(err.field_violation().field, "digest");
343        assert!(
344            err.to_string().contains("expected 32 bytes"),
345            "error should mention expected length: {err}"
346        );
347    }
348
349    #[test]
350    fn merkle_node_long_digest_rejected() {
351        let proto = MerkleNode {
352            node: Some(merkle_node::Node::Digest(Bytes::from_static(&[0u8; 64]))),
353        };
354        let err = Node::try_from(&proto).unwrap_err();
355        assert_eq!(err.field_violation().field, "digest");
356    }
357
358    /// A malformed inner node's field path includes its position in the
359    /// outer `path` list so the failure points at the bad index.
360    #[test]
361    fn malformed_inner_node_reports_path_index() {
362        let mut tree_proof: MerkleProof = (&MerkleTree::build_from_serialized([b"a", b"b"])
363            .get_proof(1)
364            .unwrap())
365            .into();
366        // Corrupt the second sibling by stripping the oneof.
367        tree_proof.path[0].node = None;
368
369        let err = sui_sdk_types::merkle::MerkleProof::try_from(&tree_proof).unwrap_err();
370        let field = &err.field_violation().field;
371        assert!(
372            field.contains("path[0]") || field.contains("path.0"),
373            "field path should reference path index, got {field}"
374        );
375    }
376
377    /// Build a small set of `ObjectReference` leaves sorted by id; used
378    /// by the inclusion / non-inclusion round-trip tests.
379    fn synthetic_refs(count: u8) -> Vec<ObjectReference> {
380        (0..count)
381            .map(|i| {
382                let mut addr = [0u8; 32];
383                addr[31] = i;
384                let mut digest = [0u8; 32];
385                digest[0] = i ^ 0x42;
386                ObjectReference::new(
387                    sui_sdk_types::Address::new(addr),
388                    u64::from(i) + 1,
389                    Digest::new(digest),
390                )
391            })
392            .collect()
393    }
394
395    /// `OcsInclusionProof` round-trips through its proto representation:
396    /// the recovered proof equals the original (modulo the wire-only
397    /// `object_ref` and `object_data` slots that this conversion does
398    /// not own — see the `OcsInclusionProof::from` doc).
399    #[test]
400    fn ocs_inclusion_proof_round_trip_via_proto() {
401        let refs = synthetic_refs(5);
402        let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
403        let tree_root = Digest::new(tree.root().bytes());
404
405        for (index, _) in refs.iter().enumerate() {
406            let original = SdkOcsInclusionProof {
407                merkle_proof: tree.get_proof(index).unwrap(),
408                leaf_index: index as u64,
409                tree_root,
410            };
411            let proto: OcsInclusionProof = (&original).into();
412            let round_tripped: SdkOcsInclusionProof = (&proto).try_into().unwrap();
413            assert_eq!(round_tripped, original);
414        }
415    }
416
417    /// `OcsNonInclusionProof` round-trips through its proto
418    /// representation across the three structural shapes the
419    /// underlying `MerkleNonInclusionProof` admits: both neighbours
420    /// present (interior target), no left neighbour (target sorts
421    /// before all leaves), and no right neighbour (target sorts after
422    /// all leaves).
423    #[test]
424    fn ocs_non_inclusion_proof_round_trip_via_proto() {
425        let refs = synthetic_refs(5);
426        let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
427        let tree_root = Digest::new(tree.root().bytes());
428
429        // Interior: a synthetic ref whose id sorts between two leaves.
430        let interior = {
431            let mut addr = [0u8; 32];
432            addr[31] = 0x02;
433            ObjectReference::new(
434                sui_sdk_types::Address::new(addr),
435                10,
436                Digest::new([0xaa; 32]),
437            )
438        };
439        // Before everything.
440        let before = ObjectReference::new(
441            sui_sdk_types::Address::new([0u8; 32]),
442            0,
443            Digest::new([0u8; 32]),
444        );
445        // After everything.
446        let after = {
447            let mut addr = [0u8; 32];
448            addr[31] = 0xff;
449            ObjectReference::new(
450                sui_sdk_types::Address::new(addr),
451                999,
452                Digest::new([0xff; 32]),
453            )
454        };
455
456        for target in [interior, before, after] {
457            assert!(!refs.contains(&target));
458            let inner = tree.compute_non_inclusion_proof(&refs, &target).unwrap();
459            let original = SdkOcsNonInclusionProof {
460                non_inclusion_proof: inner,
461                tree_root,
462            };
463            let proto: OcsNonInclusionProof = (&original).into();
464            let round_tripped: SdkOcsNonInclusionProof = (&proto).try_into().unwrap();
465            assert_eq!(round_tripped, original);
466        }
467    }
468
469    /// A `MerkleNeighbourLeaf` missing its `leaf` field is rejected with
470    /// a field-path-attributed error.
471    #[test]
472    fn merkle_neighbour_leaf_missing_leaf_rejected() {
473        let proto = MerkleNeighbourLeaf {
474            leaf: None,
475            merkle_proof: Some(MerkleProof::default()),
476        };
477        let err =
478            <(ObjectReference, sui_sdk_types::merkle::MerkleProof)>::try_from(&proto).unwrap_err();
479        assert_eq!(
480            err.field_violation().field,
481            MerkleNeighbourLeaf::LEAF_FIELD.name
482        );
483    }
484
485    /// A `MerkleNeighbourLeaf` missing its `merkle_proof` field is
486    /// rejected with a field-path-attributed error.
487    #[test]
488    fn merkle_neighbour_leaf_missing_merkle_proof_rejected() {
489        let leaf_ref = synthetic_refs(1).pop().unwrap();
490        let proto = MerkleNeighbourLeaf {
491            leaf: Some(leaf_ref.into()),
492            merkle_proof: None,
493        };
494        let err =
495            <(ObjectReference, sui_sdk_types::merkle::MerkleProof)>::try_from(&proto).unwrap_err();
496        assert_eq!(
497            err.field_violation().field,
498            MerkleNeighbourLeaf::MERKLE_PROOF_FIELD.name,
499        );
500    }
501
502    /// A `MerkleNonInclusionProof` missing its `index` is rejected.
503    #[test]
504    fn merkle_non_inclusion_proof_missing_index_rejected() {
505        let proto = MerkleNonInclusionProof::default();
506        let err = SdkMerkleNonInclusionProof::<ObjectReference>::try_from(&proto).unwrap_err();
507        assert_eq!(
508            err.field_violation().field,
509            MerkleNonInclusionProof::INDEX_FIELD.name,
510        );
511    }
512}