1use 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; const P2P_FAILURE_STORAGE_USAGE: u64 = 988000 - 7600; const 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)]
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 None,
121 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 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#[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 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 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 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 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}