Skip to content

Commit 838974b

Browse files
apollo_deployments: test overrides list match
1 parent 635f6e5 commit 838974b

6 files changed

Lines changed: 222 additions & 1 deletion

File tree

Cargo.lock

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

crates/apollo_deployments/resources/app_configs/replacer_state_sync_config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"state_sync_config.static_config.central_sync_client_config.central_source_config.retry_config.retry_base_millis": 30,
1111
"state_sync_config.static_config.central_sync_client_config.central_source_config.retry_config.retry_max_delay_millis": 30000,
1212
"state_sync_config.static_config.central_sync_client_config.sync_config.base_layer_propagation_sleep_duration": 10,
13+
"state_sync_config.static_config.central_sync_client_config.sync_config.blocks_before_tip_to_disable_batching": 100,
1314
"state_sync_config.static_config.central_sync_client_config.sync_config.blocks_max_stream_size": 1000,
1415
"state_sync_config.static_config.central_sync_client_config.sync_config.collect_pending_data": false,
1516
"state_sync_config.static_config.central_sync_client_config.sync_config.latest_block_poll_interval_millis": 500,

crates/apollo_deployments/resources/app_configs/state_sync_config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"state_sync_config.static_config.central_sync_client_config.central_source_config.retry_config.retry_base_millis": 30,
1010
"state_sync_config.static_config.central_sync_client_config.central_source_config.retry_config.retry_max_delay_millis": 30000,
1111
"state_sync_config.static_config.central_sync_client_config.sync_config.base_layer_propagation_sleep_duration": 10,
12+
"state_sync_config.static_config.central_sync_client_config.sync_config.blocks_before_tip_to_disable_batching": 100,
1213
"state_sync_config.static_config.central_sync_client_config.sync_config.latest_block_poll_interval_millis": 500,
1314
"state_sync_config.static_config.central_sync_client_config.sync_config.blocks_max_stream_size": 1000,
1415
"state_sync_config.static_config.central_sync_client_config.sync_config.collect_pending_data": false,

