sui_adapter_latest/static_programmable_transactions/typing/verify/
input_arguments.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5    execution_mode::ExecutionMode,
6    sp,
7    static_programmable_transactions::execution::context::{
8        PrimitiveArgumentLayout, bcs_argument_validate,
9    },
10    static_programmable_transactions::{
11        env::Env,
12        loading::ast::{ObjectMutability, Type},
13        typing::ast::{self as T, BytesConstraint, ObjectArg},
14    },
15};
16use indexmap::IndexSet;
17use sui_types::{
18    SUI_FRAMEWORK_ADDRESS,
19    base_types::{RESOLVED_ASCII_STR, RESOLVED_STD_OPTION, RESOLVED_UTF8_STR},
20    coin::{COIN_MODULE_NAME, SEND_FUNDS_FUNC_NAME},
21    error::{ExecutionError, SafeIndex, command_argument_error},
22    execution_status::{CommandArgumentError, ExecutionErrorKind},
23    id::RESOLVED_SUI_ID,
24    transfer::RESOLVED_RECEIVING_STRUCT,
25};
26
27struct ObjectUsage {
28    allow_by_value: bool,
29    allow_by_mut_ref: bool,
30}
31
32struct Context {
33    objects: Vec<ObjectUsage>,
34}
35
36impl Context {
37    fn new(txn: &T::Transaction) -> Self {
38        let objects = txn
39            .objects
40            .iter()
41            .map(|object_input| match &object_input.arg {
42                ObjectArg::ImmObject(_) => ObjectUsage {
43                    allow_by_value: false,
44                    allow_by_mut_ref: false,
45                },
46                ObjectArg::OwnedObject(_) => ObjectUsage {
47                    allow_by_value: true,
48                    allow_by_mut_ref: true,
49                },
50                ObjectArg::SharedObject { mutability, .. } => ObjectUsage {
51                    allow_by_value: match mutability {
52                        ObjectMutability::Mutable => true,
53                        ObjectMutability::Immutable => false,
54                        // NonExclusiveWrite can be taken by value, but unless it is re-shared
55                        // with no mutations, the transaction will abort.
56                        ObjectMutability::NonExclusiveWrite => true,
57                    },
58                    allow_by_mut_ref: match mutability {
59                        ObjectMutability::Mutable => true,
60                        ObjectMutability::Immutable => false,
61                        ObjectMutability::NonExclusiveWrite => true,
62                    },
63                },
64            })
65            .collect();
66        Self { objects }
67    }
68}
69
70/// Verifies two properties for input objects:
71/// 1. That the `Pure` inputs can be serialized to the type inferred and that the type is
72///    permissible
73///    - Can be relaxed under certain execution modes
74/// 2. That any `Object` arguments are used validly. This means mutable references are taken only
75///    on mutable objects. And that the gas coin is only taken by value in transfer objects or with
76///    `sui::coin::send_funds`.
77pub fn verify<Mode: ExecutionMode>(env: &Env, txn: &T::Transaction) -> Result<(), ExecutionError> {
78    let T::Transaction {
79        gas_payment: _,
80        bytes,
81        objects: _,
82        withdrawals: _,
83        pure,
84        receiving,
85        withdrawal_compatibility_conversions: _,
86        original_command_len: _,
87        commands,
88    } = txn;
89    for pure in pure {
90        check_pure_input::<Mode>(bytes, pure)?;
91    }
92    for receiving in receiving {
93        check_receiving_input(receiving)?;
94    }
95    let context = &mut Context::new(txn);
96    for c in commands {
97        command(env, context, c).map_err(|e| e.with_command_index(c.idx as usize))?;
98    }
99    Ok(())
100}
101
102//**************************************************************************************************
103// Pure bytes
104//**************************************************************************************************
105
106fn check_pure_input<Mode: ExecutionMode>(
107    bytes: &IndexSet<Vec<u8>>,
108    pure: &T::PureInput,
109) -> Result<(), ExecutionError> {
110    let T::PureInput {
111        original_input_index,
112        byte_index,
113        ty,
114        constraint,
115    } = pure;
116    let Some(bcs_bytes) = bytes.get_index(*byte_index) else {
117        invariant_violation!(
118            "Unbound byte index {} for pure input at index {}",
119            byte_index,
120            original_input_index.0
121        );
122    };
123    let BytesConstraint { command, argument } = constraint;
124    check_pure_bytes::<Mode>(*argument, bcs_bytes, ty)
125        .map_err(|e| e.with_command_index(*command as usize))
126}
127
128fn check_pure_bytes<Mode: ExecutionMode>(
129    command_arg_idx: u16,
130    bytes: &[u8],
131    constraint: &Type,
132) -> Result<(), ExecutionError> {
133    assert_invariant!(
134        !matches!(constraint, Type::Reference(_, _)),
135        "references should not be added as a constraint"
136    );
137    if Mode::allow_arbitrary_values() {
138        return Ok(());
139    }
140    let Some(layout) = primitive_serialization_layout(constraint)? else {
141        let msg = format!(
142            "Invalid usage of `Pure` argument for a non-primitive argument type at index {command_arg_idx}.",
143        );
144        return Err(ExecutionError::new_with_source(
145            ExecutionErrorKind::command_argument_error(
146                CommandArgumentError::InvalidUsageOfPureArg,
147                command_arg_idx,
148            ),
149            msg,
150        ));
151    };
152    bcs_argument_validate(bytes, command_arg_idx, layout)?;
153    Ok(())
154}
155
156fn primitive_serialization_layout(
157    param_ty: &Type,
158) -> Result<Option<PrimitiveArgumentLayout>, ExecutionError> {
159    Ok(match param_ty {
160        Type::Signer => return Ok(None),
161        Type::Reference(_, _) => {
162            invariant_violation!("references should not be added as a constraint")
163        }
164        Type::Bool => Some(PrimitiveArgumentLayout::Bool),
165        Type::U8 => Some(PrimitiveArgumentLayout::U8),
166        Type::U16 => Some(PrimitiveArgumentLayout::U16),
167        Type::U32 => Some(PrimitiveArgumentLayout::U32),
168        Type::U64 => Some(PrimitiveArgumentLayout::U64),
169        Type::U128 => Some(PrimitiveArgumentLayout::U128),
170        Type::U256 => Some(PrimitiveArgumentLayout::U256),
171        Type::Address => Some(PrimitiveArgumentLayout::Address),
172
173        Type::Vector(v) => {
174            let info_opt = primitive_serialization_layout(&v.element_type)?;
175            info_opt.map(|layout| PrimitiveArgumentLayout::Vector(Box::new(layout)))
176        }
177        Type::Datatype(dt) => {
178            let resolved = dt.qualified_ident();
179            // is option of a string
180            if resolved == RESOLVED_STD_OPTION && dt.type_arguments.len() == 1 {
181                let info_opt = primitive_serialization_layout(dt.type_arguments.first().unwrap())?;
182                info_opt.map(|layout| PrimitiveArgumentLayout::Option(Box::new(layout)))
183            } else if dt.type_arguments.is_empty() {
184                if resolved == RESOLVED_SUI_ID {
185                    Some(PrimitiveArgumentLayout::Address)
186                } else if resolved == RESOLVED_ASCII_STR {
187                    Some(PrimitiveArgumentLayout::Ascii)
188                } else if resolved == RESOLVED_UTF8_STR {
189                    Some(PrimitiveArgumentLayout::UTF8)
190                } else {
191                    None
192                }
193            } else {
194                None
195            }
196        }
197    })
198}
199
200fn check_receiving_input(receiving: &T::ReceivingInput) -> Result<(), ExecutionError> {
201    let T::ReceivingInput {
202        original_input_index: _,
203        object_ref: _,
204        ty,
205        constraint,
206    } = receiving;
207    let BytesConstraint { command, argument } = constraint;
208    check_receiving(*argument, ty).map_err(|e| e.with_command_index(*command as usize))
209}
210
211fn check_receiving(command_arg_idx: u16, constraint: &Type) -> Result<(), ExecutionError> {
212    if is_valid_receiving(constraint) {
213        Ok(())
214    } else {
215        Err(command_argument_error(
216            CommandArgumentError::TypeMismatch,
217            command_arg_idx as usize,
218        ))
219    }
220}
221
222pub fn is_valid_pure_type(constraint: &Type) -> Result<bool, ExecutionError> {
223    Ok(primitive_serialization_layout(constraint)?.is_some())
224}
225
226/// Returns true if a type is a `Receiving<t>` where `t` has `key`
227pub fn is_valid_receiving(constraint: &Type) -> bool {
228    let Type::Datatype(dt) = constraint else {
229        return false;
230    };
231    dt.qualified_ident() == RESOLVED_RECEIVING_STRUCT
232        && dt.type_arguments.len() == 1
233        && dt.type_arguments.first().unwrap().abilities().has_key()
234}
235
236//**************************************************************************************************
237// Object usage
238//**************************************************************************************************
239
240fn command(env: &Env, context: &mut Context, sp!(_, c): &T::Command) -> Result<(), ExecutionError> {
241    match &c.command {
242        T::Command__::MoveCall(mc) => {
243            check_obj_usages(context, &mc.arguments)?;
244            if !(env.protocol_config.enable_accumulators() && is_coin_send_funds(&mc.function)) {
245                // We allow the gas coin to be used with `sui::coin::send_funds`
246                check_gas_by_values(&mc.arguments)?;
247            }
248        }
249        T::Command__::TransferObjects(objects, recipient) => {
250            check_obj_usages(context, objects)?;
251            check_obj_usage(context, recipient)?;
252            // gas can be used by value in TransferObjects
253        }
254        T::Command__::SplitCoins(_, coin, amounts) => {
255            check_obj_usage(context, coin)?;
256            check_obj_usages(context, amounts)?;
257            check_gas_by_value(coin)?;
258            check_gas_by_values(amounts)?;
259        }
260        T::Command__::MergeCoins(_, target, coins) => {
261            check_obj_usage(context, target)?;
262            check_obj_usages(context, coins)?;
263            check_gas_by_value(target)?;
264            check_gas_by_values(coins)?;
265        }
266        T::Command__::MakeMoveVec(_, xs) => {
267            check_obj_usages(context, xs)?;
268            check_gas_by_values(xs)?;
269        }
270        T::Command__::Publish(_, _, _) => (),
271        T::Command__::Upgrade(_, _, _, x, _) => {
272            check_obj_usage(context, x)?;
273            check_gas_by_value(x)?;
274        }
275    }
276    Ok(())
277}
278
279// Checks for valid by-mut-ref and by-value usage of input objects
280fn check_obj_usages(
281    context: &mut Context,
282    arguments: &[T::Argument],
283) -> Result<(), ExecutionError> {
284    for arg in arguments {
285        check_obj_usage(context, arg)?;
286    }
287    Ok(())
288}
289
290fn check_obj_usage(context: &mut Context, arg: &T::Argument) -> Result<(), ExecutionError> {
291    match &arg.value.0 {
292        T::Argument__::Borrow(true, l) => check_obj_by_mut_ref(context, arg.idx, l),
293        T::Argument__::Use(T::Usage::Move(l)) => check_by_value(context, arg.idx, l),
294        // We do not care about
295        // - immutable object borrowing
296        // - copying/read ref (since you cannot copy objects)
297        // - freeze (since an input object cannot be a reference without a borrow)
298        T::Argument__::Borrow(false, _)
299        | T::Argument__::Use(T::Usage::Copy { .. })
300        | T::Argument__::Read(_)
301        | T::Argument__::Freeze(_) => Ok(()),
302    }
303}
304
305// Checks for valid by-mut-ref usage of input objects
306fn check_obj_by_mut_ref(
307    context: &mut Context,
308    arg_idx: u16,
309    location: &T::Location,
310) -> Result<(), ExecutionError> {
311    match location {
312        T::Location::WithdrawalInput(_)
313        | T::Location::PureInput(_)
314        | T::Location::ReceivingInput(_)
315        | T::Location::TxContext
316        | T::Location::GasCoin
317        | T::Location::Result(_, _) => Ok(()),
318        T::Location::ObjectInput(idx) => {
319            if !context.objects.safe_get(*idx as usize)?.allow_by_mut_ref {
320                Err(command_argument_error(
321                    CommandArgumentError::InvalidObjectByMutRef,
322                    arg_idx as usize,
323                ))
324            } else {
325                Ok(())
326            }
327        }
328    }
329}
330
331// Checks for valid by-value usage of input objects
332fn check_by_value(
333    context: &mut Context,
334    arg_idx: u16,
335    location: &T::Location,
336) -> Result<(), ExecutionError> {
337    match location {
338        T::Location::GasCoin
339        | T::Location::Result(_, _)
340        | T::Location::TxContext
341        | T::Location::WithdrawalInput(_)
342        | T::Location::PureInput(_)
343        | T::Location::ReceivingInput(_) => Ok(()),
344        T::Location::ObjectInput(idx) => {
345            if !context.objects.safe_get(*idx as usize)?.allow_by_value {
346                Err(command_argument_error(
347                    CommandArgumentError::InvalidObjectByValue,
348                    arg_idx as usize,
349                ))
350            } else {
351                Ok(())
352            }
353        }
354    }
355}
356
357// Checks for no by value usage of gas
358fn check_gas_by_values(arguments: &[T::Argument]) -> Result<(), ExecutionError> {
359    for arg in arguments {
360        check_gas_by_value(arg)?;
361    }
362    Ok(())
363}
364
365fn check_gas_by_value(arg: &T::Argument) -> Result<(), ExecutionError> {
366    match &arg.value.0 {
367        T::Argument__::Use(T::Usage::Move(l)) => check_gas_by_value_loc(arg.idx, l),
368        // We do not care about the read/freeze case since they cannot move an object input
369        T::Argument__::Borrow(_, _)
370        | T::Argument__::Use(T::Usage::Copy { .. })
371        | T::Argument__::Read(_)
372        | T::Argument__::Freeze(_) => Ok(()),
373    }
374}
375
376fn check_gas_by_value_loc(idx: u16, location: &T::Location) -> Result<(), ExecutionError> {
377    match location {
378        T::Location::GasCoin => Err(command_argument_error(
379            CommandArgumentError::InvalidGasCoinUsage,
380            idx as usize,
381        )),
382        T::Location::TxContext
383        | T::Location::ObjectInput(_)
384        | T::Location::WithdrawalInput(_)
385        | T::Location::PureInput(_)
386        | T::Location::ReceivingInput(_)
387        | T::Location::Result(_, _) => Ok(()),
388    }
389}
390
391pub fn is_coin_send_funds(function: &T::LoadedFunction) -> bool {
392    function.original_mid.address() == &SUI_FRAMEWORK_ADDRESS
393        && function.original_mid.name() == COIN_MODULE_NAME
394        && function.name.as_ident_str() == SEND_FUNDS_FUNC_NAME
395}