|
| 1 | +use rand::{Rng, SeedableRng}; |
| 2 | +use rand_chacha::ChaCha8Rng; |
1 | 3 | use rstest::rstest; |
2 | 4 | use starknet_api::block::GasPrice; |
3 | 5 |
|
4 | | -use crate::snip35::{compute_fee_actual, compute_fee_proposal, compute_fee_target}; |
| 6 | +use crate::snip35::{ |
| 7 | + compute_fee_actual, |
| 8 | + compute_fee_proposal, |
| 9 | + compute_fee_target, |
| 10 | + FEE_PROPOSAL_MARGIN_PPT, |
| 11 | + ORACLE_L2_GAS_FLOOR_MAX_FRI, |
| 12 | + ORACLE_L2_GAS_FLOOR_MIN_FRI, |
| 13 | + PPT_DENOMINATOR, |
| 14 | + TARGET_ATTO_USD_PER_L2_GAS, |
| 15 | +}; |
5 | 16 |
|
6 | 17 | const FRI_DECIMALS_SCALE: u128 = 10u128.pow(18); |
7 | 18 | const FLOOR_MIN_FRI: u128 = 100; |
@@ -134,3 +145,55 @@ fn test_compute_fee_actual_lone_adversary_cannot_skew_median() { |
134 | 145 | window.push(GasPrice(u128::MAX / 2)); |
135 | 146 | assert_eq!(compute_fee_actual(&window, 10), Some(GasPrice(1_000_000))); |
136 | 147 | } |
| 148 | + |
| 149 | +/// The validator's accept predicate. Must stay in sync with |
| 150 | +/// `validate_proposal::is_proposal_init_valid` SNIP-35 bounds check. |
| 151 | +fn validator_accepts(fee_actual: GasPrice, fee_proposal: GasPrice, margin_ppt: u128) -> bool { |
| 152 | + let lower = fee_actual.0.saturating_mul(PPT_DENOMINATOR) / (PPT_DENOMINATOR + margin_ppt); |
| 153 | + let upper = fee_actual.0.saturating_mul(PPT_DENOMINATOR + margin_ppt) / PPT_DENOMINATOR; |
| 154 | + fee_proposal.0 >= lower && fee_proposal.0 <= upper |
| 155 | +} |
| 156 | + |
| 157 | +#[test] |
| 158 | +fn test_malicious_high_fee_proposal_rejected() { |
| 159 | + // Upper bound for fee_actual=1_000_000 with margin=2ppt is 1_002_000. |
| 160 | + let fee_actual = GasPrice(1_000_000); |
| 161 | + assert!(validator_accepts(fee_actual, GasPrice(1_002_000), 2)); |
| 162 | + for proposal in [1_002_001u128, 1_003_000, 2_000_000, u128::MAX] { |
| 163 | + assert!(!validator_accepts(fee_actual, GasPrice(proposal), 2), "accepted {proposal}"); |
| 164 | + } |
| 165 | +} |
| 166 | + |
| 167 | +#[test] |
| 168 | +fn test_malicious_low_fee_proposal_rejected() { |
| 169 | + // Lower bound for fee_actual=1_000_000 with margin=2ppt is 998_003. |
| 170 | + let fee_actual = GasPrice(1_000_000); |
| 171 | + assert!(validator_accepts(fee_actual, GasPrice(998_003), 2)); |
| 172 | + for proposal in [998_002u128, 500_000, 1, 0] { |
| 173 | + assert!(!validator_accepts(fee_actual, GasPrice(proposal), 2), "accepted {proposal}"); |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +#[test] |
| 178 | +fn test_honest_proposer_always_passes_validation_fuzzed() { |
| 179 | + // Consensus safety: whatever compute_fee_proposal produces, the validator accepts. |
| 180 | + let mut rng = ChaCha8Rng::seed_from_u64(0xDEADBEEF); |
| 181 | + for _ in 0..10_000 { |
| 182 | + let fee_actual_value = rng.gen_range(1u128..1_000_000_000_000_000_000); |
| 183 | + let strk_usd_rate = rng.gen_range(1u128..2 * 10u128.pow(18)); |
| 184 | + let fee_actual = GasPrice(fee_actual_value); |
| 185 | + let target = compute_fee_target( |
| 186 | + TARGET_ATTO_USD_PER_L2_GAS, |
| 187 | + strk_usd_rate, |
| 188 | + ORACLE_L2_GAS_FLOOR_MIN_FRI, |
| 189 | + ORACLE_L2_GAS_FLOOR_MAX_FRI, |
| 190 | + ); |
| 191 | + let oracle_result = if rng.gen_bool(0.1) { None } else { Some(target) }; |
| 192 | + let proposal = compute_fee_proposal(oracle_result, fee_actual, FEE_PROPOSAL_MARGIN_PPT); |
| 193 | + assert!( |
| 194 | + validator_accepts(fee_actual, proposal, FEE_PROPOSAL_MARGIN_PPT), |
| 195 | + "fee_actual={fee_actual_value} rate={strk_usd_rate} proposal={}", |
| 196 | + proposal.0 |
| 197 | + ); |
| 198 | + } |
| 199 | +} |
0 commit comments