Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 2026-04-20
- #2764 Celestia adapter: re-adds block integrity verification on fetch. Replaces the previously-reverted boolean toggle (PR #2489 / reverted in PR #2520) with a
`verify_on_fetch_mode` enum accepting `"off"` (default), `"log_error"` ,or `"return_error"`. `log_error` runs verification and logs via `tracing::error!` on failure while still returning the block, enabling staged rollouts without breaking the node.
The underlying verifier bug fix from PR #2525 is already in place.
Comment thread
citizen-stig marked this conversation as resolved.

# 2026-04-16
- #2746 Removes re-export of `DaSyncState` and `SyncStatus` from sov-modules-api. Please use `sov-rollup-interface` directly
- #2744 **Manual intervention might be needed**: Adds `serde(deny_unknown_fields)`, which can fail rollup at startup if genesis config is not tidy.
Expand Down
59 changes: 58 additions & 1 deletion crates/adapters/celestia/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ pub struct CelestiaConfig {
/// Default: 30.
#[serde(default = "default_background_stat_polling_interval_secs")]
pub background_stat_polling_interval_secs: u64,
/// Controls how fetched Celestia blocks are verified against their Data Availability
/// Header before `get_block_at` returns. See [`VerifyOnFetchMode`].
#[serde(default)]
pub verify_on_fetch_mode: VerifyOnFetchMode,
/// See [`sov_rollup_interface::node::da::DaService::safe_lead_time`].
#[serde(default = "default_safe_lead_time_ms")]
pub safe_lead_time_ms: u64,
Expand Down Expand Up @@ -137,6 +141,7 @@ impl fmt::Debug for CelestiaConfig {
"background_stat_polling_interval_secs",
&self.background_stat_polling_interval_secs,
)
.field("verify_on_fetch_mode", &self.verify_on_fetch_mode)
.field("safe_lead_time_ms", &self.safe_lead_time_ms)
.field("tx_priority", &self.tx_priority)
.field("backoff_min_delay_ms", &self.backoff_min_delay_ms)
Expand Down Expand Up @@ -165,6 +170,26 @@ impl From<TxPriority> for celestia_client::tx::TxPriority {
}
}

/// Controls how fetched Celestia blocks are verified against their Data Availability Header
/// before `get_block_at` returns.
///
/// Verification guards against silent consensus-breaking forks when the connected RPC node
/// returns corrupted namespace data, at the cost of per-block overhead.
#[derive(
Debug, Clone, Copy, Default, PartialEq, serde::Deserialize, serde::Serialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum VerifyOnFetchMode {
/// Skip verification entirely.
#[default]
Off,
/// Run verification; on failure log via `tracing::error!` and return the block anyway.
/// Recommended during staged rollouts when visibility matters more than hard enforcement.
LogError,
/// Run verification; on failure propagate the error so `get_block_at` fails.
ReturnError,
}

