Skip to content

Commit

Permalink
test: Add positive and negative tests for negative fees
Browse files Browse the repository at this point in the history
In particular, this commit adds a new arbitrary implementation for
`PrimitiveWitness`; this one takes the transaction fee as argument.
This transaction fee can be negative and even (as far as the arbitrary
implementation is concerned) invalid.

The tests do not pass yet. Future changes to `NativeCurrency` and
`SingleProof` that accomodate negative fees will fix these failures.
The tests are there for test-driven development.
  • Loading branch information
aszepieniec authored and Sword-Smith committed Dec 19, 2024
1 parent 6f2ba0a commit 4dbb0f4
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 1 deletion.
98 changes: 98 additions & 0 deletions src/models/blockchain/transaction/primitive_witness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,104 @@ mod test {
)
.boxed()
}

/// A strategy for primitive witnesses with 1 input, 2 outputs, and the
/// given fee. The fee can be negative or even an invalid amount
/// (greater than the maximum number of nau).
pub(crate) fn arbitrary_with_fee(fee: NeptuneCoins) -> BoxedStrategy<Self> {
let total_amount_strategy = if fee.is_negative() {
(-fee).to_nau().try_into().unwrap()..(u128::MAX >> 1)
} else {
std::convert::TryInto::<u128>::try_into(fee.to_nau()).unwrap()..(u128::MAX >> 1)
};
let num_outputs = 2;
(
total_amount_strategy,
arb::<Digest>(),
vec(arb::<Digest>(), num_outputs),
arb::<Timestamp>(),
)
.prop_flat_map(
move |(amount, input_address_seed, output_seeds, mut timestamp)| {
while timestamp + COINBASE_TIME_LOCK_PERIOD < timestamp {
timestamp = Timestamp::millis(timestamp.to_millis() >> 1);
}

let total_amount = NeptuneCoins::from_raw_u128(amount);
let (input_utxos, input_lock_scripts_and_witnesses) =
Self::transaction_inputs_from_address_seeds_and_amounts(
&[input_address_seed],
&[total_amount],
);

let output_utxos = if fee.is_negative() {
let mut timelocked_amount = -fee;
timelocked_amount.div_two();
let timelocked_output = Utxo::new_native_currency(
LockScript::hash_lock(output_seeds[0]),
timelocked_amount,
)
.with_time_lock(timestamp + COINBASE_TIME_LOCK_PERIOD);

let liquid_amount = NeptuneCoins::from_raw_u128(
u128::try_from(total_amount.to_nau())
.unwrap()
.checked_sub(
u128::try_from(timelocked_amount.to_nau()).unwrap(),
)
.unwrap()
.checked_add(u128::try_from((-fee).to_nau()).unwrap())
.unwrap(),
);
let liquid_output = Utxo::new_native_currency(
LockScript::hash_lock(output_seeds[0]),
liquid_amount,
);

vec![timelocked_output, liquid_output]
} else {
let mut first_amount = fee;
first_amount.div_two();
while total_amount
.checked_sub(&fee)
.unwrap()
.checked_sub(&first_amount)
.is_none()
{
first_amount.div_two();
}
let first_output = Utxo::new_native_currency(
LockScript::hash_lock(output_seeds[0]),
first_amount,
)
.with_time_lock(timestamp + COINBASE_TIME_LOCK_PERIOD);

let second_amount = total_amount
.checked_sub(&first_amount)
.unwrap()
.checked_sub(&fee)
.unwrap();
let second_output = Utxo::new_native_currency(
LockScript::hash_lock(output_seeds[1]),
second_amount,
);

vec![first_output, second_output]
};

Self::arbitrary_primitive_witness_with_timestamp_and(
&input_utxos,
&input_lock_scripts_and_witnesses,
&output_utxos,
&[],
fee,
None,
timestamp,
)
},
)
.boxed()
}
}

#[proptest(cases = 5, async = "tokio")]
Expand Down
37 changes: 37 additions & 0 deletions src/models/blockchain/type_scripts/native_currency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,7 @@ pub mod test {
use crate::models::blockchain::transaction::transaction_kernel::TransactionKernelModifier;
use crate::models::blockchain::transaction::utxo::Utxo;
use crate::models::blockchain::transaction::PublicAnnouncement;
use crate::models::blockchain::type_scripts::neptune_coins::test::invalid_amount;
use crate::models::blockchain::type_scripts::time_lock::arbitrary_primitive_witness_with_active_timelocks;
use crate::models::blockchain::type_scripts::time_lock::TimeLock;
use crate::models::proof_abstractions::tasm::program::test::consensus_program_negative_test;
Expand Down Expand Up @@ -1440,4 +1441,40 @@ pub mod test {
});
prop_assert!(result.is_err());
}

#[proptest]
fn fee_can_be_positive(
#[strategy(arb::<NeptuneCoins>())] _fee: NeptuneCoins,
#[strategy(PrimitiveWitness::arbitrary_with_fee(#_fee))]
primitive_witness: PrimitiveWitness,
) {
assert_both_rust_and_tasm_halt_gracefully(NativeCurrencyWitness::from(primitive_witness))?;
}

#[proptest]
fn fee_can_be_negative(
#[strategy(arb::<NeptuneCoins>())] _fee: NeptuneCoins,
#[strategy(PrimitiveWitness::arbitrary_with_fee(-#_fee))]
primitive_witness: PrimitiveWitness,
) {
assert_both_rust_and_tasm_halt_gracefully(NativeCurrencyWitness::from(primitive_witness))?;
}

#[proptest]
fn positive_fee_cannot_exceed_max_nau(
#[strategy(invalid_amount())] _fee: NeptuneCoins,
#[strategy(PrimitiveWitness::arbitrary_with_fee(#_fee))]
primitive_witness: PrimitiveWitness,
) {
assert_both_rust_and_tasm_fail(NativeCurrencyWitness::from(primitive_witness));
}

#[proptest]
fn negative_fee_cannot_exceed_negative_max_nau(
#[strategy(invalid_amount())] _fee: NeptuneCoins,
#[strategy(PrimitiveWitness::arbitrary_with_fee(-#_fee))]
primitive_witness: PrimitiveWitness,
) {
assert_both_rust_and_tasm_fail(NativeCurrencyWitness::from(primitive_witness));
}
}
16 changes: 15 additions & 1 deletion src/models/blockchain/type_scripts/neptune_coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ impl<'a> Arbitrary<'a> for NeptuneCoins {
}

#[cfg(test)]
mod amount_tests {
pub(crate) mod test {
use std::ops::ShlAssign;
use std::str::FromStr;

Expand All @@ -457,6 +457,8 @@ mod amount_tests {
use itertools::Itertools;
use num_bigint::Sign;
use num_traits::FromPrimitive;
use proptest::prelude::BoxedStrategy;
use proptest::prelude::Strategy;
use proptest_arbitrary_interop::arb;
use rand::thread_rng;
use rand::Rng;
Expand All @@ -465,6 +467,18 @@ mod amount_tests {

use super::*;

impl NeptuneCoins {
pub(crate) fn from_raw_u128(int: u128) -> Self {
Self(int)
}
}

pub(crate) fn invalid_amount() -> BoxedStrategy<NeptuneCoins> {
((NeptuneCoins::MAX_NAU + 1)..(u128::MAX >> 1))
.prop_map(NeptuneCoins)
.boxed()
}

#[test]
fn test_slice_conversion() {
let mut rng = thread_rng();
Expand Down

0 comments on commit 4dbb0f4

Please sign in to comment.