Skip to content

Commit 5c08319

Browse files
fix: isolate prover queues by network
1 parent 8de3b8f commit 5c08319

12 files changed

Lines changed: 397 additions & 148 deletions

File tree

forester-utils/src/utils.rs

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use light_client::{
55
rpc::{Rpc, RpcError},
66
};
77
use solana_sdk::{signature::Signer, transaction::Transaction};
8-
use tokio::time::sleep;
98
use tracing::{error, warn};
109

1110
use crate::error::ForesterUtilsError;
@@ -30,10 +29,7 @@ pub async fn airdrop_lamports<R: Rpc>(
3029

3130
pub async fn wait_for_indexer<R: Rpc>(rpc: &R) -> Result<(), ForesterUtilsError> {
3231
let rpc_slot = rpc.get_slot().await?;
33-
34-
let indexer_slot = rpc.indexer()?.get_indexer_slot(None).await;
35-
36-
let mut indexer_slot = match indexer_slot {
32+
let indexer_slot = match rpc.indexer()?.get_indexer_slot(None).await {
3733
Ok(slot) => slot,
3834
Err(e) => {
3935
error!("failed to get indexer slot from indexer: {:?}", e);
@@ -43,32 +39,19 @@ pub async fn wait_for_indexer<R: Rpc>(rpc: &R) -> Result<(), ForesterUtilsError>
4339
}
4440
};
4541

46-
let max_attempts = 100;
47-
let mut attempts = 0;
48-
49-
while rpc_slot > indexer_slot {
50-
if attempts >= max_attempts {
51-
return Err(ForesterUtilsError::Indexer(
52-
"Maximum attempts reached waiting for indexer to catch up".into(),
53-
));
54-
}
55-
56-
if rpc_slot - indexer_slot > 50 {
57-
warn!(
58-
"indexer is behind {} slots (rpc_slot: {}, indexer_slot: {})",
59-
rpc_slot - indexer_slot,
60-
rpc_slot,
61-
indexer_slot
62-
);
63-
}
64-
65-
sleep(std::time::Duration::from_millis(1000)).await;
66-
indexer_slot = rpc.indexer()?.get_indexer_slot(None).await.map_err(|e| {
67-
error!("failed to get indexer slot from indexer: {:?}", e);
68-
ForesterUtilsError::Indexer("Failed to get indexer slot".into())
69-
})?;
70-
71-
attempts += 1;
42+
let max_lag_slots = std::env::var("INDEXER_MAX_LAG_SLOTS")
43+
.ok()
44+
.and_then(|value| value.parse::<u64>().ok())
45+
.unwrap_or(100);
46+
let lag = rpc_slot.saturating_sub(indexer_slot);
47+
if lag > max_lag_slots {
48+
warn!(
49+
lag,
50+
max_lag_slots, rpc_slot, indexer_slot, "indexer freshness gate rejected proof work"
51+
);
52+
return Err(ForesterUtilsError::Indexer(format!(
53+
"Indexer is behind {lag} slots (maximum allowed: {max_lag_slots})"
54+
)));
7255
}
7356
Ok(())
7457
}

forester/src/epoch_manager.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3176,6 +3176,8 @@ impl<R: Rpc + Indexer> EpochManager<R> {
31763176
.external_services
31773177
.prover_max_wait_time
31783178
.unwrap_or(Duration::from_secs(600)),
3179+
network: std::env::var("FORESTER_NETWORK")
3180+
.unwrap_or_else(|_| "default".to_string()),
31793181
}),
31803182
ops_cache: self.ops_cache.clone(),
31813183
epoch_phases: epoch_info.phases.clone(),

forester/src/processor/v1/helpers.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,16 @@ pub async fn fetch_proofs_and_create_instructions<R: Rpc>(
9696
};
9797

9898
let rpc = pool.get_connection().await?;
99-
if let Err(e) = wait_for_indexer(&*rpc).await {
99+
wait_for_indexer(&*rpc).await.map_err(|e| {
100100
if should_emit_rate_limited_warning("v1_wait_for_indexer", Duration::from_secs(30)) {
101101
warn!(
102102
event = "v1_wait_for_indexer_error",
103103
error = %e,
104-
"Indexer not fully caught up, but proceeding anyway"
104+
"Skipping V1 proof work because the indexer is not fresh"
105105
);
106106
}
107-
}
107+
e
108+
})?;
108109

109110
let address_proofs = if let Some((merkle_tree, addresses)) = address_data {
110111
let total_addresses = addresses.len();

forester/src/processor/v2/common.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ pub struct ProverConfig {
7979
pub api_key: Option<String>,
8080
pub polling_interval: Duration,
8181
pub max_wait_time: Duration,
82+
pub network: String,
8283
}
8384

8485
#[derive(Debug)]

forester/src/processor/v2/processor.rs

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::{
44
};
55

66
use anyhow::anyhow;
7-
use forester_utils::{forester_epoch::EpochPhases, utils::wait_for_indexer};
7+
use forester_utils::forester_epoch::EpochPhases;
88
use light_client::rpc::Rpc;
99
use light_compressed_account::QueueType;
1010
use solana_sdk::pubkey::Pubkey;
@@ -14,7 +14,6 @@ use tracing::{debug, info, warn};
1414
use crate::{
1515
epoch_manager::{CircuitMetrics, ProcessingMetrics},
1616
errors::ForesterError,
17-
logging::should_emit_rate_limited_warning,
1817
processor::v2::{
1918
batch_job_builder::BatchJobBuilder,
2019
common::WorkerPool,
@@ -136,6 +135,20 @@ where
136135
self.worker_pool = Some(WorkerPool { job_tx });
137136
}
138137

138+
if self.cached_state.is_some() {
139+
let onchain_root = self.strategy.fetch_onchain_root(&self.context).await?;
140+
if onchain_root != self.current_root {
141+
warn!(
142+
tree = %self.context.merkle_tree,
143+
expected_root_prefix = ?&self.current_root[..4],
144+
onchain_root_prefix = ?&onchain_root[..4],
145+
"Discarding cached proof state because the on-chain root advanced"
146+
);
147+
self.current_root = onchain_root;
148+
self.clear_cache().await;
149+
}
150+
}
151+
139152
if let Some(cached) = self.cached_state.take() {
140153
let actual_available = self
141154
.strategy
@@ -188,26 +201,6 @@ where
188201
);
189202
}
190203

191-
{
192-
let rpc = self.context.rpc_pool.get_connection().await?;
193-
if let Err(e) = wait_for_indexer(&*rpc).await {
194-
if should_emit_rate_limited_warning("v2_wait_for_indexer", Duration::from_secs(30))
195-
{
196-
warn!(
197-
event = "wait_for_indexer_error",
198-
error = %e,
199-
"wait_for_indexer error (proceeding anyway)"
200-
);
201-
} else {
202-
debug!(
203-
event = "wait_for_indexer_error_suppressed",
204-
error = %e,
205-
"Suppressing repeated wait_for_indexer warning"
206-
);
207-
}
208-
}
209-
}
210-
211204
let queue_data = match self
212205
.strategy
213206
.fetch_queue_data(&self.context, fetch_batches, self.zkp_batch_size)
@@ -217,14 +210,6 @@ where
217210
None => return Ok(ProcessingResult::default()),
218211
};
219212

220-
if self.current_root == [0u8; 32] || queue_data.initial_root == self.current_root {
221-
let total_batches = queue_data.num_batches;
222-
let process_now = total_batches.min(self.context.max_batches_per_tree);
223-
return self
224-
.process_batches(queue_data, 0, process_now, total_batches)
225-
.await;
226-
}
227-
228213
let onchain_root = self.strategy.fetch_onchain_root(&self.context).await?;
229214
match reconcile_roots(self.current_root, queue_data.initial_root, onchain_root) {
230215
RootReconcileDecision::Proceed => {

forester/src/processor/v2/proof_worker.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,19 +139,22 @@ impl ProofClients {
139139
config.polling_interval,
140140
config.max_wait_time,
141141
config.api_key.clone(),
142-
),
142+
)
143+
.with_network(config.network.clone()),
143144
nullify_client: ProofClient::with_config(
144145
config.update_url.clone(),
145146
config.polling_interval,
146147
config.max_wait_time,
147148
config.api_key.clone(),
148-
),
149+
)
150+
.with_network(config.network.clone()),
149151
address_append_client: ProofClient::with_config(
150152
config.address_append_url.clone(),
151153
config.polling_interval,
152154
config.max_wait_time,
153155
config.api_key.clone(),
154-
),
156+
)
157+
.with_network(config.network.clone()),
155158
}
156159
}
157160

