sui_types/
coin_reservation.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! This module defines the protocol for specifying an address balance reservation
5//! via an ObjectRef, in order to provide backward compatibility for clients that do
6//! not understand address balances.
7//!
8//! The layout of the reservation ObjectRef is as follows:
9//!
10//!    (ObjectID, SequenceNumber, ObjectDigest)
11//!
12//! The ObjectID points to an accumulator object (i.e. a dynamic field of the accumulator root object).
13//! This identifies both the owner and type (e.g. SUI, USDC, etc) of the balance being spent.
14//!
15//! It is masked by XORing with the current chain identifier (i.e. genesis checkpoint digest).
16//! This prevents cross-chain replay, as an attacker would have to mine an address and currency
17//! type such that `dynamic_field_key(address, type) = V` such that
18//! `V ^ FOREIGN_CHAIN_IDENTIFIER = TARGET_ACCUMULATOR_OBJECT_ID ^ TARGET_CHAIN_IDENTIFIER`
19//! and then trick the target into signing a transaction as V on the foreign chain.
20//!
21//! The masking also allows read APIs to positively identify attempts to read a "fake" object ID, as
22//! follows:
23//!   1. First, read the requested object ID.
24//!   2. If it does not exist, unmask the ID using the local chain identifier and read it again.
25//!   3. If it exists on the second attempt, the ID must have originated by masking an accumulator object ID.
26//!
27//! The SequenceNumber is a monotonically increasing version number, typically the version of the
28//! accumulator root object. It is not used by the protocol, but is intended to help the
29//! caching behavior of old clients.
30//!
31//! ObjectDigest contains the remainder of the payload:
32//!
33//! 1. The amount of the reservation [8 bytes]
34//! 2. The epoch(s) in which the tx is valid [4 bytes] (good enough for 12 million years of 24 hour epochs).
35//! 3. A magic number to identify this ObjectRef as a coin reservation [20 bytes].
36
37use thiserror::Error;
38
39use crate::{
40    base_types::{ObjectID, ObjectRef, SequenceNumber, SuiAddress},
41    committee::EpochId,
42    digests::{ChainIdentifier, ObjectDigest},
43    error::UserInputResult,
44    transaction::FundsWithdrawalArg,
45};
46
47/// Trait for resolving funds withdrawal from a coin reservation
48pub trait CoinReservationResolverTrait {
49    // Used to check validity of the transaction. If the coin_reservation does not
50    // point to an existing accumulator object, the transaction will be rejected.
51    fn resolve_funds_withdrawal(
52        &self,
53        // Note: must be the sender. We do not support sponsorship.
54        sender: SuiAddress,
55        coin_reservation: ParsedObjectRefWithdrawal,
56    ) -> UserInputResult<FundsWithdrawalArg>;
57}
58
59pub const COIN_RESERVATION_MAGIC: [u8; 20] = [
60    0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
61    0xac, 0xac, 0xac, 0xac,
62];
63
64#[derive(Clone, Copy, PartialEq, Eq, Debug)]
65pub struct ParsedDigest {
66    epoch_id: u32,
67    reservation_amount: u64,
68}
69
70impl ParsedDigest {
71    pub fn epoch_id(&self) -> EpochId {
72        self.epoch_id as EpochId
73    }
74
75    pub fn reservation_amount(&self) -> u64 {
76        self.reservation_amount
77    }
78
79    pub fn is_coin_reservation_digest(digest: &ObjectDigest) -> bool {
80        let inner = digest.inner();
81        // check if the last 20 bytes of digest match the magic number
82        let last_20_bytes: &[u8; 20] = inner[12..32].try_into().unwrap();
83        *last_20_bytes == COIN_RESERVATION_MAGIC
84    }
85}
86
87#[derive(Debug, Error)]
88#[error("Invalid digest")]
89pub struct ParsedDigestError;
90
91impl TryFrom<ObjectDigest> for ParsedDigest {
92    type Error = ParsedDigestError;
93
94    fn try_from(digest: ObjectDigest) -> Result<Self, Self::Error> {
95        if ParsedDigest::is_coin_reservation_digest(&digest) {
96            let inner = digest.inner();
97            let reservation_amount_bytes: &[u8; 8] = inner[0..8].try_into().unwrap();
98            let epoch_bytes: &[u8; 4] = inner[8..12].try_into().unwrap();
99
100            let epoch_id = u32::from_le_bytes(*epoch_bytes);
101            let reservation_amount = u64::from_le_bytes(*reservation_amount_bytes);
102
103            Ok(Self {
104                epoch_id,
105                reservation_amount,
106            })
107        } else {
108            Err(ParsedDigestError)
109        }
110    }
111}
112
113impl From<ParsedDigest> for ObjectDigest {
114    fn from(parsed: ParsedDigest) -> Self {
115        let mut inner = [0; 32];
116        inner[0..8].copy_from_slice(&parsed.reservation_amount.to_le_bytes());
117        inner[8..12].copy_from_slice(&parsed.epoch_id.to_le_bytes());
118        inner[12..32].copy_from_slice(&COIN_RESERVATION_MAGIC);
119        ObjectDigest::new(inner)
120    }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct ParsedObjectRefWithdrawal {
125    pub unmasked_object_id: ObjectID,
126    pub parsed_digest: ParsedDigest,
127}
128
129impl ParsedObjectRefWithdrawal {
130    pub fn new(unmasked_object_id: ObjectID, epoch_id: EpochId, reservation_amount: u64) -> Self {
131        Self {
132            unmasked_object_id,
133            parsed_digest: ParsedDigest {
134                epoch_id: epoch_id.try_into().unwrap(),
135                reservation_amount,
136            },
137        }
138    }
139
140    pub fn reservation_amount(&self) -> u64 {
141        self.parsed_digest.reservation_amount()
142    }
143
144    pub fn epoch_id(&self) -> EpochId {
145        self.parsed_digest.epoch_id()
146    }
147
148    pub fn encode(&self, version: SequenceNumber, chain_identifier: ChainIdentifier) -> ObjectRef {
149        let digest = self.parsed_digest.into();
150        let masked_id = mask_or_unmask_id(self.unmasked_object_id, chain_identifier);
151        (masked_id, version, digest)
152    }
153
154    pub fn parse(object_ref: &ObjectRef, chain_identifier: ChainIdentifier) -> Option<Self> {
155        let (object_id, _version, digest) = object_ref;
156        let parsed_digest = ParsedDigest::try_from(*digest).ok()?;
157
158        let unmasked_object_id = mask_or_unmask_id(*object_id, chain_identifier);
159
160        Some(ParsedObjectRefWithdrawal {
161            unmasked_object_id,
162            parsed_digest,
163        })
164    }
165}
166
167pub fn mask_or_unmask_id(object_id: ObjectID, chain_identifier: ChainIdentifier) -> ObjectID {
168    let mask_bytes: &[u8; 32] = chain_identifier.as_bytes();
169
170    let object_id_bytes: [u8; 32] = object_id.into_bytes();
171    let mut masked_object_id_bytes = [0; 32];
172    for i in 0..32 {
173        masked_object_id_bytes[i] = object_id_bytes[i] ^ mask_bytes[i];
174    }
175    ObjectID::new(masked_object_id_bytes)
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_parse_normal_digest() {
184        let digest = ObjectDigest::new([0; 32]);
185        assert!(ParsedDigest::try_from(digest).is_err());
186    }
187
188    #[test]
189    fn test_is_coin_reservation_digest() {
190        let digest = ObjectDigest::random();
191        assert!(!ParsedDigest::is_coin_reservation_digest(&digest));
192
193        let digest = ParsedDigest {
194            epoch_id: 42,
195            reservation_amount: 1232348999,
196        }
197        .into();
198        assert!(ParsedDigest::is_coin_reservation_digest(&digest));
199    }
200
201    #[test]
202    fn test_encode_and_parse_digest() {
203        let parsed_digest = ParsedDigest {
204            epoch_id: 42,
205            reservation_amount: 1232348999,
206        };
207
208        let digest = ObjectDigest::from(parsed_digest);
209        assert_eq!(parsed_digest, ParsedDigest::try_from(digest).unwrap());
210    }
211
212    #[test]
213    fn test_parse_object_ref() {
214        let object_ref = (
215            ObjectID::new([0; 32]),
216            SequenceNumber::new(),
217            ObjectDigest::new([0; 32]),
218        );
219
220        assert!(
221            ParsedObjectRefWithdrawal::parse(&object_ref, ChainIdentifier::default()).is_none()
222        );
223    }
224
225    #[test]
226    fn test_parse_object_ref_with_valid_digest() {
227        let chain_id = ChainIdentifier::random();
228
229        let id = ObjectID::random();
230        let parsed_obj_ref = ParsedObjectRefWithdrawal {
231            unmasked_object_id: id,
232            parsed_digest: ParsedDigest {
233                epoch_id: 42,
234                reservation_amount: 1232348999,
235            },
236        };
237        let encoded_obj_ref = parsed_obj_ref.encode(SequenceNumber::new(), chain_id);
238
239        assert_ne!(encoded_obj_ref.0, id, "object id should be masked");
240
241        let parsed_obj_ref = ParsedObjectRefWithdrawal::parse(&encoded_obj_ref, chain_id).unwrap();
242        assert_eq!(parsed_obj_ref.unmasked_object_id, id);
243        assert_eq!(parsed_obj_ref.parsed_digest.epoch_id, 42);
244        assert_eq!(parsed_obj_ref.parsed_digest.reservation_amount, 1232348999);
245    }
246}