transaction_fuzzer/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5pub mod account_universe;
6pub mod config_fuzzer;
7pub mod executor;
8pub mod programmable_transaction_gen;
9pub mod transaction_data_gen;
10pub mod type_arg_fuzzer;
11
12use std::fmt::Debug;
13
14use executor::Executor;
15use iota_protocol_config::ProtocolConfig;
16use iota_types::{
17    base_types::{IotaAddress, ObjectID},
18    crypto::{AccountKeyPair, get_key_pair},
19    digests::TransactionDigest,
20    gas_coin::NANOS_PER_IOTA,
21    object::{MoveObject, OBJECT_START_VERSION, Object, Owner},
22    transaction::GasData,
23};
24use proptest::{collection::vec, prelude::*, test_runner::TestRunner};
25use rand::{SeedableRng, rngs::StdRng};
26
27fn new_gas_coin_with_balance_and_owner(balance: u64, owner: Owner) -> Object {
28    Object::new_move(
29        MoveObject::new_gas_coin(OBJECT_START_VERSION, ObjectID::random(), balance),
30        owner,
31        TransactionDigest::genesis_marker(),
32    )
33}
34
35/// Given a list of gas coin owners, generate random gas data and gas coins
36/// with the given owners.
37fn generate_random_gas_data(
38    seed: [u8; 32],
39    gas_coin_owners: Vec<Owner>, /* arbitrarily generated owners, can be shared or immutable or
40                                  * obj-owned too */
41    owned_by_sender: bool, // whether to set owned gas coins to be owned by the sender
42) -> GasDataWithObjects {
43    // This value is nested from the STARDUST_TOTAL_SUPPLY_NANOS constant that had
44    // been used as the maximum gas balance here before the inflation mechanism
45    // was implemented.
46    const MAX_GAS_BALANCE: u64 = 4_600_000_000 * NANOS_PER_IOTA;
47
48    let (sender, sender_key): (IotaAddress, AccountKeyPair) = get_key_pair();
49    let mut rng = StdRng::from_seed(seed);
50    let mut gas_objects = vec![];
51    let mut object_refs = vec![];
52
53    let total_gas_balance = rng.gen_range(0..=MAX_GAS_BALANCE);
54    let mut remaining_gas_balance = total_gas_balance;
55    let num_gas_objects = gas_coin_owners.len();
56    let gas_coin_owners = gas_coin_owners
57        .iter()
58        .map(|o| match o {
59            Owner::ObjectOwner(_) | Owner::AddressOwner(_) if owned_by_sender => {
60                Owner::AddressOwner(sender)
61            }
62            _ => *o,
63        })
64        .collect::<Vec<_>>();
65    for owner in gas_coin_owners.iter().take(num_gas_objects - 1) {
66        let gas_balance = rng.gen_range(0..=remaining_gas_balance);
67        let gas_object = new_gas_coin_with_balance_and_owner(gas_balance, *owner);
68        remaining_gas_balance -= gas_balance;
69        object_refs.push(gas_object.compute_object_reference());
70        gas_objects.push(gas_object);
71    }
72    // Put the remaining balance in the last gas object.
73    let last_gas_object = new_gas_coin_with_balance_and_owner(
74        remaining_gas_balance,
75        gas_coin_owners[num_gas_objects - 1],
76    );
77    object_refs.push(last_gas_object.compute_object_reference());
78    gas_objects.push(last_gas_object);
79
80    assert_eq!(gas_objects.len(), num_gas_objects);
81    assert_eq!(
82        gas_objects
83            .iter()
84            .map(|o| o.data.try_as_move().unwrap().get_coin_value_unsafe())
85            .sum::<u64>(),
86        total_gas_balance
87    );
88
89    GasDataWithObjects {
90        gas_data: GasData {
91            payment: object_refs,
92            owner: sender,
93            price: rng.gen_range(0..=ProtocolConfig::get_for_max_version_UNSAFE().max_gas_price()),
94            budget: rng.gen_range(0..=ProtocolConfig::get_for_max_version_UNSAFE().max_tx_gas()),
95        },
96        objects: gas_objects,
97        sender_key,
98    }
99}
100
101/// Need to have a wrapper struct here so we can implement Arbitrary for it.
102#[derive(Debug)]
103pub struct GasDataWithObjects {
104    pub gas_data: GasData,
105    pub sender_key: AccountKeyPair,
106    pub objects: Vec<Object>,
107}
108
109#[derive(Debug, Default)]
110pub struct GasDataGenConfig {
111    pub max_num_gas_objects: usize,
112    pub owned_by_sender: bool,
113}
114
115impl GasDataGenConfig {
116    pub fn owned_by_sender_or_immut() -> Self {
117        Self {
118            max_num_gas_objects: ProtocolConfig::get_for_max_version_UNSAFE()
119                .max_gas_payment_objects() as usize,
120            owned_by_sender: true,
121        }
122    }
123
124    pub fn any_owner() -> Self {
125        Self {
126            max_num_gas_objects: ProtocolConfig::get_for_max_version_UNSAFE()
127                .max_gas_payment_objects() as usize,
128            owned_by_sender: false,
129        }
130    }
131}
132
133impl proptest::arbitrary::Arbitrary for GasDataWithObjects {
134    type Parameters = GasDataGenConfig;
135    type Strategy = BoxedStrategy<Self>;
136
137    fn arbitrary_with(params: Self::Parameters) -> Self::Strategy {
138        (
139            any::<[u8; 32]>(),
140            vec(any::<Owner>(), 1..=params.max_num_gas_objects),
141        )
142            .prop_map(move |(seed, owners)| {
143                generate_random_gas_data(seed, owners, params.owned_by_sender)
144            })
145            .boxed()
146    }
147}
148
149#[derive(Clone, Debug)]
150pub struct TestData<D> {
151    pub data: D,
152    pub executor: Executor,
153}
154
155/// Run a proptest test with give number of test cases, a strategy for something
156/// and a test function testing that something with an `Arc<AuthorityState>`.
157pub fn run_proptest<D>(
158    num_test_cases: u32,
159    strategy: impl Strategy<Value = D>,
160    test_fn: impl Fn(D, Executor) -> Result<(), TestCaseError>,
161) where
162    D: Debug + 'static,
163{
164    let mut runner = TestRunner::new(ProptestConfig {
165        cases: num_test_cases,
166        ..Default::default()
167    });
168    let executor = Executor::new();
169    let strategy_with_authority = strategy.prop_map(|data| TestData {
170        data,
171        executor: executor.clone(),
172    });
173    let result = runner.run(&strategy_with_authority, |test_data| {
174        test_fn(test_data.data, test_data.executor)
175    });
176    if result.is_err() {
177        panic!("test failed: {:?}", result);
178    }
179}