forester/src/processor/v2/root_guard.rs

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,29 +29,15 @@ pub fn reconcile_roots(
2929
indexer_root: [u8; 32],
3030
onchain_root: [u8; 32],
3131
) -> RootReconcileDecision {
32-
if expected_root == [0u8; 32] {
33-
// Uninitialized expected root — proceed but adopt the indexer root.
34-
// Validate that indexer and on-chain agree when possible.
35-
if indexer_root != onchain_root {
36-
tracing::warn!(
37-
"Proceeding with uninitialized expected root, but indexer root ({:?}) != onchain root ({:?}). Indexer may be stale.",
38-
&indexer_root[..4],
39-
&onchain_root[..4],
40-
);
41-
}
32+
if indexer_root == onchain_root && expected_root == onchain_root {
4233
return RootReconcileDecision::Proceed;
4334
}
44-
if indexer_root == expected_root {
45-
return RootReconcileDecision::Proceed;
46-
}
47-
48-
if onchain_root == expected_root {
49-
return RootReconcileDecision::WaitForIndexer;
50-
}
51-
5235
if indexer_root == onchain_root {
5336
return RootReconcileDecision::ResetToOnchainAndProceed(onchain_root);
5437
}
38+
if expected_root == onchain_root {
39+
return RootReconcileDecision::WaitForIndexer;
40+
}
5541

5642
RootReconcileDecision::ResetToOnchainAndStop(onchain_root)
5743
}
@@ -93,18 +79,26 @@ mod tests {
9379
}
9480

9581
#[test]
96-
fn proceeds_when_expected_is_zero() {
82+
fn cold_start_stops_when_indexer_and_chain_disagree() {
9783
assert_eq!(
9884
reconcile_roots(root(0), root(1), root(2)),
99-
RootReconcileDecision::Proceed
85+
RootReconcileDecision::ResetToOnchainAndStop(root(2))
86+
);
87+
}
88+
89+
#[test]
90+
fn cold_start_proceeds_only_when_indexer_matches_chain() {
91+
assert_eq!(
92+
reconcile_roots(root(0), root(2), root(2)),
93+
RootReconcileDecision::ResetToOnchainAndProceed(root(2))
10094
);
10195
}
10296

10397
#[test]
104-
fn proceeds_when_expected_matches_indexer() {
98+
fn stops_when_expected_matches_stale_indexer() {
10599
assert_eq!(
106100
reconcile_roots(root(9), root(9), root(8)),
107-
RootReconcileDecision::Proceed
101+
RootReconcileDecision::ResetToOnchainAndStop(root(8))
108102
);
109103
}
110104

prover/client/src/proof_client.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ pub struct ProofClient {
6363
polling_interval: Duration,
6464
max_wait_time: Duration,
6565
api_key: Option<String>,
66+
network: Option<String>,
6667
initial_poll_delay: Duration,
6768
}
6869

@@ -74,6 +75,7 @@ impl ProofClient {
7475
polling_interval: Duration::from_millis(DEFAULT_POLLING_INTERVAL_MS),
7576
max_wait_time: Duration::from_secs(DEFAULT_MAX_WAIT_TIME_SECS),
7677
api_key: None,
78+
network: None,
7779
initial_poll_delay: Duration::from_millis(INITIAL_POLL_DELAY_SMALL_CIRCUIT_MS),
7880
}
7981
}
@@ -97,6 +99,7 @@ impl ProofClient {
9799
polling_interval,
98100
max_wait_time,
99101
api_key,
102+
network: None,
100103
initial_poll_delay,
101104
}
102105
}
@@ -115,10 +118,16 @@ impl ProofClient {
115118
polling_interval,
116119
max_wait_time,
117120
api_key,
121+
network: None,
118122
initial_poll_delay,
119123
}
120124
}
121125

126+
pub fn with_network(mut self, network: String) -> Self {
127+
self.network = Some(network);
128+
self
129+
}
130+
122131
pub async fn submit_proof_async(
123132
&self,
124133
inputs_json: String,
@@ -242,6 +251,9 @@ impl ProofClient {
242251
if let Some(api_key) = &self.api_key {
243252
request = request.header("X-API-Key", api_key);
244253
}
254+
if let Some(network) = &self.network {
255+
request = request.header("X-Light-Network", network);
256+
}
245257

246258
request
247259
.body(inputs_json.to_string())

0 commit comments

Comments
 (0)