Skip to main content

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