Skip to content

Commit 1c2514e

Browse files
apollo_consensus_orchestrator: add SNIP-35 proposer-validator symmetry tests
1 parent dff0a9f commit 1c2514e

3 files changed

Lines changed: 58 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/apollo_consensus_orchestrator/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ metrics-exporter-prometheus.workspace = true
7777
mockall.workspace = true
7878
mockito.workspace = true
7979
num-bigint.workspace = true
80+
rand.workspace = true
81+
rand_chacha.workspace = true
8082
rstest.workspace = true
8183
serde_json.workspace = true
8284

crates/apollo_consensus_orchestrator/src/snip35/test.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
use std::collections::BTreeMap;
22

3+
use apollo_versioned_constants::VersionedConstants;
4+
use rand::{Rng, SeedableRng};
5+
use rand_chacha::ChaCha8Rng;
36
use rstest::rstest;
47
use starknet_api::block::{BlockNumber, GasPrice};
8+
use starknet_api::versioned_constants_logic::VersionedConstantsTrait;
59

610
use crate::snip35::{
711
compute_fee_actual,
812
compute_fee_proposal,
913
compute_fee_target,
1014
FeeProposalInfo,
15+
PPT_DENOMINATOR,
16+
TARGET_ATTO_USD_PER_L2_GAS,
1117
};
1218

1319
const TEST_FEE_PROPOSAL_WINDOW_SIZE: u64 = 10;
@@ -184,3 +190,51 @@ fn test_compute_fee_actual_lone_adversary_cannot_skew_median() {
184190
Some(GasPrice(1_000_000))
185191
);
186192
}
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

Comments
 (0)