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