transaction_fuzzer/
programmable_transaction_gen.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{cmp, str::FromStr};
5
6use move_core_types::identifier::Identifier;
7use once_cell::sync::Lazy;
8use proptest::collection::vec;
9use proptest::prelude::*;
10use sui_protocol_config::ProtocolConfig;
11use sui_types::base_types::{ObjectID, ObjectRef, SuiAddress};
12use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder;
13use sui_types::transaction::{Argument, CallArg, Command, ProgrammableTransaction};
14
15static PROTOCOL_CONFIG: Lazy<ProtocolConfig> =
16    Lazy::new(ProtocolConfig::get_for_max_version_UNSAFE);
17
18prop_compose! {
19    pub fn gen_transfer()
20        (x in arg_len_strategy())
21        (args in vec(gen_argument(), x..=x), arg_to in gen_argument()) -> Command {
22                Command::TransferObjects(args, arg_to)
23    }
24}
25
26prop_compose! {
27    pub fn gen_split_coins()
28        (x in arg_len_strategy())
29        (args in vec(gen_argument(), x..=x), arg_to in gen_argument()) -> Command {
30                Command::SplitCoins(arg_to, args)
31    }
32}
33
34prop_compose! {
35    pub fn gen_merge_coins()
36        (x in arg_len_strategy())
37        (args in vec(gen_argument(), x..=x), arg_from in gen_argument()) -> Command {
38                Command::MergeCoins(arg_from, args)
39    }
40}
41
42prop_compose! {
43    pub fn gen_move_vec()
44        (x in arg_len_strategy())
45        (args in vec(gen_argument(), x..=x)) -> Command {
46                Command::MakeMoveVec(None, args)
47    }
48}
49
50prop_compose! {
51    pub fn gen_programmable_transaction()
52        (len in command_len_strategy())
53        (commands in vec(gen_command(), len..=len)) -> ProgrammableTransaction {
54            let mut builder = ProgrammableTransactionBuilder::new();
55            for command in commands {
56                builder.command(command);
57            }
58            builder.finish()
59    }
60}
61
62pub fn gen_command() -> impl Strategy<Value = Command> {
63    prop_oneof![
64        gen_transfer(),
65        gen_split_coins(),
66        gen_merge_coins(),
67        gen_move_vec(),
68    ]
69}
70
71pub fn gen_argument() -> impl Strategy<Value = Argument> {
72    prop_oneof![
73        Just(Argument::GasCoin),
74        u16_with_boundaries_strategy().prop_map(Argument::Input),
75        u16_with_boundaries_strategy().prop_map(Argument::Result),
76        (
77            u16_with_boundaries_strategy(),
78            u16_with_boundaries_strategy()
79        )
80            .prop_map(|(a, b)| Argument::NestedResult(a, b))
81    ]
82}
83
84pub fn u16_with_boundaries_strategy() -> impl Strategy<Value = u16> {
85    prop_oneof![
86        5 => 0u16..u16::MAX - 1,
87        1 => Just(u16::MAX - 1),
88        1 => Just(u16::MAX),
89    ]
90}
91
92pub fn arg_len_strategy() -> impl Strategy<Value = usize> {
93    let max_args = PROTOCOL_CONFIG.max_arguments() as usize;
94    1usize..max_args
95}
96
97pub fn command_len_strategy() -> impl Strategy<Value = usize> {
98    let max_commands = PROTOCOL_CONFIG.max_programmable_tx_commands() as usize;
99    // Favor smaller transactions to make things faster. But generate a big one every once in a while
100    prop_oneof![
101        10 => 1usize..10,
102        1 => 10..=max_commands,
103    ]
104}
105
106// these constants have been chosen to deliver a reasonable runtime overhead and can be played with
107
108/// this also reflects the fact that we have coin-generating functions that can generate between 1
109/// and MAX_ARG_LEN_INPUT_MATCH coins
110pub const MAX_ARG_LEN_INPUT_MATCH: usize = 64;
111pub const MAX_COMMANDS_INPUT_MATCH: usize = 24;
112pub const MAX_ITERATIONS_INPUT_MATCH: u32 = 10;
113pub const MAX_SPLIT_AMOUNT: u64 = 1000;
114/// the merge command takes must take no more than MAX_ARG_LEN_INPUT_MATCH total to make sure that
115/// we have enough coins to pass as input
116pub const MAX_COINS_TO_MERGE: u64 = (MAX_ARG_LEN_INPUT_MATCH - 1) as u64;
117/// the max number of coins that the vector can be made out of cannot exceed the number of coins we
118/// can generate as input
119pub const MAX_VECTOR_COINS: usize = MAX_ARG_LEN_INPUT_MATCH;
120
121/// Stand-ins for programmable transaction Commands used to randomly generate values used when
122/// creating the actual command instances
123#[derive(Debug)]
124pub enum CommandSketch {
125    // Command::TransferObjects sketch - argument describes number of objects to transfer
126    TransferObjects(u64),
127    // Command::SplitCoins sketch - argument describes coin values to split
128    SplitCoins(Vec<u64>),
129    // Command::MergeCoins sketch - argument describes number of coins to merge
130    MergeCoins(u64),
131    // Command::MakeMoveVec sketch - argument describes coins to be put into a vector
132    MakeMoveVec(Vec<u64>),
133}
134
135prop_compose! {
136    pub fn gen_transfer_input_match()
137        (x in arg_len_strategy_input_match()) -> CommandSketch {
138            CommandSketch::TransferObjects(x as u64)
139    }
140}
141
142prop_compose! {
143    pub fn gen_split_coins_input_match()
144        (x in arg_len_strategy_input_match())
145        (args in vec(1..MAX_SPLIT_AMOUNT, x..=x)) -> CommandSketch {
146            CommandSketch::SplitCoins(args)
147    }
148}
149
150prop_compose! {
151    pub fn gen_merge_coins_input_match()
152        (coins_to_merge in 1..MAX_COINS_TO_MERGE) -> CommandSketch {
153            CommandSketch::MergeCoins(coins_to_merge)
154    }
155}
156
157prop_compose! {
158    pub fn gen_move_vec_input_match()
159        (vec_size in 1..MAX_VECTOR_COINS)
160        (args in vec(1u64..7u64, vec_size..=vec_size)) -> CommandSketch {
161            // at this point we don't care about coin values to be put into the vector but we keep
162            // the vector itself to be able to match on a union of MakeMoveVec and SplitCoins when
163            // generating the actual commands
164            CommandSketch::MakeMoveVec(args)
165    }
166}
167
168pub fn gen_command_input_match() -> impl Strategy<Value = CommandSketch> {
169    prop_oneof![
170        gen_transfer_input_match(),
171        gen_split_coins_input_match(),
172        gen_merge_coins_input_match(),
173        gen_move_vec_input_match(),
174    ]
175}
176
177pub fn arg_len_strategy_input_match() -> impl Strategy<Value = usize> {
178    prop_oneof![
179        20 => 1usize..10,
180        10 => 10usize..MAX_ARG_LEN_INPUT_MATCH
181    ]
182}
183
184prop_compose! {
185    pub fn gen_many_input_match(recipient: SuiAddress, package: ObjectID, cap: ObjectRef)
186        (mut command_sketches in vec(gen_command_input_match(), 1..=MAX_COMMANDS_INPUT_MATCH)) -> ProgrammableTransaction {
187            let mut builder = ProgrammableTransactionBuilder::new();
188            let mut prev_cmd_num = -1;
189            // does not matter which is picked as first as they are generated randomly anyway
190            let first_cmd_sketch = command_sketches.pop().unwrap();
191            let (first_cmd, cmd_inc) = gen_input(&mut builder, None, &first_cmd_sketch, prev_cmd_num, recipient, package, cap);
192            builder.command(first_cmd);
193            prev_cmd_num += cmd_inc + 1;
194            let mut prev_cmd = first_cmd_sketch;
195            for cmd_sketch in command_sketches {
196                let (cmd, cmd_inc) = gen_input(&mut builder, Some(&prev_cmd), &cmd_sketch, prev_cmd_num, recipient, package, cap);
197                builder.command(cmd);
198                prev_cmd_num += cmd_inc + 1;
199                prev_cmd = cmd_sketch;
200            }
201            builder.finish()
202    }
203}
204
205fn gen_input(
206    builder: &mut ProgrammableTransactionBuilder,
207    prev_command: Option<&CommandSketch>,
208    cmd: &CommandSketch,
209    prev_cmd_num: i64,
210    recipient: SuiAddress,
211    package: ObjectID,
212    cap: ObjectRef,
213) -> (Command, i64) {
214    match cmd {
215        CommandSketch::TransferObjects(_) => gen_transfer_input(
216            builder,
217            prev_command,
218            cmd,
219            prev_cmd_num,
220            recipient,
221            package,
222            cap,
223        ),
224        CommandSketch::SplitCoins(_) => {
225            gen_split_coins_input(builder, cmd, prev_cmd_num, package, cap)
226        }
227        CommandSketch::MergeCoins(_) => {
228            gen_merge_coins_input(builder, prev_command, cmd, prev_cmd_num, package, cap)
229        }
230        CommandSketch::MakeMoveVec(_) => {
231            gen_move_vec_input(builder, prev_command, cmd, prev_cmd_num, package, cap)
232        }
233    }
234}
235
236pub fn gen_transfer_input(
237    builder: &mut ProgrammableTransactionBuilder,
238    prev_command: Option<&CommandSketch>,
239    cmd: &CommandSketch,
240    prev_cmd_num: i64,
241    recipient: SuiAddress,
242    package: ObjectID,
243    cap: ObjectRef,
244) -> (Command, i64) {
245    let CommandSketch::TransferObjects(args_len) = cmd else {
246        panic!("Should be TransferObjects command");
247    };
248    let mut coins = vec![];
249    // we need that many coins as input to transfer
250    let coins_needed = *args_len as usize;
251
252    let cmd_inc = gen_transfer_or_move_vec_input_internal(
253        builder,
254        prev_cmd_num,
255        package,
256        cap,
257        prev_command,
258        coins_needed,
259        &mut coins,
260    );
261    assert!(coins.len() == *args_len as usize);
262
263    let next_cmd = Command::TransferObjects(coins, builder.pure(recipient).unwrap());
264    (next_cmd, cmd_inc)
265}
266
267pub fn gen_split_coins_input(
268    builder: &mut ProgrammableTransactionBuilder,
269    cmd: &CommandSketch,
270    prev_cmd_num: i64,
271    package: ObjectID,
272    cap: ObjectRef,
273) -> (Command, i64) {
274    let CommandSketch::SplitCoins(split_amounts) = cmd else {
275        panic!("Should be SplitCoins command");
276    };
277    let mut cmd_inc = 0;
278    let mut split_args = vec![];
279
280    // the tradeoff here is that we either generate output for each split command that will make it
281    // succeed or we will very quickly hit the insufficient coin error only after a few (often just
282    // 2) split coin transactions are executed making the whole batch testing into a rather narrow
283    // error case
284    create_input_calls(
285        builder,
286        package,
287        cap,
288        prev_cmd_num,
289        MAX_SPLIT_AMOUNT * split_amounts.len() as u64,
290        1,
291    );
292    cmd_inc += 2; // two input calls
293
294    for s in split_amounts {
295        split_args.push(builder.pure(*s).unwrap());
296    }
297
298    let coin_arg = Argument::Result((prev_cmd_num + cmd_inc) as u16);
299    let next_cmd = Command::SplitCoins(coin_arg, split_args);
300    (next_cmd, cmd_inc)
301}
302
303pub fn gen_merge_coins_input(
304    builder: &mut ProgrammableTransactionBuilder,
305    prev_command: Option<&CommandSketch>,
306    cmd: &CommandSketch,
307    prev_cmd_num: i64,
308    package: ObjectID,
309    cap: ObjectRef,
310) -> (Command, i64) {
311    let CommandSketch::MergeCoins(coins_to_merge) = cmd else {
312        panic!("Should be MergeCoins command");
313    };
314    let mut cmd_inc = 0;
315    let mut coins = vec![];
316    // we need all coins that are going to be merged plus on that they are going to be merged into
317    let coins_needed = *coins_to_merge as usize + 1;
318
319    let output_coin = if let Some(prev_cmd) = prev_command {
320        match prev_cmd {
321            CommandSketch::TransferObjects(_) | CommandSketch::MergeCoins(_) => {
322                // no useful input
323                create_input_calls(builder, package, cap, prev_cmd_num, 7, coins_needed as u64);
324                cmd_inc += 2; // two input calls
325                for i in 0..coins_needed - 1 {
326                    coins.push(Argument::NestedResult(
327                        (prev_cmd_num + cmd_inc) as u16,
328                        i as u16,
329                    ));
330                }
331                Argument::NestedResult((prev_cmd_num + cmd_inc) as u16, *coins_to_merge as u16)
332            }
333            CommandSketch::SplitCoins(output) | CommandSketch::MakeMoveVec(output) => {
334                // how many coins we have a available as output from previous command that we can
335                // immediately use as input to the next command
336                let usable_coins = cmp::min(output.len(), coins_needed);
337                if let CommandSketch::MakeMoveVec(_) = prev_cmd {
338                    create_unpack_call(builder, package, prev_cmd_num, output.len() as u64);
339                    cmd_inc += 1; // unpack call
340                };
341                // there is at least one coin in the output - use it as the coin others are merged into
342                let res_coin = Argument::NestedResult((prev_cmd_num + cmd_inc) as u16, 0);
343
344                cmd_inc = gen_enough_arguments(
345                    builder,
346                    prev_cmd_num,
347                    package,
348                    cap,
349                    coins_needed,
350                    usable_coins,
351                    1, /* one available coin already used */
352                    output.len(),
353                    &mut coins,
354                    cmd_inc,
355                );
356                res_coin
357            }
358        }
359    } else {
360        // first command - no input
361        create_input_calls(builder, package, cap, prev_cmd_num, 7, coins_needed as u64);
362        cmd_inc += 2; // two input calls
363        for i in 0..coins_needed - 1 {
364            coins.push(Argument::NestedResult(
365                (prev_cmd_num + cmd_inc) as u16,
366                i as u16,
367            ));
368        }
369        Argument::NestedResult((prev_cmd_num + cmd_inc) as u16, *coins_to_merge as u16)
370    };
371
372    let next_cmd = Command::MergeCoins(output_coin, coins);
373    (next_cmd, cmd_inc)
374}
375
376pub fn gen_move_vec_input(
377    builder: &mut ProgrammableTransactionBuilder,
378    prev_command: Option<&CommandSketch>,
379    cmd: &CommandSketch,
380    prev_cmd_num: i64,
381    package: ObjectID,
382    cap: ObjectRef,
383) -> (Command, i64) {
384    let CommandSketch::MakeMoveVec(vector_coins) = cmd else {
385        panic!("Should be MakeMoveVec command");
386    };
387    let mut coins = vec![];
388    // we need that many coins as input to transfer
389    let coins_needed = vector_coins.len();
390
391    let cmd_inc = gen_transfer_or_move_vec_input_internal(
392        builder,
393        prev_cmd_num,
394        package,
395        cap,
396        prev_command,
397        coins_needed,
398        &mut coins,
399    );
400
401    let next_cmd = Command::MakeMoveVec(None, coins);
402    (next_cmd, cmd_inc)
403}
404
405/// A helper function to generate enough input coins for a command (transfer, merge, or create vector)
406/// - either collect them all from previous command or generate additional ones if the previous
407///   command does not deliver enough.
408fn gen_enough_arguments(
409    builder: &mut ProgrammableTransactionBuilder,
410    prev_cmd_num: i64,
411    package: ObjectID,
412    cap: ObjectRef,
413    coins_needed: usize,
414    coins_available: usize,
415    available_coins_used: usize,
416    prev_cmd_out_len: usize,
417    coins: &mut Vec<Argument>,
418    mut cmd_inc: i64,
419) -> i64 {
420    for i in available_coins_used..coins_available {
421        coins.push(Argument::NestedResult(
422            (prev_cmd_num + cmd_inc) as u16,
423            i as u16,
424        ));
425    }
426    if prev_cmd_out_len < coins_needed {
427        // we have some arguments from previous command's output but not all
428        let remaining_args_num = (coins_needed - prev_cmd_out_len) as u64;
429        create_input_calls(
430            builder,
431            package,
432            cap,
433            prev_cmd_num + cmd_inc,
434            7,
435            remaining_args_num,
436        );
437        cmd_inc += 2; // two input calls
438        for i in 0..remaining_args_num {
439            coins.push(Argument::NestedResult(
440                (prev_cmd_num + cmd_inc) as u16,
441                i as u16,
442            ));
443        }
444    }
445    cmd_inc
446}
447
448/// A helper function to generate arguments fro transfer or create vector commands as they are
449/// exactly the same.
450fn gen_transfer_or_move_vec_input_internal(
451    builder: &mut ProgrammableTransactionBuilder,
452    prev_cmd_num: i64,
453    package: ObjectID,
454    cap: ObjectRef,
455    prev_command: Option<&CommandSketch>,
456    coins_needed: usize,
457    coins: &mut Vec<Argument>,
458) -> i64 {
459    let mut cmd_inc = 0;
460    if let Some(prev_cmd) = prev_command {
461        match prev_cmd {
462            CommandSketch::TransferObjects(_) | CommandSketch::MergeCoins(_) => {
463                // no useful input
464                create_input_calls(builder, package, cap, prev_cmd_num, 7, coins_needed as u64);
465                cmd_inc += 2; // two input calls
466                for i in 0..coins_needed {
467                    coins.push(Argument::NestedResult(
468                        (prev_cmd_num + cmd_inc) as u16,
469                        i as u16,
470                    ));
471                }
472            }
473            CommandSketch::SplitCoins(output) | CommandSketch::MakeMoveVec(output) => {
474                // how many coins we have a available as output from previous command that we can
475                // immediately use as input to the next command
476                let usable_coins = cmp::min(output.len(), coins_needed);
477                if let CommandSketch::MakeMoveVec(_) = prev_cmd {
478                    create_unpack_call(builder, package, prev_cmd_num, output.len() as u64);
479                    cmd_inc += 1; // unpack call
480                };
481
482                cmd_inc = gen_enough_arguments(
483                    builder,
484                    prev_cmd_num,
485                    package,
486                    cap,
487                    coins_needed,
488                    usable_coins,
489                    0, /* no available coins used */
490                    output.len(),
491                    coins,
492                    cmd_inc,
493                )
494            }
495        }
496    } else {
497        // first command - no input
498        create_input_calls(builder, package, cap, prev_cmd_num, 7, coins_needed as u64);
499        cmd_inc += 2; // two input calls
500        for i in 0..coins_needed {
501            coins.push(Argument::NestedResult(
502                (prev_cmd_num + cmd_inc) as u16,
503                i as u16,
504            ));
505        }
506    }
507    cmd_inc
508}
509
510fn create_input_calls(
511    builder: &mut ProgrammableTransactionBuilder,
512    package: ObjectID,
513    cap: ObjectRef,
514    prev_cmd_num: i64,
515    coin_value: u64,
516    input_size: u64,
517) {
518    builder
519        .move_call(
520            package,
521            Identifier::from_str("coin_factory").unwrap(),
522            Identifier::from_str("mint_vec").unwrap(),
523            vec![],
524            vec![
525                CallArg::from(cap),
526                CallArg::from(coin_value),
527                CallArg::from(input_size),
528            ],
529        )
530        .unwrap();
531    create_unpack_call(builder, package, prev_cmd_num + 1, input_size);
532}
533
534fn create_unpack_call(
535    builder: &mut ProgrammableTransactionBuilder,
536    package: ObjectID,
537    prev_cmd_num: i64,
538    input_size: u64,
539) {
540    builder.programmable_move_call(
541        package,
542        Identifier::from_str("coin_factory").unwrap(),
543        Identifier::from_str(format!("unpack_{input_size}").as_str()).unwrap(),
544        vec![],
545        vec![Argument::Result(prev_cmd_num as u16)],
546    );
547}