Skip to main content

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