crates/apollo_node/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ apollo_state_sync_types.workspace = true
5858
futures.workspace = true
5959
metrics-exporter-prometheus.workspace = true
6060
papyrus_base_layer.workspace = true
61+
serde_json.workspace = true
6162
tokio.workspace = true
6263
tokio-util = { workspace = true, optional = true, features = ["rt"] }
6364
tracing.workspace = true
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
//! Local preset/native config-load parity check (uncommitted dev tool).
2+
//!
3+
//! Loads two config artifacts produced by the deployment pipeline for the same node:
4+
//! - a `preset` artifact (flat dotted-key) via `--config_format preset`
5+
//! - a `native` artifact (nested) via `--config_format native`
6+
//! Both are loaded with the SAME secrets file as a second `--config_file` so secret
7+
//! values match by construction, then the deserialized `SequencerNodeConfig`s are
8+
//! compared via the derived `PartialEq`.
9+
//!
10+
//! NOTE: this uses `SequencerNodeConfig::load_and_process` (deserialize only), NOT
11+
//! `load_and_validate_config`. `validate_node_config` additionally checks runtime
12+
//! environment invariants (e.g. that `/data/*` paths exist and that component URLs / k8s
13+
//! service DNS resolve) that only hold inside the pod and are irrelevant to deserialization
14+
//! parity; running them locally fails on every config. We compare what the node deserializes.
15+
//!
16+
//! Exit codes: 0 = PARITY: PASS, 1 = configs differ (prints a JSON diff), 2 = a load/
17+
//! deserialize error occurred.
18+
19+
use std::process::exit;
20+
21+
use apollo_node_config::node_config::SequencerNodeConfig;
22+
use serde_json::Value;
23+
24+
struct Args {
25+
preset_file: String,
26+
native_file: String,
27+
secrets_file: String,
28+
}
29+
30+
fn parse_args() -> Args {
31+
let mut preset_file = None;
32+
let mut native_file = None;
33+
let mut secrets_file = None;
34+
35+
let mut raw_args = std::env::args().skip(1);
36+
while let Some(flag) = raw_args.next() {
37+
let value = match raw_args.next() {
38+
Some(value) => value,
39+
None => {
40+
eprintln!("Missing value for argument {flag}");
41+
exit(2);
42+
}
43+
};
44+
match flag.as_str() {
45+
"--preset-file" => preset_file = Some(value),
46+
"--native-file" => native_file = Some(value),
47+
"--secrets-file" => secrets_file = Some(value),
48+
other => {
49+
eprintln!("Unknown argument: {other}");
50+
eprintln!(
51+
"Usage: config_parity --preset-file P --native-file N --secrets-file S"
52+
);
53+
exit(2);
54+
}
55+
}
56+
}
57+
58+
match (preset_file, native_file, secrets_file) {
59+
(Some(preset_file), Some(native_file), Some(secrets_file)) => {
60+
Args { preset_file, native_file, secrets_file }
61+
}
62+
_ => {
63+
eprintln!(
64+
"Usage: config_parity --preset-file P --native-file N --secrets-file S"
65+
);
66+
exit(2);
67+
}
68+
}
69+
}
70+
71+
fn load_preset(preset_file: &str, secrets_file: &str) -> SequencerNodeConfig {
72+
let args = vec![
73+
"config_parity",
74+
"--config_format",
75+
"preset",
76+
"--config_file",
77+
preset_file,
78+
"--config_file",
79+
secrets_file,
80+
]
81+
.into_iter()
82+
.map(String::from)
83+
.collect();
84+
match SequencerNodeConfig::load_and_process(args) {
85+
Ok(config) => config,
86+
Err(error) => {
87+
eprintln!("Failed to load PRESET config from {preset_file}: {error}");
88+
exit(2);
89+
}
90+
}
91+
}
92+
93+
fn load_native(native_file: &str, secrets_file: &str) -> SequencerNodeConfig {
94+
// Native requires the first `--config_file` to be the nested base; later files are
95+
// flat secret overrides.
96+
let args = vec![
97+
"config_parity",
98+
"--config_format",
99+
"native",
100+
"--config_file",
101+
native_file,
102+
"--config_file",
103+
secrets_file,
104+
]
105+
.into_iter()
106+
.map(String::from)
107+
.collect();
108+
match SequencerNodeConfig::load_and_process(args) {
109+
Ok(config) => config,
110+
Err(error) => {
111+
eprintln!("Failed to load NATIVE config from {native_file}: {error}");
112+
exit(2);
113+
}
114+
}
115+
}
116+
117+
/// Prints the top-level keys whose JSON values differ between the two configs.
118+
fn print_diff(preset_config: &SequencerNodeConfig, native_config: &SequencerNodeConfig) {
119+
let preset_value = serde_json::to_value(preset_config)
120+
.expect("SequencerNodeConfig should serialize to JSON");
121+
let native_value = serde_json::to_value(native_config)
122+
.expect("SequencerNodeConfig should serialize to JSON");
123+
124+
let empty = serde_json::Map::new();
125+
let preset_object = preset_value.as_object().unwrap_or(&empty);
126+
let native_object = native_value.as_object().unwrap_or(&empty);
127+
128+
let mut all_keys: Vec<&String> =
129+
preset_object.keys().chain(native_object.keys()).collect();
130+
all_keys.sort_unstable();
131+
all_keys.dedup();
132+
133+
let null = Value::Null;
134+
for key in all_keys {
135+
let preset_field = preset_object.get(key).unwrap_or(&null);
136+
let native_field = native_object.get(key).unwrap_or(&null);
137+
if preset_field != native_field {
138+
eprintln!("--- DIFF in top-level field `{key}` ---");
139+
eprintln!(
140+
" preset: {}",
141+
serde_json::to_string(preset_field).unwrap_or_default()
142+
);
143+
eprintln!(
144+
" native: {}",
145+
serde_json::to_string(native_field).unwrap_or_default()
146+
);
147+
}
148+
}
149+
}
150+
151+
fn main() {
152+
let args = parse_args();
153+
let preset_config = load_preset(&args.preset_file, &args.secrets_file);
154+
let native_config = load_native(&args.native_file, &args.secrets_file);
155+
156+
if preset_config == native_config {
157+
println!("PARITY: PASS");
158+
exit(0);
159+
}
160+
161+
eprintln!("PARITY: FAIL - loaded SequencerNodeConfig values differ");
162+
print_diff(&preset_config, &native_config);
163+
exit(1);
164+
}

