1use 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#[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#[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#[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#[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#[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 None,
120 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 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#[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 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 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 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 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}