transaction_fuzzer/
programmable_transaction_gen.rs

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