crates/apollo_reverts/src/lib.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,70 @@ use apollo_storage::state_commitment_infos::StateCommitmentInfosStorageWriter;
1919
use apollo_storage::StorageWriter;
2020
use futures::future::pending;
2121
use futures::never::Never;
22-
use serde::{Deserialize, Serialize};
22+
use std::fmt;
23+
24+
use serde::de::{self, Visitor};
25+
use serde::{Deserialize, Deserializer, Serialize};
2326
use starknet_api::block::BlockNumber;
2427
use tracing::info;
2528
use validator::Validate;
2629

2730
#[derive(Clone, Debug, Serialize, Deserialize, Validate, PartialEq)]
2831
pub struct RevertConfig {
32+
#[serde(deserialize_with = "deserialize_revert_block_number")]
2933
pub revert_up_to_and_including: BlockNumber,
3034
pub should_revert: bool,
3135
}
3236

37+
/// Deserialize `revert_up_to_and_including`, tolerating a floating-point representation of the
38+
/// value.
39+
///
40+
/// The default is the `u64::MAX` "never revert" sentinel. Configs assembled via jsonnet (the
41+
/// `native` config format) render every number as an IEEE-754 double, so `u64::MAX`
42+
/// (18446744073709551615) is emitted as the nearest double, `2^64` (18446744073709551616), which
43+
/// overflows `u64` and would otherwise fail to deserialize. We accept a float and saturating-cast
44+
/// it back to `u64`, so a config produced by jsonnet deserializes to the same `BlockNumber` as the
45+
/// legacy flat (`preset`) path, which carries the exact `u64::MAX` integer. Plain integer values
46+
/// (the common case, including real revert heights) take the `u64` branch unchanged.
47+
fn deserialize_revert_block_number<'de, D>(deserializer: D) -> Result<BlockNumber, D::Error>
48+
where
49+
D: Deserializer<'de>,
50+
{
51+
struct RevertBlockNumberVisitor;
52+
53+
impl<'de> Visitor<'de> for RevertBlockNumberVisitor {
54+
type Value = BlockNumber;
55+
56+
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57+
formatter.write_str(
58+
"a block number; jsonnet renders the u64::MAX sentinel as the 2^64 value, which \
59+
deserializers may present as u128 or f64",
60+
)
61+
}
62+
63+
fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
64+
Ok(BlockNumber(value))
65+
}
66+
67+
// jsonnet's f64 rounding turns u64::MAX into 2^64, which serde_json surfaces as a u128.
68+
// Saturate anything past u64::MAX back to the sentinel.
69+
fn visit_u128<E: de::Error>(self, value: u128) -> Result<Self::Value, E> {
70+
Ok(BlockNumber(u64::try_from(value).unwrap_or(u64::MAX)))
71+
}
72+
73+
fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
74+
Ok(BlockNumber(u64::try_from(value).unwrap_or(0)))
75+
}
76+
77+
// Saturating float-to-int cast: 2^64 (and larger) maps to u64::MAX.
78+
fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
79+
Ok(BlockNumber(value as u64))
80+
}
81+
}
82+
83+
deserializer.deserialize_any(RevertBlockNumberVisitor)
84+
}
85+
3386
impl Default for RevertConfig {
3487
fn default() -> Self {
3588
Self {

0 commit comments

Comments
 (0)