transaction_fuzzer/account_universe/
transfer_gen.rs

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