transaction_fuzzer/account_universe/
transfer_gen.rs

1// Copyright (c) Mysten Labs, Inc.
2// Copyright (c) The Diem Core Contributors
3// Modifications Copyright (c) 2024 IOTA Stiftung
4// SPDX-License-Identifier: Apache-2.0
5
6use std::sync::Arc;
7
8use iota_protocol_config::ProtocolConfig;
9use iota_types::{
10    base_types::{IotaAddress, ObjectRef},
11    error::{IotaError, UserInputError},
12    execution_status::{ExecutionFailureStatus, ExecutionStatus},
13    object::Object,
14    programmable_transaction_builder::ProgrammableTransactionBuilder,
15    transaction::{GasData, Transaction, TransactionData, TransactionKind},
16    utils::{to_sender_signed_transaction, to_sender_signed_transaction_with_multi_signers},
17};
18use once_cell::sync::Lazy;
19use proptest::prelude::*;
20use proptest_derive::Arbitrary;
21
22use crate::{
23    account_universe::{
24        AUTransactionGen, AccountCurrent, AccountPairGen, AccountTriple, AccountUniverse,
25    },
26    executor::{ExecutionResult, Executor},
27};
28
29const GAS_UNIT_PRICE: u64 = 2;
30const DEFAULT_TRANSFER_AMOUNT: u64 = 1;
31const P2P_COMPUTE_GAS_USAGE: u64 = 1000;
32const P2P_SUCCESS_STORAGE_USAGE: u64 = 1976000 - 15200; // this needs to be adapted if the size of objects changes
33const P2P_FAILURE_STORAGE_USAGE: u64 = 988000 - 7600; // this needs to be adapted if the size of objects change
34const INSUFFICIENT_GAS_UNITS_THRESHOLD: u64 = 2;
35
36static PROTOCOL_CONFIG: Lazy<ProtocolConfig> =
37    Lazy::new(ProtocolConfig::get_for_max_version_UNSAFE);
38
39/// Represents a peer-to-peer transaction performed in the account universe.
40///
41/// The parameters are the minimum and maximum balances to transfer.
42#[derive(Arbitrary, Clone, Debug)]
43#[proptest(params = "(u64, u64)")]
44pub struct P2PTransferGenGoodGas {
45    sender_receiver: AccountPairGen,
46    #[proptest(strategy = "params.0 ..= params.1")]
47    amount: u64,
48}
49
50/// Represents a peer-to-peer transaction performed in the account universe with
51/// the gas budget randomly selected.
52#[derive(Arbitrary, Clone, Debug)]
53#[proptest(params = "(u64, u64)")]
54pub struct P2PTransferGenRandomGas {
55    sender_receiver: AccountPairGen,
56    #[proptest(strategy = "params.0 ..= params.1")]
57    amount: u64,
58    #[proptest(strategy = "gas_budget_selection_strategy()")]
59    gas: u64,
60}
61
62/// Represents a peer-to-peer transaction performed in the account universe with
63/// the gas budget and gas price randomly selected.
64#[derive(Arbitrary, Clone, Debug)]
65#[proptest(params = "(u64, u64)")]
66pub struct P2PTransferGenRandomGasRandomPrice {
67    sender_receiver: AccountPairGen,
68    #[proptest(strategy = "params.0 ..= params.1")]
69    amount: u64,
70    #[proptest(strategy = "gas_budget_selection_strategy()")]
71    gas: u64,
72    #[proptest(strategy = "gas_price_selection_strategy()")]
73    gas_price: u64,
74}
75
76#[derive(Arbitrary, Clone, Debug)]
77#[proptest(params = "(u64, u64)")]
78pub struct P2PTransferGenGasPriceInRange {
79    sender_receiver: AccountPairGen,
80    #[proptest(strategy = "params.0 ..= params.1")]
81    gas_price: u64,
82}
83
84/// Represents a peer-to-peer transaction performed in the account universe with
85/// the gas budget, gas price and number of gas coins randomly selected.
86#[derive(Arbitrary, Clone, Debug)]
87#[proptest(params = "(u64, u64)")]
88pub struct P2PTransferGenRandGasRandPriceRandCoins {
89    sender_receiver: AccountPairGen,
90    #[proptest(strategy = "params.0 ..= params.1")]
91    amount: u64,
92    #[proptest(strategy = "gas_budget_selection_strategy()")]
93    gas: u64,
94    #[proptest(strategy = "gas_price_selection_strategy()")]
95    gas_price: u64,
96    #[proptest(strategy = "gas_coins_selection_strategy()")]
97    gas_coins: u32,
98}
99/// Represents a peer-to-peer transaction performed in the account universe with
100/// the gas budget and gas price randomly selected and sponsorship state also
101/// randomly selected.
102#[derive(Arbitrary, Clone, Debug)]
103#[proptest(params = "(u64, u64)")]
104pub struct P2PTransferGenRandomGasRandomPriceRandomSponsorship {
105    sender_receiver: AccountPairGen,
106    #[proptest(strategy = "params.0 ..= params.1")]
107    amount: u64,
108    #[proptest(strategy = "gas_budget_selection_strategy()")]
109    gas: u64,
110    #[proptest(strategy = "gas_price_selection_strategy()")]
111    gas_price: u64,
112    #[proptest(strategy = "gas_coins_selection_strategy()")]
113    gas_coins: u32,
114    sponsorship: TransactionSponsorship,
115}
116
117#[derive(Arbitrary, Clone, Debug)]
118pub enum TransactionSponsorship {
119    // No sponsorship for the transaction.
120    None,
121    // Valid sponsorship for the transaction.
122    Good,
123    WrongGasOwner,
124}
125
126impl TransactionSponsorship {
127    pub fn select_gas(
128        &self,
129        accounts: &mut AccountTriple,
130        exec: &mut Executor,
131        gas_coins: u32,
132    ) -> (Vec<ObjectRef>, (u64, Object), IotaAddress) {
133        match self {
134            TransactionSponsorship::None => {
135                let gas_object = accounts.account_1.new_gas_object(exec);
136                let mut gas_amount = *accounts.account_1.current_balances.last().unwrap();
137                let mut gas_coin_refs = vec![gas_object.compute_object_reference()];
138                for _ in 1..gas_coins {
139                    let gas_object = accounts.account_1.new_gas_object(exec);
140                    gas_coin_refs.push(gas_object.compute_object_reference());
141                    gas_amount += *accounts.account_1.current_balances.last().unwrap();
142                }
143                (
144                    gas_coin_refs,
145                    (gas_amount, gas_object),
146                    accounts.account_1.initial_data.account.address,
147                )
148            }
149            TransactionSponsorship::Good => {
150                let gas_object = accounts.account_3.new_gas_object(exec);
151                let mut gas_amount = *accounts.account_3.current_balances.last().unwrap();
152                let mut gas_coin_refs = vec![gas_object.compute_object_reference()];
153                for _ in 1..gas_coins {
154                    let gas_object = accounts.account_3.new_gas_object(exec);
155                    gas_coin_refs.push(gas_object.compute_object_reference());
156                    gas_amount += *accounts.account_3.current_balances.last().unwrap();
157                }
158                (
159                    gas_coin_refs,
160                    (gas_amount, gas_object),
161                    accounts.account_3.initial_data.account.address,
162                )
163            }
164            TransactionSponsorship::WrongGasOwner => {
165                let gas_object = accounts.account_1.new_gas_object(exec);
166                let mut gas_amount = *accounts.account_1.current_balances.last().unwrap();
167                let mut gas_coin_refs = vec![gas_object.compute_object_reference()];
168                for _ in 1..gas_coins {
169                    let gas_object = accounts.account_1.new_gas_object(exec);
170                    gas_coin_refs.push(gas_object.compute_object_reference());
171                    gas_amount += *accounts.account_1.current_balances.last().unwrap();
172                }
173                (
174                    gas_coin_refs,
175                    (gas_amount, gas_object),
176                    accounts.account_3.initial_data.account.address,
177                )
178            }
179        }
180    }
181
182    pub fn sign_transaction(&self, accounts: &AccountTriple, txn: TransactionData) -> Transaction {
183        match self {
184            TransactionSponsorship::None => {
185                to_sender_signed_transaction(txn, &accounts.account_1.initial_data.account.key)
186            }
187            TransactionSponsorship::Good | TransactionSponsorship::WrongGasOwner => {
188                to_sender_signed_transaction_with_multi_signers(
189                    txn,
190                    vec![
191                        &accounts.account_1.initial_data.account.key,
192                        &accounts.account_3.initial_data.account.key,
193                    ],
194                )
195            }
196        }
197    }
198
199    pub fn sponsor<'a>(&self, account_triple: &'a mut AccountTriple) -> &'a mut AccountCurrent {
200        match self {
201            TransactionSponsorship::None => account_triple.account_1,
202            TransactionSponsorship::Good | TransactionSponsorship::WrongGasOwner => {
203                account_triple.account_3
204            }
205        }
206    }
207}
208
209fn p2p_success_gas(gas_price: u64) -> u64 {
210    gas_price * P2P_COMPUTE_GAS_USAGE + P2P_SUCCESS_STORAGE_USAGE
211}
212
213fn p2p_failure_gas(gas_price: u64) -> u64 {
214    gas_price * P2P_COMPUTE_GAS_USAGE + P2P_FAILURE_STORAGE_USAGE
215}
216
217pub fn gas_price_selection_strategy() -> impl Strategy<Value = u64> {
218    prop_oneof![
219        Just(0u64),
220        1u64..10_000,
221        Just(PROTOCOL_CONFIG.max_gas_price() - 1),
222        Just(PROTOCOL_CONFIG.max_gas_price()),
223        Just(PROTOCOL_CONFIG.max_gas_price() + 1),
224        // Div and subtract so we don't need to worry about overflow in the test when computing our
225        // success gas.
226        Just(u64::MAX / P2P_COMPUTE_GAS_USAGE - 1 - P2P_SUCCESS_STORAGE_USAGE),
227        Just(u64::MAX / P2P_COMPUTE_GAS_USAGE - P2P_SUCCESS_STORAGE_USAGE),
228    ]
229}
230
231pub fn gas_budget_selection_strategy() -> impl Strategy<Value = u64> {
232    prop_oneof![
233        Just(0u64),
234        PROTOCOL_CONFIG.base_tx_cost_fixed() / 2..=PROTOCOL_CONFIG.base_tx_cost_fixed() * 2000,
235        1_000_000u64..=3_000_000,
236        Just(PROTOCOL_CONFIG.max_tx_gas() - 1),
237        Just(PROTOCOL_CONFIG.max_tx_gas()),
238        Just(PROTOCOL_CONFIG.max_tx_gas() + 1),
239        Just(u64::MAX - 1),
240        Just(u64::MAX)
241    ]
242}
243
244fn gas_coins_selection_strategy() -> impl Strategy<Value = u32> {
245    prop_oneof![
246        2 => Just(1u32),
247        6 => 2u32..PROTOCOL_CONFIG.max_gas_payment_objects(),
248        1 => Just(PROTOCOL_CONFIG.max_gas_payment_objects()),
249        1 => Just(PROTOCOL_CONFIG.max_gas_payment_objects() + 1),
250    ]
251}
252
253impl AUTransactionGen for P2PTransferGenGoodGas {
254    fn apply(
255        &self,
256        universe: &mut AccountUniverse,
257        exec: &mut Executor,
258    ) -> (Transaction, ExecutionResult) {
259        P2PTransferGenRandomGas {
260            sender_receiver: self.sender_receiver.clone(),
261            amount: self.amount,
262            gas: p2p_success_gas(GAS_UNIT_PRICE),
263        }
264        .apply(universe, exec)
265    }
266}
267
268impl AUTransactionGen for P2PTransferGenRandomGas {
269    fn apply(
270        &self,
271        universe: &mut AccountUniverse,
272        exec: &mut Executor,
273    ) -> (Transaction, ExecutionResult) {
274        P2PTransferGenRandomGasRandomPriceRandomSponsorship {
275            sender_receiver: self.sender_receiver.clone(),
276            amount: self.amount,
277            gas: self.gas,
278            gas_price: GAS_UNIT_PRICE,
279            gas_coins: 1,
280            sponsorship: TransactionSponsorship::None,
281        }
282        .apply(universe, exec)
283    }
284}
285
286impl AUTransactionGen for P2PTransferGenGasPriceInRange {
287    fn apply(
288        &self,
289        universe: &mut AccountUniverse,
290        exec: &mut Executor,
291    ) -> (Transaction, ExecutionResult) {
292        P2PTransferGenRandomGasRandomPriceRandomSponsorship {
293            sender_receiver: self.sender_receiver.clone(),
294            amount: DEFAULT_TRANSFER_AMOUNT,
295            gas: p2p_success_gas(self.gas_price),
296            gas_price: self.gas_price,
297            gas_coins: 1,
298            sponsorship: TransactionSponsorship::None,
299        }
300        .apply(universe, exec)
301    }
302}
303
304impl AUTransactionGen for P2PTransferGenRandomGasRandomPrice {
305    fn apply(
306        &self,
307        universe: &mut AccountUniverse,
308        exec: &mut Executor,
309    ) -> (Transaction, ExecutionResult) {
310        P2PTransferGenRandomGasRandomPriceRandomSponsorship {
311            sender_receiver: self.sender_receiver.clone(),
312            amount: self.amount,
313            gas: self.gas,
314            gas_price: self.gas_price,
315            gas_coins: 1,
316            sponsorship: TransactionSponsorship::None,
317        }
318        .apply(universe, exec)
319    }
320}
321
322impl AUTransactionGen for P2PTransferGenRandGasRandPriceRandCoins {
323    fn apply(
324        &self,
325        universe: &mut AccountUniverse,
326        exec: &mut Executor,
327    ) -> (Transaction, ExecutionResult) {
328        P2PTransferGenRandomGasRandomPriceRandomSponsorship {
329            sender_receiver: self.sender_receiver.clone(),
330            amount: self.amount,
331            gas: self.gas,
332            gas_price: self.gas_price,
333            gas_coins: self.gas_coins,
334            sponsorship: TransactionSponsorship::None,
335        }
336        .apply(universe, exec)
337    }
338}
339
340// Encapsulates information needed to determine the result of a transaction
341// execution.
342#[derive(Debug)]
343struct RunInfo {
344    enough_max_gas: bool,
345    enough_computation_gas: bool,
346    enough_to_succeed: bool,
347    not_enough_gas: bool,
348    gas_budget_too_high: bool,
349    gas_budget_too_low: bool,
350    gas_price_too_high: bool,
351    gas_price_too_low: bool,
352    gas_units_too_low: bool,
353    too_many_gas_coins: bool,
354    wrong_gas_owner: bool,
355}
356
357impl RunInfo {
358    pub fn new(
359        payer_balance: u64,
360        rgp: u64,
361        p2p: &P2PTransferGenRandomGasRandomPriceRandomSponsorship,
362    ) -> Self {
363        let to_deduct = p2p.amount as u128 + p2p.gas as u128;
364        let enough_max_gas = payer_balance >= p2p.gas;
365        let enough_computation_gas = p2p.gas >= p2p.gas_price * P2P_COMPUTE_GAS_USAGE;
366        let enough_to_succeed = payer_balance as u128 >= to_deduct;
367        let gas_budget_too_high = p2p.gas > PROTOCOL_CONFIG.max_tx_gas();
368        let gas_budget_too_low = p2p.gas < PROTOCOL_CONFIG.base_tx_cost_fixed() * p2p.gas_price;
369        let not_enough_gas = p2p.gas < p2p_success_gas(p2p.gas_price);
370        let gas_price_too_low = p2p.gas_price < rgp;
371        let gas_price_too_high = p2p.gas_price > PROTOCOL_CONFIG.max_gas_price();
372        let gas_price_greater_than_budget = p2p.gas_price > p2p.gas;
373        let gas_units_too_low = p2p.gas_price > 0
374            && p2p.gas / p2p.gas_price < INSUFFICIENT_GAS_UNITS_THRESHOLD
375            || gas_price_greater_than_budget;
376        let too_many_gas_coins = p2p.gas_coins >= PROTOCOL_CONFIG.max_gas_payment_objects();
377        Self {
378            enough_max_gas,
379            enough_computation_gas,
380            enough_to_succeed,
381            not_enough_gas,
382            gas_budget_too_high,
383            gas_budget_too_low,
384            gas_price_too_high,
385            gas_price_too_low,
386            gas_units_too_low,
387            too_many_gas_coins,
388            wrong_gas_owner: matches!(p2p.sponsorship, TransactionSponsorship::WrongGasOwner),
389        }
390    }
391}
392
393impl AUTransactionGen for P2PTransferGenRandomGasRandomPriceRandomSponsorship {
394    fn apply(
395        &self,
396        universe: &mut AccountUniverse,
397        exec: &mut Executor,
398    ) -> (Transaction, ExecutionResult) {
399        let mut account_triple = self.sender_receiver.pick(universe);
400        let (gas_coin_refs, (gas_balance, gas_object), gas_payer) =
401            self.sponsorship
402                .select_gas(&mut account_triple, exec, self.gas_coins);
403
404        let AccountTriple {
405            account_1: sender,
406            account_2: recipient,
407            ..
408        } = &account_triple;
409        // construct a p2p transfer of a random amount of IOTA
410        let txn = {
411            let mut builder = ProgrammableTransactionBuilder::new();
412            builder.transfer_iota(recipient.initial_data.account.address, Some(self.amount));
413            builder.finish()
414        };
415        let sender_address = sender.initial_data.account.address;
416        let kind = TransactionKind::ProgrammableTransaction(txn);
417        let tx_data = TransactionData::new_with_gas_data(
418            kind,
419            sender_address,
420            GasData {
421                payment: gas_coin_refs,
422                owner: gas_payer,
423                price: self.gas_price,
424                budget: self.gas,
425            },
426        );
427        let signed_txn = self.sponsorship.sign_transaction(&account_triple, tx_data);
428        let payer = self.sponsorship.sponsor(&mut account_triple);
429        // *sender.current_balances.last().unwrap();
430        let rgp = exec.get_reference_gas_price();
431        let run_info = RunInfo::new(gas_balance, rgp, self);
432        let reference_gas_price = if PROTOCOL_CONFIG.protocol_defined_base_fee() {
433            PROTOCOL_CONFIG.base_gas_price()
434        } else {
435            exec.get_reference_gas_price()
436        };
437        let status = match run_info {
438            RunInfo {
439                enough_max_gas: true,
440                enough_computation_gas: true,
441                enough_to_succeed: true,
442                not_enough_gas: false,
443                gas_budget_too_high: false,
444                gas_budget_too_low: false,
445                gas_price_too_low: false,
446                gas_price_too_high: false,
447                gas_units_too_low: false,
448                too_many_gas_coins: false,
449                wrong_gas_owner: false,
450            } => {
451                self.fix_balance_and_gas_coins(payer, true);
452                Ok(ExecutionStatus::Success)
453            }
454            RunInfo {
455                too_many_gas_coins: true,
456                ..
457            } => Err(IotaError::UserInput {
458                error: UserInputError::SizeLimitExceeded {
459                    limit: "maximum number of gas payment objects".to_string(),
460                    value: "256".to_string(),
461                },
462            }),
463            RunInfo {
464                gas_price_too_low: true,
465                ..
466            } => Err(IotaError::UserInput {
467                error: UserInputError::GasPriceUnderRGP {
468                    gas_price: self.gas_price,
469                    reference_gas_price,
470                },
471            }),
472            RunInfo {
473                gas_price_too_high: true,
474                ..
475            } => Err(IotaError::UserInput {
476                error: UserInputError::GasPriceTooHigh {
477                    max_gas_price: PROTOCOL_CONFIG.max_gas_price(),
478                },
479            }),
480            RunInfo {
481                gas_budget_too_low: true,
482                ..
483            } => Err(IotaError::UserInput {
484                error: UserInputError::GasBudgetTooLow {
485                    gas_budget: self.gas,
486                    min_budget: PROTOCOL_CONFIG.base_tx_cost_fixed() * self.gas_price,
487                },
488            }),
489            RunInfo {
490                gas_budget_too_high: true,
491                ..
492            } => Err(IotaError::UserInput {
493                error: UserInputError::GasBudgetTooHigh {
494                    gas_budget: self.gas,
495                    max_budget: PROTOCOL_CONFIG.max_tx_gas(),
496                },
497            }),
498            RunInfo {
499                enough_max_gas: false,
500                ..
501            } => Err(IotaError::UserInput {
502                error: UserInputError::GasBalanceTooLow {
503                    gas_balance: gas_balance as u128,
504                    needed_gas_amount: self.gas as u128,
505                },
506            }),
507            RunInfo {
508                wrong_gas_owner: true,
509                ..
510            } => Err(IotaError::UserInput {
511                error: UserInputError::IncorrectUserSignature {
512                    error: format!(
513                        "Object {} is owned by account address {}, but given owner/signer address is {}",
514                        gas_object.id(),
515                        sender_address,
516                        payer.initial_data.account.address,
517                    ),
518                },
519            }),
520            RunInfo {
521                enough_max_gas: true,
522                enough_to_succeed: false,
523                gas_units_too_low: false,
524                ..
525            } => {
526                self.fix_balance_and_gas_coins(payer, false);
527                Ok(ExecutionStatus::Failure {
528                    error: ExecutionFailureStatus::InsufficientCoinBalance,
529                    command: Some(0),
530                })
531            }
532            RunInfo {
533                enough_max_gas: true,
534                ..
535            } => {
536                self.fix_balance_and_gas_coins(payer, false);
537                Ok(ExecutionStatus::Failure {
538                    error: ExecutionFailureStatus::InsufficientGas,
539                    command: None,
540                })
541            }
542        };
543        (signed_txn, status)
544    }
545}
546
547impl P2PTransferGenRandomGasRandomPriceRandomSponsorship {
548    fn fix_balance_and_gas_coins(&self, sender: &mut AccountCurrent, success: bool) {
549        // collect all the coins smashed and update the balance of the one true gas
550        // coin. Gas objects are all coming from genesis which implies there is
551        // no rebate. In making things simple that does not really exercise an
552        // important aspect of the gas logic
553        let mut smash_balance = 0;
554        for _ in 1..self.gas_coins {
555            sender.current_coins.pop().expect("coin must exist");
556            smash_balance += sender.current_balances.pop().expect("balance must exist");
557        }
558        *sender.current_balances.last_mut().unwrap() += smash_balance;
559        // Fine to cast to u64 at this point, since otherwise enough_max_gas would be
560        // false since sender_balance is a u64.
561        if success {
562            *sender.current_balances.last_mut().unwrap() -=
563                self.amount + p2p_success_gas(self.gas_price);
564        } else {
565            *sender.current_balances.last_mut().unwrap() -=
566                std::cmp::min(self.gas, p2p_failure_gas(self.gas_price));
567        }
568    }
569}
570
571pub fn p2p_transfer_strategy(
572    min: u64,
573    max: u64,
574) -> impl Strategy<Value = Arc<dyn AUTransactionGen + 'static>> {
575    prop_oneof![
576        3 => any_with::<P2PTransferGenGoodGas>((min, max)).prop_map(P2PTransferGenGoodGas::arced),
577        2 => any_with::<P2PTransferGenRandomGasRandomPrice>((min, max)).prop_map(P2PTransferGenRandomGasRandomPrice::arced),
578        1 => any_with::<P2PTransferGenRandomGas>((min, max)).prop_map(P2PTransferGenRandomGas::arced),
579    ]
580}