1use std::sync::Arc;
38
39use move_core_types::language_storage::TypeTag;
40use thiserror::Error;
41
42use crate::{
43 accumulator_root::{AccumulatorKey, AccumulatorValue},
44 base_types::{ObjectID, ObjectRef, SequenceNumber, SuiAddress},
45 committee::EpochId,
46 digests::{ChainIdentifier, ObjectDigest},
47 error::{UserInputError, UserInputResult},
48 storage::RuntimeObjectResolver,
49 transaction::FundsWithdrawalArg,
50};
51
52macro_rules! invalid_res_error {
53 ($($args:tt)*) => {
54 UserInputError::InvalidWithdrawReservation {
55 error: format!($($args)*),
56 }
57 };
58}
59
60pub trait CoinReservationResolverTrait {
62 fn resolve_funds_withdrawal(
65 &self,
66 sender: SuiAddress,
68 coin_reservation: ParsedObjectRefWithdrawal,
69 accumulator_version: Option<SequenceNumber>,
72 ) -> UserInputResult<FundsWithdrawalArg>;
73}
74
75pub const COIN_RESERVATION_MAGIC: [u8; 20] = [
76 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac,
77 0xac, 0xac, 0xac, 0xac,
78];
79
80#[derive(Clone, Copy, PartialEq, Eq, Debug)]
81pub struct ParsedDigest {
82 epoch_id: u32,
83 reservation_amount: u64,
84}
85
86impl ParsedDigest {
87 pub fn epoch_id(&self) -> EpochId {
88 self.epoch_id as EpochId
89 }
90
91 pub fn reservation_amount(&self) -> u64 {
92 self.reservation_amount
93 }
94
95 pub fn is_coin_reservation_digest(digest: &ObjectDigest) -> bool {
96 let inner = digest.inner();
97 let last_20_bytes: &[u8; 20] = inner[12..32].try_into().unwrap();
99 *last_20_bytes == COIN_RESERVATION_MAGIC
100 }
101}
102
103#[derive(Debug, Error)]
104#[error("Invalid digest")]
105pub struct ParsedDigestError;
106
107impl TryFrom<ObjectDigest> for ParsedDigest {
108 type Error = ParsedDigestError;
109
110 fn try_from(digest: ObjectDigest) -> Result<Self, Self::Error> {
111 if ParsedDigest::is_coin_reservation_digest(&digest) {
112 let inner = digest.inner();
113 let reservation_amount_bytes: &[u8; 8] = inner[0..8].try_into().unwrap();
114 let epoch_bytes: &[u8; 4] = inner[8..12].try_into().unwrap();
115
116 let epoch_id = u32::from_le_bytes(*epoch_bytes);
117 let reservation_amount = u64::from_le_bytes(*reservation_amount_bytes);
118
119 Ok(Self {
120 epoch_id,
121 reservation_amount,
122 })
123 } else {
124 Err(ParsedDigestError)
125 }
126 }
127}
128
129impl From<ParsedDigest> for ObjectDigest {
130 fn from(parsed: ParsedDigest) -> Self {
131 let mut inner = [0; 32];
132 inner[0..8].copy_from_slice(&parsed.reservation_amount.to_le_bytes());
133 inner[8..12].copy_from_slice(&parsed.epoch_id.to_le_bytes());
134 inner[12..32].copy_from_slice(&COIN_RESERVATION_MAGIC);
135 ObjectDigest::new(inner)
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct ParsedObjectRefWithdrawal {
141 pub unmasked_object_id: ObjectID,
142 pub parsed_digest: ParsedDigest,
143}
144
145impl ParsedObjectRefWithdrawal {
146 pub fn new(unmasked_object_id: ObjectID, epoch_id: EpochId, reservation_amount: u64) -> Self {
147 Self {
148 unmasked_object_id,
149 parsed_digest: ParsedDigest {
150 epoch_id: epoch_id.try_into().unwrap(),
151 reservation_amount,
152 },
153 }
154 }
155
156 pub fn reservation_amount(&self) -> u64 {
157 self.parsed_digest.reservation_amount()
158 }
159
160 pub fn epoch_id(&self) -> EpochId {
161 self.parsed_digest.epoch_id()
162 }
163
164 pub fn encode(&self, version: SequenceNumber, chain_identifier: ChainIdentifier) -> ObjectRef {
165 let digest = self.parsed_digest.into();
166 let masked_id = mask_or_unmask_id(self.unmasked_object_id, chain_identifier);
167 (masked_id, version, digest)
168 }
169
170 pub fn parse(object_ref: &ObjectRef, chain_identifier: ChainIdentifier) -> Option<Self> {
171 let (object_id, _version, digest) = object_ref;
172 let parsed_digest = ParsedDigest::try_from(*digest).ok()?;
173
174 let unmasked_object_id = mask_or_unmask_id(*object_id, chain_identifier);
175
176 Some(ParsedObjectRefWithdrawal {
177 unmasked_object_id,
178 parsed_digest,
179 })
180 }
181}
182
183pub fn mask_or_unmask_id(object_id: ObjectID, chain_identifier: ChainIdentifier) -> ObjectID {
184 let mask_bytes: &[u8; 32] = chain_identifier.as_bytes();
185
186 let object_id_bytes: [u8; 32] = object_id.into_bytes();
187 let mut masked_object_id_bytes = [0; 32];
188 for i in 0..32 {
189 masked_object_id_bytes[i] = object_id_bytes[i] ^ mask_bytes[i];
190 }
191 ObjectID::new(masked_object_id_bytes)
192}
193
194pub fn encode_object_ref(
198 unmasked_object_id: ObjectID,
199 version: SequenceNumber,
200 epoch: EpochId,
201 balance: u64,
202 chain_identifier: ChainIdentifier,
203) -> ObjectRef {
204 ParsedObjectRefWithdrawal::new(unmasked_object_id, epoch, balance)
205 .encode(version, chain_identifier)
206}
207
208fn get_owner_and_type_for_object_impl(
209 runtime_object_resolver: &dyn RuntimeObjectResolver,
210 object_id: ObjectID,
211 accumulator_version: Option<SequenceNumber>,
212) -> UserInputResult<Option<(SuiAddress, TypeTag)>> {
213 let Some(object) = AccumulatorValue::load_object_by_id(
214 runtime_object_resolver,
215 accumulator_version,
216 object_id,
217 )
218 .map_err(|e| invalid_res_error!("could not load coin reservation object id {}", e))?
219 else {
220 return Ok(None);
221 };
222
223 let move_object = object.data.try_as_move().unwrap();
224
225 let type_tag: TypeTag = move_object
226 .type_()
227 .balance_accumulator_field_type_maybe()
228 .ok_or_else(|| {
229 invalid_res_error!(
230 "coin reservation object id {} is not a balance accumulator field",
231 object_id
232 )
233 })?;
234
235 let (key, _): (AccumulatorKey, AccumulatorValue) = move_object
236 .try_into()
237 .map_err(|e| invalid_res_error!("could not load coin reservation object id {}", e))?;
238
239 Ok(Some((key.owner, type_tag)))
240}
241
242fn resolve_funds_withdrawal_impl(
243 runtime_object_resolver: &dyn RuntimeObjectResolver,
244 sender: SuiAddress,
245 coin_reservation: ParsedObjectRefWithdrawal,
246 accumulator_version: Option<SequenceNumber>,
247) -> UserInputResult<FundsWithdrawalArg> {
248 let (owner, type_tag) = get_owner_and_type_for_object_impl(
249 runtime_object_resolver,
250 coin_reservation.unmasked_object_id,
251 accumulator_version,
252 )?
253 .ok_or_else(|| {
254 invalid_res_error!(
255 "coin reservation object id {} not found",
256 coin_reservation.unmasked_object_id
257 )
258 })?;
259
260 if sender != owner {
261 return Err(invalid_res_error!(
262 "coin reservation object id {} is owned by {}, not sender {}",
263 coin_reservation.unmasked_object_id,
264 owner,
265 sender
266 ));
267 }
268
269 Ok(FundsWithdrawalArg::balance_from_sender(
270 coin_reservation.reservation_amount(),
271 type_tag,
272 ))
273}
274
275pub struct CoinReservationResolver {
278 runtime_object_resolver: Arc<dyn RuntimeObjectResolver + Send + Sync>,
279}
280
281impl CoinReservationResolver {
282 pub fn new(runtime_object_resolver: Arc<dyn RuntimeObjectResolver + Send + Sync>) -> Self {
283 Self {
284 runtime_object_resolver,
285 }
286 }
287
288 pub fn get_owner_and_type_for_object(
294 &self,
295 object_id: ObjectID,
296 accumulator_version: Option<SequenceNumber>,
297 ) -> UserInputResult<Option<(SuiAddress, TypeTag)>> {
298 get_owner_and_type_for_object_impl(
299 self.runtime_object_resolver.as_ref(),
300 object_id,
301 accumulator_version,
302 )
303 }
304
305 pub fn resolve_funds_withdrawal(
306 &self,
307 sender: SuiAddress,
308 coin_reservation: ParsedObjectRefWithdrawal,
309 accumulator_version: Option<SequenceNumber>,
310 ) -> UserInputResult<FundsWithdrawalArg> {
311 resolve_funds_withdrawal_impl(
312 self.runtime_object_resolver.as_ref(),
313 sender,
314 coin_reservation,
315 accumulator_version,
316 )
317 }
318}
319
320pub struct BorrowedCoinReservationResolver<'a> {
322 runtime_object_resolver: &'a dyn RuntimeObjectResolver,
323}
324
325impl<'a> BorrowedCoinReservationResolver<'a> {
326 pub fn new(runtime_object_resolver: &'a dyn RuntimeObjectResolver) -> Self {
328 Self {
329 runtime_object_resolver,
330 }
331 }
332}
333
334impl CoinReservationResolverTrait for BorrowedCoinReservationResolver<'_> {
335 fn resolve_funds_withdrawal(
336 &self,
337 sender: SuiAddress,
338 coin_reservation: ParsedObjectRefWithdrawal,
339 accumulator_version: Option<SequenceNumber>,
340 ) -> UserInputResult<FundsWithdrawalArg> {
341 resolve_funds_withdrawal_impl(
342 self.runtime_object_resolver,
343 sender,
344 coin_reservation,
345 accumulator_version,
346 )
347 }
348}
349
350impl CoinReservationResolverTrait for CoinReservationResolver {
351 fn resolve_funds_withdrawal(
352 &self,
353 sender: SuiAddress,
354 coin_reservation: ParsedObjectRefWithdrawal,
355 accumulator_version: Option<SequenceNumber>,
356 ) -> UserInputResult<FundsWithdrawalArg> {
357 CoinReservationResolver::resolve_funds_withdrawal(
358 self,
359 sender,
360 coin_reservation,
361 accumulator_version,
362 )
363 }
364}
365
366impl CoinReservationResolverTrait for &'_ CoinReservationResolver {
367 fn resolve_funds_withdrawal(
368 &self,
369 sender: SuiAddress,
370 coin_reservation: ParsedObjectRefWithdrawal,
371 accumulator_version: Option<SequenceNumber>,
372 ) -> UserInputResult<FundsWithdrawalArg> {
373 CoinReservationResolver::resolve_funds_withdrawal(
374 self,
375 sender,
376 coin_reservation,
377 accumulator_version,
378 )
379 }
380}
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn test_parse_normal_digest() {
387 let digest = ObjectDigest::new([0; 32]);
388 assert!(ParsedDigest::try_from(digest).is_err());
389 }
390
391 #[test]
392 fn test_is_coin_reservation_digest() {
393 let digest = ObjectDigest::random();
394 assert!(!ParsedDigest::is_coin_reservation_digest(&digest));
395
396 let digest = ParsedDigest {
397 epoch_id: 42,
398 reservation_amount: 1232348999,
399 }
400 .into();
401 assert!(ParsedDigest::is_coin_reservation_digest(&digest));
402 }
403
404 #[test]
405 fn test_encode_and_parse_digest() {
406 let parsed_digest = ParsedDigest {
407 epoch_id: 42,
408 reservation_amount: 1232348999,
409 };
410
411 let digest = ObjectDigest::from(parsed_digest);
412 assert_eq!(parsed_digest, ParsedDigest::try_from(digest).unwrap());
413 }
414
415 #[test]
416 fn test_parse_object_ref() {
417 let object_ref = (
418 ObjectID::new([0; 32]),
419 SequenceNumber::new(),
420 ObjectDigest::new([0; 32]),
421 );
422
423 assert!(
424 ParsedObjectRefWithdrawal::parse(&object_ref, ChainIdentifier::default()).is_none()
425 );
426 }
427
428 #[test]
429 fn test_borrowed_resolver_uses_runtime_object_resolver() {
430 let store = crate::in_memory_storage::InMemoryStorage::default();
431 let resolver = BorrowedCoinReservationResolver::new(&store);
432 let object_id = ObjectID::random();
433 let result = resolver.resolve_funds_withdrawal(
434 SuiAddress::random_for_testing_only(),
435 ParsedObjectRefWithdrawal::new(object_id, 0, 1),
436 None,
437 );
438
439 assert!(matches!(
440 result,
441 Err(UserInputError::InvalidWithdrawReservation { error })
442 if error == format!("coin reservation object id {object_id} not found")
443 ));
444 }
445
446 #[test]
447 fn test_parse_object_ref_with_valid_digest() {
448 let chain_id = ChainIdentifier::random();
449
450 let id = ObjectID::random();
451 let parsed_obj_ref = ParsedObjectRefWithdrawal {
452 unmasked_object_id: id,
453 parsed_digest: ParsedDigest {
454 epoch_id: 42,
455 reservation_amount: 1232348999,
456 },
457 };
458 let encoded_obj_ref = parsed_obj_ref.encode(SequenceNumber::new(), chain_id);
459
460 assert_ne!(encoded_obj_ref.0, id, "object id should be masked");
461
462 let parsed_obj_ref = ParsedObjectRefWithdrawal::parse(&encoded_obj_ref, chain_id).unwrap();
463 assert_eq!(parsed_obj_ref.unmasked_object_id, id);
464 assert_eq!(parsed_obj_ref.parsed_digest.epoch_id, 42);
465 assert_eq!(parsed_obj_ref.parsed_digest.reservation_amount, 1232348999);
466 }
467}