Skip to main content

sui_adapter_latest/static_programmable_transactions/typing/verify/
drop_safety.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5    execution_mode::ExecutionMode,
6    static_programmable_transactions::{env::Env, typing::ast as T},
7};
8
9/// Refines usage of values so that the last `Copy` of a value is a `Move` if it is not borrowed
10/// After, it verifies the following
11/// - No results without `drop` are unused (all unused non-input values have `drop`)
12pub fn refine_and_verify<Mode: ExecutionMode>(
13    env: &Env<Mode>,
14    ast: &mut T::Transaction,
15) -> Result<(), Mode::Error> {
16    refine::transaction(env, ast)?;
17    verify::transaction::<Mode>(env, ast)?;
18    Ok(())
19}
20
21mod refine {
22    use crate::execution_mode::ExecutionMode;
23    use sui_types::coin::{COIN_MODULE_NAME, SEND_FUNDS_FUNC_NAME};
24
25    use crate::{
26        sp,
27        static_programmable_transactions::{
28            env::Env,
29            spanned::sp,
30            typing::{
31                ast::{self as T, Type},
32                translate::coin_inner_type,
33            },
34        },
35    };
36    use std::collections::BTreeSet;
37
38    struct Context {
39        // All locations that were used
40        used: BTreeSet<T::Location>,
41        // All locations that were used via a Move. This is a subset of `used`, but `used` does
42        // not track how each value was used in each instance. For simplicity, this is kept as a
43        // separate set.
44        moved: BTreeSet<T::Location>,
45    }
46
47    impl Context {
48        fn new() -> Self {
49            Self {
50                used: BTreeSet::new(),
51                moved: BTreeSet::new(),
52            }
53        }
54    }
55
56    /// After memory safety, we can switch the last usage of a `Copy` to a `Move` if it is not
57    /// borrowed at the time of the last usage.
58    pub fn transaction<Mode: ExecutionMode>(
59        env: &Env<Mode>,
60        ast: &mut T::Transaction,
61    ) -> Result<(), Mode::Error> {
62        let mut context = Context::new();
63        for c in ast.commands.iter_mut().rev() {
64            command(&mut context, c);
65        }
66        return_unused_withdrawal_conversions(env, ast, &context.moved)
67    }
68
69    fn command(context: &mut Context, sp!(_, c): &mut T::Command) {
70        match &mut c.command {
71            T::Command__::MoveCall(mc) => arguments(context, &mut mc.arguments),
72            T::Command__::TransferObjects(objects, recipient) => {
73                argument(context, recipient);
74                arguments(context, objects);
75            }
76            T::Command__::SplitCoins(_, coin, amounts) => {
77                arguments(context, amounts);
78                argument(context, coin);
79            }
80            T::Command__::MergeCoins(_, target, coins) => {
81                arguments(context, coins);
82                argument(context, target);
83            }
84            T::Command__::MakeMoveVec(_, xs) => arguments(context, xs),
85            T::Command__::Publish(_, _, _) => (),
86            T::Command__::Upgrade(_, _, _, x, _) => argument(context, x),
87        }
88    }
89
90    fn arguments(context: &mut Context, args: &mut [T::Argument]) {
91        for arg in args.iter_mut().rev() {
92            argument(context, arg)
93        }
94    }
95
96    fn argument(context: &mut Context, arg: &mut T::Argument) {
97        let usage = match &mut arg.value.0 {
98            T::Argument__::Use(u) | T::Argument__::Read(u) | T::Argument__::Freeze(u) => u,
99            T::Argument__::Borrow(_, loc) => {
100                // mark location as used
101                context.used.insert(*loc);
102                return;
103            }
104        };
105        match &usage {
106            T::Usage::Move(loc) => {
107                // mark location as used
108                context.used.insert(*loc);
109                context.moved.insert(*loc);
110            }
111            T::Usage::Copy { location, borrowed } => {
112                // we are at the last usage of a reference result if it was not yet added to the set
113                let location = *location;
114                let last_usage = context.used.insert(location);
115                if last_usage && !borrowed.get().unwrap() {
116                    // if it was the last usage, we need to change the Copy to a Move
117                    *usage = T::Usage::Move(location);
118                    context.moved.insert(location);
119                }
120            }
121        }
122    }
123
124    /// For any withdrawal conversion where the value was not moved, send it back to the original
125    /// owner
126    fn return_unused_withdrawal_conversions<Mode: ExecutionMode>(
127        env: &Env<Mode>,
128        ast: &mut T::Transaction,
129        moved_locations: &BTreeSet<T::Location>,
130    ) -> Result<(), Mode::Error> {
131        // withdrawal conversions not empty ==> accumulators enabled
132        assert_invariant!(
133            ast.withdrawal_compatibility_conversions.is_empty()
134                || env.protocol_config.enable_accumulators(),
135            "Withdrawal conversions should be empty if accumulators are not enabled"
136        );
137        for conversion_info in
138            ast.withdrawal_compatibility_conversions
139                .values()
140                .filter(|conversion| {
141                    // The conversion result is always a single return value, as such we can specify
142                    // a secondary index of `0` without worrying about other results.
143                    let conversion_location = T::Location::Result(conversion.conversion_result, 0);
144                    !moved_locations.contains(&conversion_location)
145                })
146        {
147            let Some(cur_command) = ast.commands.len().checked_sub(1) else {
148                invariant_violation!("cannot be zero commands with a conversion")
149            };
150            let cur_command = checked_as!(cur_command, u16)?;
151            let T::WithdrawalCompatibilityConversion {
152                owner,
153                conversion_result,
154            } = *conversion_info;
155            let Some(conversion_command) = ast.commands.get(conversion_result as usize) else {
156                invariant_violation!("conversion result should be a valid command index")
157            };
158            assert_invariant!(
159                conversion_command.value.result_type.len() == 1,
160                "conversion should have one result"
161            );
162            let T::Location::PureInput(owner_pure_idx) = owner else {
163                invariant_violation!("owner should be a pure input")
164            };
165            assert_invariant!(
166                ast.pure.len() > owner_pure_idx as usize,
167                "owner pure input index out of bounds"
168            );
169            assert_invariant!(
170                ast.pure.get(owner_pure_idx as usize).unwrap().ty == T::Type::Address,
171                "owner pure input should be an address"
172            );
173            let Some(conversion_ty) = conversion_command.value.result_type.first() else {
174                invariant_violation!("conversion should have a result type")
175            };
176            let Some(inner_ty) = coin_inner_type(conversion_ty) else {
177                invariant_violation!("conversion result should be a coin type")
178            };
179            let move_result_ = T::Argument__::new_move(T::Location::Result(conversion_result, 0));
180            let move_result = sp(cur_command, (move_result_, conversion_ty.clone()));
181            let owner_ty = Type::Address;
182            let owner_arg_ = T::Argument__::new_move(owner);
183            let owner_arg = sp(cur_command, (owner_arg_, owner_ty));
184            let return_command__ = T::Command__::MoveCall(Box::new(T::MoveCall {
185                function: env.load_framework_function(
186                    COIN_MODULE_NAME,
187                    SEND_FUNDS_FUNC_NAME,
188                    vec![inner_ty.clone()],
189                    ast.unified_linkage.as_ref(),
190                )?,
191                arguments: vec![move_result, owner_arg],
192            }));
193            let return_command = sp(
194                cur_command,
195                T::Command_ {
196                    command: return_command__,
197                    result_type: vec![],
198                    drop_values: vec![],
199                    incurs_post_execution_checks: false,
200                },
201            );
202            ast.commands.push(return_command);
203        }
204        Ok(())
205    }
206}
207
208mod verify {
209    use crate::{
210        execution_mode::ExecutionMode,
211        sp,
212        static_programmable_transactions::{
213            env::Env,
214            typing::ast::{self as T, Type},
215        },
216    };
217    use mysten_common::ZipDebugEqIteratorExt;
218    use sui_types::error::{ExecutionErrorTrait, SafeIndex};
219    use sui_types::execution_status::ExecutionErrorKind;
220
221    #[must_use]
222    struct Value;
223
224    struct Context {
225        tx_context: Option<Value>,
226        gas_coin: Option<Value>,
227        objects: Vec<Option<Value>>,
228        withdrawals: Vec<Option<Value>>,
229        pure: Vec<Option<Value>>,
230        receiving: Vec<Option<Value>>,
231        results: Vec<Vec<Option<Value>>>,
232    }
233
234    impl Context {
235        fn new<Mode: ExecutionMode>(_env: &Env<Mode>, ast: &T::Transaction) -> Self {
236            let objects = ast.objects.iter().map(|_| Some(Value)).collect::<Vec<_>>();
237            let withdrawals = ast
238                .withdrawals
239                .iter()
240                .map(|_| Some(Value))
241                .collect::<Vec<_>>();
242            let pure = ast.pure.iter().map(|_| Some(Value)).collect::<Vec<_>>();
243            let receiving = ast
244                .receiving
245                .iter()
246                .map(|_| Some(Value))
247                .collect::<Vec<_>>();
248            let gas_coin = if ast.gas_payment.is_none() {
249                None
250            } else {
251                Some(Value)
252            };
253            Self {
254                tx_context: Some(Value),
255                gas_coin,
256                objects,
257                withdrawals,
258                pure,
259                receiving,
260                results: Vec::with_capacity(ast.commands.len()),
261            }
262        }
263
264        fn location<E: ExecutionErrorTrait>(
265            &mut self,
266            l: T::Location,
267        ) -> Result<&mut Option<Value>, E> {
268            Ok(match l {
269                T::Location::TxContext => &mut self.tx_context,
270                T::Location::GasCoin => &mut self.gas_coin,
271                T::Location::ObjectInput(i) => self.objects.safe_get_mut(i as usize)?,
272                T::Location::WithdrawalInput(i) => self.withdrawals.safe_get_mut(i as usize)?,
273                T::Location::PureInput(i) => self.pure.safe_get_mut(i as usize)?,
274                T::Location::ReceivingInput(i) => self.receiving.safe_get_mut(i as usize)?,
275                T::Location::Result(i, j) => self
276                    .results
277                    .safe_get_mut(i as usize)?
278                    .safe_get_mut(j as usize)?,
279            })
280        }
281    }
282
283    /// Checks the following
284    /// - All unused result values have `drop`
285    pub fn transaction<Mode: ExecutionMode>(
286        env: &Env<Mode>,
287        ast: &T::Transaction,
288    ) -> Result<(), Mode::Error> {
289        let mut context = Context::new(env, ast);
290        let commands = &ast.commands;
291        for c in commands {
292            let result = command::<Mode::Error>(&mut context, c)
293                .map_err(|e| e.with_command_index(c.idx as usize))?;
294            assert_invariant!(
295                result.len() == c.value.result_type.len(),
296                "result length mismatch"
297            );
298            // drop unused result values
299            assert_invariant!(
300                result.len() == c.value.drop_values.len(),
301                "drop values length mismatch"
302            );
303            let result_values = result
304                .into_iter()
305                .zip_debug_eq(c.value.drop_values.iter().copied())
306                .map(|(v, drop)| {
307                    if !drop {
308                        Some(v)
309                    } else {
310                        consume_value(v);
311                        None
312                    }
313                })
314                .collect();
315            context.results.push(result_values);
316        }
317
318        let Context {
319            tx_context,
320            gas_coin,
321            objects,
322            withdrawals,
323            pure,
324            receiving,
325            results,
326        } = context;
327        consume_value_opt(gas_coin);
328        // TODO do we want to check inputs in the dev inspect case?
329        consume_value_opts(objects);
330        consume_value_opts(withdrawals);
331        consume_value_opts(pure);
332        consume_value_opts(receiving);
333        assert_invariant!(results.len() == commands.len(), "result length mismatch");
334        for (i, (result, c)) in results.into_iter().zip_debug_eq(&ast.commands).enumerate() {
335            let tys = &c.value.result_type;
336            assert_invariant!(result.len() == tys.len(), "result length mismatch");
337            for (j, (vopt, ty)) in result.into_iter().zip_debug_eq(tys).enumerate() {
338                drop_value_opt::<Mode>((i, j), vopt, ty)?;
339            }
340        }
341        assert_invariant!(tx_context.is_some(), "tx_context should never be moved");
342        Ok(())
343    }
344
345    fn command<E: ExecutionErrorTrait>(
346        context: &mut Context,
347        sp!(_, c): &T::Command,
348    ) -> Result<Vec<Value>, E> {
349        let result_tys = &c.result_type;
350        Ok(match &c.command {
351            T::Command__::MoveCall(mc) => {
352                let T::MoveCall {
353                    function,
354                    arguments: args,
355                } = &**mc;
356                let return_ = &function.signature.return_;
357                let arg_values = arguments(context, args)?;
358                consume_values(arg_values);
359                (0..return_.len()).map(|_| Value).collect()
360            }
361            T::Command__::TransferObjects(objects, recipient) => {
362                let object_values = arguments(context, objects)?;
363                let recipient_value = argument(context, recipient)?;
364                consume_values(object_values);
365                consume_value(recipient_value);
366                vec![]
367            }
368            T::Command__::SplitCoins(_, coin, amounts) => {
369                let coin_value = argument(context, coin)?;
370                let amount_values = arguments(context, amounts)?;
371                consume_values(amount_values);
372                consume_value(coin_value);
373                (0..amounts.len()).map(|_| Value).collect()
374            }
375            T::Command__::MergeCoins(_, target, coins) => {
376                let target_value = argument(context, target)?;
377                let coin_values = arguments(context, coins)?;
378                consume_values(coin_values);
379                consume_value(target_value);
380                vec![]
381            }
382            T::Command__::MakeMoveVec(_, xs) => {
383                let vs = arguments(context, xs)?;
384                consume_values(vs);
385                vec![Value]
386            }
387            T::Command__::Publish(_, _, _) => result_tys.iter().map(|_| Value).collect(),
388            T::Command__::Upgrade(_, _, _, x, _) => {
389                let v = argument(context, x)?;
390                consume_value(v);
391                vec![Value]
392            }
393        })
394    }
395
396    fn consume_values(_: Vec<Value>) {}
397
398    fn consume_value(_: Value) {}
399
400    fn consume_value_opts(_: Vec<Option<Value>>) {}
401
402    fn consume_value_opt(_: Option<Value>) {}
403
404    fn drop_value_opt<Mode: ExecutionMode>(
405        idx: (usize, usize),
406        value: Option<Value>,
407        ty: &Type,
408    ) -> Result<(), Mode::Error> {
409        match value {
410            Some(v) => drop_value::<Mode>(idx, v, ty),
411            None => Ok(()),
412        }
413    }
414
415    fn drop_value<Mode: ExecutionMode>(
416        (i, j): (usize, usize),
417        value: Value,
418        ty: &Type,
419    ) -> Result<(), Mode::Error> {
420        let abilities = ty.abilities();
421        if !abilities.has_drop() && !Mode::allow_arbitrary_values() {
422            let msg = if abilities.has_copy() {
423                "The value has copy, but not drop. \
424                Its last usage must be by-value so it can be taken."
425            } else {
426                "Unused value without drop"
427            };
428            return Err(Mode::Error::new_with_source(
429                ExecutionErrorKind::UnusedValueWithoutDrop {
430                    result_idx: checked_as!(i, u16)?,
431                    secondary_idx: checked_as!(j, u16)?,
432                },
433                msg,
434            ));
435        }
436        consume_value(value);
437        Ok(())
438    }
439
440    fn arguments<E: ExecutionErrorTrait>(
441        context: &mut Context,
442        xs: &[T::Argument],
443    ) -> Result<Vec<Value>, E> {
444        xs.iter().map(|x| argument(context, x)).collect()
445    }
446
447    fn argument<E: ExecutionErrorTrait>(
448        context: &mut Context,
449        sp!(_, x): &T::Argument,
450    ) -> Result<Value, E> {
451        match &x.0 {
452            T::Argument__::Use(T::Usage::Move(location)) => move_value(context, *location),
453            T::Argument__::Use(T::Usage::Copy { location, .. }) => copy_value(context, *location),
454            T::Argument__::Borrow(_, location) => borrow_location(context, *location),
455            T::Argument__::Read(usage) => read_ref(context, usage),
456            T::Argument__::Freeze(usage) => freeze_ref(context, usage),
457        }
458    }
459
460    fn move_value<E: ExecutionErrorTrait>(
461        context: &mut Context,
462        l: T::Location,
463    ) -> Result<Value, E> {
464        let Some(value) = context.location::<E>(l)?.take() else {
465            invariant_violation!("memory safety should have failed")
466        };
467        Ok(value)
468    }
469
470    fn copy_value<E: ExecutionErrorTrait>(
471        context: &mut Context,
472        l: T::Location,
473    ) -> Result<Value, E> {
474        assert_invariant!(
475            context.location::<E>(l)?.is_some(),
476            "memory safety should have failed"
477        );
478        Ok(Value)
479    }
480
481    fn borrow_location<E: ExecutionErrorTrait>(
482        context: &mut Context,
483        l: T::Location,
484    ) -> Result<Value, E> {
485        assert_invariant!(
486            context.location::<E>(l)?.is_some(),
487            "memory safety should have failed"
488        );
489        Ok(Value)
490    }
491
492    fn read_ref<E: ExecutionErrorTrait>(context: &mut Context, u: &T::Usage) -> Result<Value, E> {
493        let value = match u {
494            T::Usage::Move(l) => move_value::<E>(context, *l)?,
495            T::Usage::Copy { location, .. } => copy_value::<E>(context, *location)?,
496        };
497        consume_value(value);
498        Ok(Value)
499    }
500
501    fn freeze_ref<E: ExecutionErrorTrait>(context: &mut Context, u: &T::Usage) -> Result<Value, E> {
502        let value = match u {
503            T::Usage::Move(l) => move_value::<E>(context, *l)?,
504            T::Usage::Copy { location, .. } => copy_value::<E>(context, *location)?,
505        };
506        consume_value(value);
507        Ok(Value)
508    }
509}