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        commands,
87    } = txn;
88    for pure in pure {
89        check_pure_input::<Mode>(bytes, pure)?;
90    }
91    for receiving in receiving {
92        check_receiving_input(receiving)?;
93    }
94    let context = &mut Context::new(txn);
95    for c in commands {
96        command(env, context, c).map_err(|e| e.with_command_index(c.idx as usize))?;
97    }
98    Ok(())
99}
100
101//**************************************************************************************************
102// Pure bytes
103//**************************************************************************************************
104
105fn check_pure_input<Mode: ExecutionMode>(
106    bytes: &IndexSet<Vec<u8>>,
107    pure: &T::PureInput,
108) -> Result<(), ExecutionError> {
109    let T::PureInput {
110        original_input_index,
111        byte_index,
112        ty,
113        constraint,
114    } = pure;
115    let Some(bcs_bytes) = bytes.get_index(*byte_index) else {
116        invariant_violation!(
117            "Unbound byte index {} for pure input at index {}",
118            byte_index,
119            original_input_index.0
120        );
121    };
122    let BytesConstraint { command, argument } = constraint;
123    check_pure_bytes::<Mode>(*argument, bcs_bytes, ty)
124        .map_err(|e| e.with_command_index(*command as usize))
125}
126
127fn check_pure_bytes<Mode: ExecutionMode>(
128    command_arg_idx: u16,
129    bytes: &[u8],
130    constraint: &Type,
131) -> Result<(), ExecutionError> {
132    assert_invariant!(
133        !matches!(constraint, Type::Reference(_, _)),
134        "references should not be added as a constraint"
135    );
136    if Mode::allow_arbitrary_values() {
137        return Ok(());
138    }
139    let Some(layout) = primitive_serialization_layout(constraint)? else {
140        let msg = format!(
141            "Invalid usage of `Pure` argument for a non-primitive argument type at index {command_arg_idx}.",
142        );
143        return Err(ExecutionError::new_with_source(
144            ExecutionErrorKind::command_argument_error(
145                CommandArgumentError::InvalidUsageOfPureArg,
146                command_arg_idx,
147            ),
148            msg,
149        ));
150    };
151    bcs_argument_validate(bytes, command_arg_idx, layout)?;
152    Ok(())
153}
154
155fn primitive_serialization_layout(
156    param_ty: &Type,
157) -> Result<Option<PrimitiveArgumentLayout>, ExecutionError> {
158    Ok(match param_ty {
159        Type::Signer => return Ok(None),
160        Type::Reference(_, _) => {
161            invariant_violation!("references should not be added as a constraint")
162        }
163        Type::Bool => Some(PrimitiveArgumentLayout::Bool),
164        Type::U8 => Some(PrimitiveArgumentLayout::U8),
165        Type::U16 => Some(PrimitiveArgumentLayout::U16),
166        Type::U32 => Some(PrimitiveArgumentLayout::U32),
167        Type::U64 => Some(PrimitiveArgumentLayout::U64),
168        Type::U128 => Some(PrimitiveArgumentLayout::U128),
169        Type::U256 => Some(PrimitiveArgumentLayout::U256),
170        Type::Address => Some(PrimitiveArgumentLayout::Address),
171
172        Type::Vector(v) => {
173            let info_opt = primitive_serialization_layout(&v.element_type)?;
174            info_opt.map(|layout| PrimitiveArgumentLayout::Vector(Box::new(layout)))
175        }
176        Type::Datatype(dt) => {
177            let resolved = dt.qualified_ident();
178            // is option of a string
179            if resolved == RESOLVED_STD_OPTION && dt.type_arguments.len() == 1 {
180                let info_opt = primitive_serialization_layout(dt.type_arguments.first().unwrap())?;
181                info_opt.map(|layout| PrimitiveArgumentLayout::Option(Box::new(layout)))
182            } else if dt.type_arguments.is_empty() {
183                if resolved == RESOLVED_SUI_ID {
184                    Some(PrimitiveArgumentLayout::Address)
185                } else if resolved == RESOLVED_ASCII_STR {
186                    Some(PrimitiveArgumentLayout::Ascii)
187                } else if resolved == RESOLVED_UTF8_STR {
188                    Some(PrimitiveArgumentLayout::UTF8)
189                } else {
190                    None
191                }
192            } else {
193                None
194            }
195        }
196    })
197}
198
199fn check_receiving_input(receiving: &T::ReceivingInput) -> Result<(), ExecutionError> {
200    let T::ReceivingInput {
201        original_input_index: _,
202        object_ref: _,
203        ty,
204        constraint,
205    } = receiving;
206    let BytesConstraint { command, argument } = constraint;
207    check_receiving(*argument, ty).map_err(|e| e.with_command_index(*command as usize))
208}
209
210fn check_receiving(command_arg_idx: u16, constraint: &Type) -> Result<(), ExecutionError> {
211    if is_valid_receiving(constraint) {
212        Ok(())
213    } else {
214        Err(command_argument_error(
215            CommandArgumentError::TypeMismatch,
216            command_arg_idx as usize,
217        ))
218    }
219}
220
221pub fn is_valid_pure_type(constraint: &Type) -> Result<bool, ExecutionError> {
222    Ok(primitive_serialization_layout(constraint)?.is_some())
223}
224
225/// Returns true if a type is a `Receiving<t>` where `t` has `key`
226pub fn is_valid_receiving(constraint: &Type) -> bool {
227    let Type::Datatype(dt) = constraint else {
228        return false;
229    };
230    dt.qualified_ident() == RESOLVED_RECEIVING_STRUCT
231        && dt.type_arguments.len() == 1
232        && dt.type_arguments.first().unwrap().abilities().has_key()
233}
234
235//**************************************************************************************************
236// Object usage
237//**************************************************************************************************
238
239fn command(env: &Env, context: &mut Context, sp!(_, c): &T::Command) -> Result<(), ExecutionError> {
240    match &c.command {
241        T::Command__::MoveCall(mc) => {
242            check_obj_usages(context, &mc.arguments)?;
243            if !(env.protocol_config.enable_accumulators() && is_coin_send_funds(&mc.function)) {
244                // We allow the gas coin to be used with `sui::coin::send_funds`
245                check_gas_by_values(&mc.arguments)?;
246            }
247        }
248        T::Command__::TransferObjects(objects, recipient) => {
249            check_obj_usages(context, objects)?;
250            check_obj_usage(context, recipient)?;
251            // gas can be used by value in TransferObjects
252        }
253        T::Command__::SplitCoins(_, coin, amounts) => {
254            check_obj_usage(context, coin)?;
255            check_obj_usages(context, amounts)?;
256            check_gas_by_value(coin)?;
257            check_gas_by_values(amounts)?;
258        }
259        T::Command__::MergeCoins(_, target, coins) => {
260            check_obj_usage(context, target)?;
261            check_obj_usages(context, coins)?;
262            check_gas_by_value(target)?;
263            check_gas_by_values(coins)?;
264        }
265        T::Command__::MakeMoveVec(_, xs) => {
266            check_obj_usages(context, xs)?;
267            check_gas_by_values(xs)?;
268        }
269        T::Command__::Publish(_, _, _) => (),
270        T::Command__::Upgrade(_, _, _, x, _) => {
271            check_obj_usage(context, x)?;
272            check_gas_by_value(x)?;
273        }
274    }
275    Ok(())
276}
277
278// Checks for valid by-mut-ref and by-value usage of input objects
279fn check_obj_usages(
280    context: &mut Context,
281    arguments: &[T::Argument],
282) -> Result<(), ExecutionError> {
283    for arg in arguments {
284        check_obj_usage(context, arg)?;
285    }
286    Ok(())
287}
288
289fn check_obj_usage(context: &mut Context, arg: &T::Argument) -> Result<(), ExecutionError> {
290    match &arg.value.0 {
291        T::Argument__::Borrow(true, l) => check_obj_by_mut_ref(context, arg.idx, l),
292        T::Argument__::Use(T::Usage::Move(l)) => check_by_value(context, arg.idx, l),
293        // We do not care about
294        // - immutable object borrowing
295        // - copying/read ref (since you cannot copy objects)
296        // - freeze (since an input object cannot be a reference without a borrow)
297        T::Argument__::Borrow(false, _)
298        | T::Argument__::Use(T::Usage::Copy { .. })
299        | T::Argument__::Read(_)
300        | T::Argument__::Freeze(_) => Ok(()),
301    }
302}
303
304// Checks for valid by-mut-ref usage of input objects
305fn check_obj_by_mut_ref(
306    context: &mut Context,
307    arg_idx: u16,
308    location: &T::Location,
309) -> Result<(), ExecutionError> {
310    match location {
311        T::Location::WithdrawalInput(_)
312        | T::Location::PureInput(_)
313        | T::Location::ReceivingInput(_)
314        | T::Location::TxContext
315        | T::Location::GasCoin
316        | T::Location::Result(_, _) => Ok(()),
317        T::Location::ObjectInput(idx) => {
318            if !context.objects.safe_get(*idx as usize)?.allow_by_mut_ref {
319                Err(command_argument_error(
320                    CommandArgumentError::InvalidObjectByMutRef,
321                    arg_idx as usize,
322                ))
323            } else {
324                Ok(())
325            }
326        }
327    }
328}
329
330// Checks for valid by-value usage of input objects
331fn check_by_value(
332    context: &mut Context,
333    arg_idx: u16,
334    location: &T::Location,
335) -> Result<(), ExecutionError> {
336    match location {
337        T::Location::GasCoin
338        | T::Location::Result(_, _)
339        | T::Location::TxContext
340        | T::Location::WithdrawalInput(_)
341        | T::Location::PureInput(_)
342        | T::Location::ReceivingInput(_) => Ok(()),
343        T::Location::ObjectInput(idx) => {
344            if !context.objects.safe_get(*idx as usize)?.allow_by_value {
345                Err(command_argument_error(
346                    CommandArgumentError::InvalidObjectByValue,
347                    arg_idx as usize,
348                ))
349            } else {
350                Ok(())
351            }
352        }
353    }
354}
355
356// Checks for no by value usage of gas
357fn check_gas_by_values(arguments: &[T::Argument]) -> Result<(), ExecutionError> {
358    for arg in arguments {
359        check_gas_by_value(arg)?;
360    }
361    Ok(())
362}
363
364fn check_gas_by_value(arg: &T::Argument) -> Result<(), ExecutionError> {
365    match &arg.value.0 {
366        T::Argument__::Use(T::Usage::Move(l)) => check_gas_by_value_loc(arg.idx, l),
367        // We do not care about the read/freeze case since they cannot move an object input
368        T::Argument__::Borrow(_, _)
369        | T::Argument__::Use(T::Usage::Copy { .. })
370        | T::Argument__::Read(_)
371        | T::Argument__::Freeze(_) => Ok(()),
372    }
373}
374
375fn check_gas_by_value_loc(idx: u16, location: &T::Location) -> Result<(), ExecutionError> {
376    match location {
377        T::Location::GasCoin => Err(command_argument_error(
378            CommandArgumentError::InvalidGasCoinUsage,
379            idx as usize,
380        )),
381        T::Location::TxContext
382        | T::Location::ObjectInput(_)
383        | T::Location::WithdrawalInput(_)
384        | T::Location::PureInput(_)
385        | T::Location::ReceivingInput(_)
386        | T::Location::Result(_, _) => Ok(()),
387    }
388}
389
390pub fn is_coin_send_funds(function: &T::LoadedFunction) -> bool {
391    function.original_mid.address() == &SUI_FRAMEWORK_ADDRESS
392        && function.original_mid.name() == COIN_MODULE_NAME
393        && function.name.as_ident_str() == SEND_FUNDS_FUNC_NAME
394}