1use 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
47pub trait CoinReservationResolverTrait {
49 fn resolve_funds_withdrawal(
52 &self,
53 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 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}