impl CelestiaConfig {
/// Absolutely minimal config for client that is capable of reading
pub fn minimal(rpc_url: String) -> Self {
Expand All @@ -179,6 +204,7 @@ impl CelestiaConfig {
api_request_timeout_secs: default_api_request_timeout_secs(),
tx_status_polling_millis: default_tx_status_polling_millis(),
background_stat_polling_interval_secs: default_background_stat_polling_interval_secs(),
verify_on_fetch_mode: VerifyOnFetchMode::default(),
safe_lead_time_ms: default_safe_lead_time_ms(),
tx_priority: default_tx_priority(),
backoff_min_delay_ms: default_min_delay_ms(),
Expand Down Expand Up @@ -335,7 +361,7 @@ pub(crate) fn default_background_stat_polling_interval_secs() -> u64 {

#[cfg(test)]
mod tests {
use super::{validate_rpc_url, CelestiaConfig, GrpcEndpointConfig};
use super::{validate_rpc_url, CelestiaConfig, GrpcEndpointConfig, VerifyOnFetchMode};

const RPC_ENV_VAR: &str = "SOV_CELESTIA_RPC_URL";
const GRPC_ENV_VAR: &str = "SOV_CELESTIA_GRPC_URL";
Expand Down Expand Up @@ -524,4 +550,35 @@ mod tests {
let deserialized: CelestiaConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config, deserialized);
}

#[test]
fn verify_on_fetch_mode_defaults_to_off() {
let _rpc_guard = EnvVarGuard::set(RPC_ENV_VAR, Some("ws://env-rpc:26658"));
let _grpc_guard = EnvVarGuard::set(GRPC_ENV_VAR, None);

let config = deserialize_config("{}").unwrap();
assert_eq!(config.verify_on_fetch_mode, VerifyOnFetchMode::Off);
}

#[test]
fn verify_on_fetch_mode_accepts_all_variants() {
let cases = [
(r#"{"verify_on_fetch_mode":"off"}"#, VerifyOnFetchMode::Off),
(
r#"{"verify_on_fetch_mode":"log_error"}"#,
VerifyOnFetchMode::LogError,
),
(
r#"{"verify_on_fetch_mode":"return_error"}"#,
VerifyOnFetchMode::ReturnError,
),
];
for (json, expected) in cases {
let _rpc_guard = EnvVarGuard::set(RPC_ENV_VAR, Some("ws://env-rpc:26658"));
let _grpc_guard = EnvVarGuard::set(GRPC_ENV_VAR, None);

let config = deserialize_config(json).expect(json);
assert_eq!(config.verify_on_fetch_mode, expected);
}
}
}
56 changes: 53 additions & 3 deletions crates/adapters/celestia/src/da_service/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#[cfg(test)]
mod tests;

pub use crate::config::CelestiaConfig;
pub use crate::config::{CelestiaConfig, VerifyOnFetchMode};
use crate::metrics::client::{
BlobGetAllMeasurement, GetBlockHeaderMeasurement, GetChainHeadMeasurement,
GetNamespaceDataMeasurement, HeaderSyncStateMeasurement, StateBalanceForAddressMeasurement,
Expand All @@ -28,7 +28,9 @@ use celestia_types::nmt::Namespace;
use futures::stream::BoxStream;
use futures::StreamExt;
use sov_rollup_interface::common::HexHash;
use sov_rollup_interface::da::{DaProof, DaSpec, RelevantBlobs, RelevantProofs};
use sov_rollup_interface::da::{
BlobReaderTrait, DaProof, DaSpec, DaVerifier, RelevantBlobs, RelevantProofs,
};
use sov_rollup_interface::node::da::{
run_maybe_retryable_async_fn_with_retries, DaService, MaybeRetryable, SubmitBlobReceipt,
};
Expand All @@ -45,6 +47,7 @@ pub struct CelestiaService {
client: Arc<celestia_client::Client>,
rollup_batch_namespace: Namespace,
rollup_proof_namespace: Namespace,
verify_on_fetch_mode: VerifyOnFetchMode,
signer_address: Option<CelestiaAddress>,
safe_lead_time: Duration,
backoff_policy: ExponentialBuilder,
Expand All @@ -54,10 +57,12 @@ pub struct CelestiaService {
}

impl CelestiaService {
#[allow(clippy::too_many_arguments)]
fn with_client(
client: celestia_client::Client,
rollup_batch_namespace: Namespace,
rollup_proof_namespace: Namespace,
verify_on_fetch_mode: VerifyOnFetchMode,
signer_address: Option<CelestiaAddress>,
safe_lead_time: Duration,
backoff_policy: ExponentialBuilder,
Expand All @@ -69,6 +74,7 @@ impl CelestiaService {
client: Arc::new(client),
rollup_batch_namespace,
rollup_proof_namespace,
verify_on_fetch_mode,
signer_address,
safe_lead_time,
backoff_policy,
Expand Down Expand Up @@ -231,6 +237,7 @@ impl CelestiaService {
client,
chain_params.rollup_batch_namespace,
chain_params.rollup_proof_namespace,
config.verify_on_fetch_mode,
fetched_signer,
Duration::from_millis(config.safe_lead_time_ms),
backoff_policy,
Expand Down Expand Up @@ -356,7 +363,28 @@ impl CelestiaService {
tracker.submit(get_block_measurement);
});
tracing::trace!(height, "get_block_at metrics send, returning");
FilteredCelestiaBlock::new(rollup_batch_shares, rollup_proof_shares, header)
let block = FilteredCelestiaBlock::new(rollup_batch_shares, rollup_proof_shares, header)?;
match self.verify_on_fetch_mode {
VerifyOnFetchMode::Off => {}
VerifyOnFetchMode::LogError => {
if let Err(error) = self.verify_block_integrity(&block) {
tracing::error!(
height,
?error,
"Celestia block integrity verification failed; continuing because \
verify_on_fetch_mode is `log_error`"
);
}
Comment thread
citizen-stig marked this conversation as resolved.
}
VerifyOnFetchMode::ReturnError => {
self.verify_block_integrity(&block).map_err(|error| {
anyhow::anyhow!(
"Celestia block integrity verification failed at height {height}: {error}"
)
})?;
}
Comment on lines +379 to +385
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: ReturnError mode double-wraps the error, losing the anyhow chain

At crates/adapters/celestia/src/da_service/mod.rs:380-384, the ReturnError branch calls .map_err(|error| anyhow::anyhow!("...{error}")) on the result of verify_block_integrity, which already returns anyhow::Result. This creates a new anyhow::Error using the Display of the inner error, discarding the original error's backtrace and cause chain. The user-visible error message is preserved (since {error} uses Display), but anyhow's .context() or direct ? propagation would retain the full chain. This is a minor style concern — not a functional bug — but could make debugging slightly harder in production when investigating verification failures.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
Ok(block)
}

async fn get_head_block_header_inner(
Expand Down Expand Up @@ -423,6 +451,28 @@ impl CelestiaService {
.map(|res| res.map(CelestiaHeader::from).map_err(|e| e.into()))
.boxed())
}

fn verify_block_integrity(&self, block: &FilteredCelestiaBlock) -> anyhow::Result<()> {
let verifier = CelestiaVerifier::new(RollupParams {
rollup_batch_namespace: self.rollup_batch_namespace,
rollup_proof_namespace: self.rollup_proof_namespace,
});

let mut relevant_blobs = extract_relevant_blobs(block);
// Advance full blob data first, then derive proofs for the consumed ranges.
for blob in relevant_blobs
.batch_blobs
.iter_mut()
.chain(relevant_blobs.proof_blobs.iter_mut())
{
blob.advance(blob.total_len());
}
let relevant_proofs = get_extraction_proof(block, &relevant_blobs);

verifier.verify_relevant_tx_list(&block.header, &relevant_blobs, relevant_proofs)?;

Ok(())
}
}

#[async_trait]
Expand Down
2 changes: 1 addition & 1 deletion crates/adapters/celestia/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ pub use sov_rollup_interface::da::*;
pub use sov_rollup_interface::node::da::*;

#[cfg(feature = "native")]
pub use da_service::{CelestiaConfig, CelestiaService};
pub use da_service::{CelestiaConfig, CelestiaService, VerifyOnFetchMode};

pub use crate::celestia::*;
3 changes: 2 additions & 1 deletion crates/adapters/celestia/src/test_helper/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::config::{
default_tx_status_polling_millis,
};
use crate::verifier::address::CelestiaAddress;
use crate::{CelestiaConfig, CelestiaService};
use crate::{CelestiaConfig, CelestiaService, VerifyOnFetchMode};
use anyhow::{anyhow, Context};
use sov_rollup_interface::da::BlockHeaderTrait;
use sov_rollup_interface::node::da::DaService;
Expand Down Expand Up @@ -295,6 +295,7 @@ impl CelestiaDevNode {
api_request_timeout_secs: default_api_request_timeout_secs(),
tx_status_polling_millis: default_tx_status_polling_millis(),
background_stat_polling_interval_secs: default_background_stat_polling_interval_secs(),
verify_on_fetch_mode: VerifyOnFetchMode::ReturnError,
safe_lead_time_ms: default_safe_lead_time_ms(),
tx_priority: default_tx_priority(),
backoff_min_delay_ms: default_min_delay_ms(),
Expand Down
7 changes: 7 additions & 0 deletions examples/demo-rollup/configs/celestia_rollup_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ signer_private_key = "aec34bb1cfae6e906594dc106af4057a9046f769e2eeed67d040f0107e
# Default: 30
#background_stat_polling_interval_secs = 30

# Controls how fetched Celestia blocks are verified against their Data Availability Header.
# Options:
# "off" - skip verification (fastest, may silently accept corrupted RPC data). Default.
# "log_error" - run verification; on failure log and continue.
# "return_error" - run verification; on failure fail the fetch and propagate the error
#verify_on_fetch_mode = "off"

# Options are "Low", "Medium" and "High"
# Defines a priority of rollup blob transactions. Set it to "High" for faster inclusion.
# Default: High for better blob inclusion.
Expand Down
Loading