1use 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; const P2P_FAILURE_STORAGE_USAGE: u64 = 988000 - 7600; const INSUFFICIENT_GAS_UNITS_THRESHOLD: u64 = 2;
37
38static PROTOCOL_CONFIG: Lazy<ProtocolConfig> =
39 Lazy::new(ProtocolConfig::get_for_max_version_UNSAFE);
40
41#[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#[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#[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#[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#[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 None,
123 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 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#[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 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 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 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 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}