|
1 | 1 | use std::collections::BTreeMap; |
2 | 2 |
|
| 3 | +use apollo_versioned_constants::VersionedConstants; |
| 4 | +use rand::{Rng, SeedableRng}; |
| 5 | +use rand_chacha::ChaCha8Rng; |
3 | 6 | use rstest::rstest; |
4 | 7 | use starknet_api::block::{BlockNumber, GasPrice}; |
| 8 | +use starknet_api::versioned_constants_logic::VersionedConstantsTrait; |
5 | 9 |
|
6 | 10 | use crate::snip35::{ |
7 | 11 | compute_fee_actual, |
8 | 12 | compute_fee_proposal, |
9 | 13 | compute_fee_target, |
10 | 14 | FeeProposalInfo, |
| 15 | + PPT_DENOMINATOR, |
| 16 | + TARGET_ATTO_USD_PER_L2_GAS, |
11 | 17 | }; |
12 | 18 |
|
13 | 19 | const TEST_FEE_PROPOSAL_WINDOW_SIZE: u64 = 10; |
@@ -184,3 +190,51 @@ fn test_compute_fee_actual_lone_adversary_cannot_skew_median() { |
184 | 190 | Some(GasPrice(1_000_000)) |
185 | 191 | ); |
186 | 192 | } |
| 193 | + |
| 194 | +/// The validator's accept predicate. Must stay in sync with |
| 195 | +/// `validate_proposal::is_proposal_init_valid` SNIP-35 bounds check. |
| 196 | +fn validator_accepts(fee_actual: GasPrice, fee_proposal: GasPrice, margin_ppt: u128) -> bool { |
| 197 | + let lower = fee_actual.0.saturating_mul(PPT_DENOMINATOR) / (PPT_DENOMINATOR + margin_ppt); |
| 198 | + let upper = fee_actual.0.saturating_mul(PPT_DENOMINATOR + margin_ppt) / PPT_DENOMINATOR; |
| 199 | + fee_proposal.0 >= lower && fee_proposal.0 <= upper |
| 200 | +} |
| 201 | + |
| 202 | +#[test] |
| 203 | +fn test_malicious_high_fee_proposal_rejected() { |
| 204 | + // Upper bound for fee_actual=1_000_000 with margin=2ppt is 1_002_000. |
| 205 | + let fee_actual = GasPrice(1_000_000); |
| 206 | + assert!(validator_accepts(fee_actual, GasPrice(1_002_000), 2)); |
| 207 | + for proposal in [1_002_001u128, 1_003_000, 2_000_000, u128::MAX] { |
| 208 | + assert!(!validator_accepts(fee_actual, GasPrice(proposal), 2), "accepted {proposal}"); |
| 209 | + } |
| 210 | +} |
| 211 | + |
| 212 | +#[test] |
| 213 | +fn test_malicious_low_fee_proposal_rejected() { |
| 214 | + // Lower bound for fee_actual=1_000_000 with margin=2ppt is 998_003. |
| 215 | + let fee_actual = GasPrice(1_000_000); |
| 216 | + assert!(validator_accepts(fee_actual, GasPrice(998_003), 2)); |
| 217 | + for proposal in [998_002u128, 500_000, 1, 0] { |
| 218 | + assert!(!validator_accepts(fee_actual, GasPrice(proposal), 2), "accepted {proposal}"); |
| 219 | + } |
| 220 | +} |
| 221 | + |
| 222 | +#[test] |
| 223 | +fn test_honest_proposer_always_passes_validation_fuzzed() { |
| 224 | + // Consensus safety: whatever compute_fee_proposal produces, the validator accepts. |
| 225 | + let margin_ppt = VersionedConstants::latest_constants().fee_proposal_margin_ppt; |
| 226 | + let mut rng = ChaCha8Rng::seed_from_u64(0xDEADBEEF); |
| 227 | + for _ in 0..10_000 { |
| 228 | + let fee_actual_value = rng.gen_range(1u128..1_000_000_000_000_000_000); |
| 229 | + let strk_usd_rate = rng.gen_range(1u128..2 * 10u128.pow(18)); |
| 230 | + let fee_actual = GasPrice(fee_actual_value); |
| 231 | + let target = compute_fee_target(TARGET_ATTO_USD_PER_L2_GAS, strk_usd_rate); |
| 232 | + let oracle_result = if rng.gen_bool(0.1) { None } else { target }; |
| 233 | + let proposal = compute_fee_proposal(oracle_result, fee_actual, margin_ppt); |
| 234 | + assert!( |
| 235 | + validator_accepts(fee_actual, proposal, margin_ppt), |
| 236 | + "fee_actual={fee_actual_value} rate={strk_usd_rate} proposal={}", |
| 237 | + proposal.0 |
| 238 | + ); |
| 239 | + } |
| 240 | +} |
0 commit comments