Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 6 additions & 86 deletions common/continuity/src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -550,33 +550,12 @@ impl ContinuityBuilder {
///
/// # Note
///
/// This returns the raw chain tip without any reorg protection. It does **not** account
/// for `block_confirmation_depth`. Use [`get_confirmed_last_block`](Self::get_confirmed_last_block)
/// when you need a safe confirmed height.
/// This is the raw chain tip. It is not a serving boundary: whether a height may be served is
/// decided against the attested set, never against this value.
pub async fn get_last_block(&self) -> Result<u64> {
self.eth_provider.get_last_block().await
}

/// Get both the raw chain tip and the confirmed block height for reorg protection.
///
/// With a [`confirmation_tag`](ContinuityConfig::confirmation_tag) the confirmed height is
/// the node's tagged block (clamped to the tip); otherwise it is
/// `tip - block_confirmation_depth` (saturating at 0).
/// Use the confirmed value to decide whether to accept a requested block;
/// use the tip value in user-facing error messages so clients see the real chain height.
pub async fn get_confirmed_last_block(&self) -> Result<(u64, u64)> {
let tip = self.eth_provider.get_last_block().await?;
let confirmed = match self.config.confirmation_tag {
Some(tag) => self
.eth_provider
.get_block_number_by_tag(tag)
.await?
.min(tip),
None => tip.saturating_sub(self.config.block_confirmation_depth),
};
Ok((tip, confirmed))
}

/// Get the source chain ID.
///
/// Useful for health checks and chain validation.
Expand Down Expand Up @@ -683,50 +662,19 @@ mod tests {
use super::*;
use crate::{config::ContinuityConfig, mocks::make_mock_providers, rpc::EthRpcProvider};

fn make_builder(block_confirmation_depth: u64) -> ContinuityBuilder {
let chain_key = 2u64;
let config = ContinuityConfig::builder()
.cc3_rpc_url("http://mock")
.eth_rpc_url("http://mock")
.chain_key(chain_key)
.attestation_interval(10)
.checkpoint_interval(10)
.block_confirmation_depth(block_confirmation_depth)
.build();
let (cc_provider, eth_provider) = make_mock_providers(chain_key);
ContinuityBuilder::new_with_providers(config, cc_provider, eth_provider)
}

fn make_tag_builder(tag: eth::BlockTag) -> ContinuityBuilder {
fn make_builder() -> ContinuityBuilder {
let chain_key = 2u64;
let config = ContinuityConfig::builder()
.cc3_rpc_url("http://mock")
.eth_rpc_url("http://mock")
.chain_key(chain_key)
.attestation_interval(10)
.checkpoint_interval(10)
.block_confirmation_depth(0)
.confirmation_tag(Some(tag))
.build();
let (cc_provider, eth_provider) = make_mock_providers(chain_key);
ContinuityBuilder::new_with_providers(config, cc_provider, eth_provider)
}

/// With a confirmation tag the confirmed height is the node's tagged block, not `tip - depth`.
#[tokio::test]
async fn confirmed_last_block_follows_the_block_tag() {
let (tip, confirmed) = make_tag_builder(eth::BlockTag::Safe)
.get_confirmed_last_block()
.await
.unwrap();
assert_eq!((tip, confirmed), (1000, 968));
let (tip, confirmed) = make_tag_builder(eth::BlockTag::Finalized)
.get_confirmed_last_block()
.await
.unwrap();
assert_eq!((tip, confirmed), (1000, 936));
}

/// The archiver-backed provider forwards tag lookups to its live ETH fallback.
#[tokio::test]
async fn archiver_provider_forwards_block_tag_lookups() {
Expand All @@ -744,38 +692,10 @@ mod tests {
);
}

/// `get_confirmed_last_block` with depth 0 returns (tip, tip).
#[tokio::test]
async fn confirmed_last_block_depth_zero() {
let builder = make_builder(0);
let (tip, confirmed) = builder.get_confirmed_last_block().await.unwrap();
// MockEthRpcProvider::get_last_block always returns 1000
assert_eq!(tip, 1000);
assert_eq!(confirmed, 1000);
}

/// `get_confirmed_last_block` with depth N returns (tip, tip - N).
#[tokio::test]
async fn confirmed_last_block_subtracts_depth() {
let builder = make_builder(64);
let (tip, confirmed) = builder.get_confirmed_last_block().await.unwrap();
assert_eq!(tip, 1000);
assert_eq!(confirmed, 936);
}

/// `get_confirmed_last_block` saturates at 0 when depth >= tip.
#[tokio::test]
async fn confirmed_last_block_saturates_at_zero() {
let builder = make_builder(2000);
let (tip, confirmed) = builder.get_confirmed_last_block().await.unwrap();
assert_eq!(tip, 1000);
assert_eq!(confirmed, 0);
}

/// `get_last_block` returns the raw chain tip, ignoring confirmation depth.
/// `get_last_block` is the raw chain tip.
#[tokio::test]
async fn get_last_block_ignores_depth() {
let builder = make_builder(500);
async fn get_last_block_returns_the_raw_tip() {
let builder = make_builder();
let tip = builder.get_last_block().await.unwrap();
assert_eq!(tip, 1000);
}
Expand Down
42 changes: 0 additions & 42 deletions common/continuity/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,24 +63,6 @@ pub struct ContinuityConfig {
///
/// When `None`, checkpoint checks are always performed (slower but always correct).
pub last_checkpoint_block: Option<u64>,

/// Number of blocks to lag behind the EVM chain tip when validating block existence.
///
/// EVM chains (Ethereum, etc.) use probabilistic finality: blocks near the tip can be
/// reorganised away. By requiring that requested blocks are at least
/// `block_confirmation_depth` behind the current head we reduce the chance of serving a
/// proof for a block that later disappears.
///
/// Set to `0` for chains with instant / irreversible finality.
/// A typical safe value for Ethereum mainnet is `12` (~2 min at 12 s/block).
///
/// Ignored when [`confirmation_tag`](Self::confirmation_tag) is set.
pub block_confirmation_depth: u64,

/// Confirm blocks by the source node's `safe` / `finalized` block tag instead of a fixed
/// depth. This is what the `RpcSafe` / `RpcFinalized` on-chain maturity strategies resolve
/// to, so the prover confirms on exactly the schedule the attestors attest on.
pub confirmation_tag: Option<eth::BlockTag>,
}

impl ContinuityConfig {
Expand Down Expand Up @@ -199,8 +181,6 @@ pub struct ConfigBuilder {
attestation_interval: Option<u64>,
checkpoint_interval: Option<u64>,
last_checkpoint_block: Option<u64>,
block_confirmation_depth: u64,
confirmation_tag: Option<eth::BlockTag>,
}

impl ConfigBuilder {
Expand Down Expand Up @@ -284,26 +264,6 @@ impl ConfigBuilder {
self
}

/// Set the number of blocks to lag behind the EVM chain tip for reorg protection.
///
/// # Arguments
///
/// * `depth` - Number of confirmation blocks (0 = no lag, use chain tip directly)
///
/// A typical safe value for Ethereum mainnet is `12` (~2 min at 12 s/block).
/// Set to `0` for chains with instant / irreversible finality.
pub fn block_confirmation_depth(mut self, depth: u64) -> Self {
self.block_confirmation_depth = depth;
self
}

/// Confirm blocks by a source-node block tag (`safe` / `finalized`) instead of a fixed
/// depth. See [`ContinuityConfig::confirmation_tag`].
pub fn confirmation_tag(mut self, tag: Option<eth::BlockTag>) -> Self {
self.confirmation_tag = tag;
self
}

/// Build the configuration.
///
/// # Panics
Expand Down Expand Up @@ -335,8 +295,6 @@ impl ConfigBuilder {
.checkpoint_interval
.expect("checkpoint_interval is required"),
last_checkpoint_block: self.last_checkpoint_block,
block_confirmation_depth: self.block_confirmation_depth,
confirmation_tag: self.confirmation_tag,
}
}

Expand Down
1 change: 0 additions & 1 deletion proof-gen-api-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ hex = { workspace = true }
merkle = { workspace = true }
prometheus-client = { workspace = true }
stream = { workspace = true, features = ["cc3"] }
supported-chains-primitives = { workspace = true }
sysinfo = { workspace = true }
usc-abi-encoding = { workspace = true }

Expand Down
16 changes: 12 additions & 4 deletions proof-gen-api-server/bin/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ pub struct ProofGenApiServer {
#[arg(
long,
env = "BLOCK_CONFIRMATION_DEPTH",
help = "Reorg-protection depth override, in blocks. Omit to derive it from the chain's \
on-chain MaturityStrategy (recommended; matches the attestors). If set and it \
differs from the on-chain value, startup logs a warning."
hide = true,
help = "Deprecated and ignored. Heights are confirmed against the attested set on \
Creditcoin, not against this process's own view of the source tip."
)]
block_confirmation_depth: Option<u64>,
}
Expand All @@ -112,6 +112,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.with_env_filter(env_filter)
.try_init();

if let Some(depth) = args.block_confirmation_depth {
tracing::warn!(
block_confirmation_depth = depth,
"--block-confirmation-depth / BLOCK_CONFIRMATION_DEPTH is deprecated and ignored: \
heights are confirmed against the attested set, not against this process's view of \
the source tip. Remove it."
);
}

let resolved_cc3_key = args.cc3_key.or_else(|| env::var("CC3_KEY").ok());

let config = if let Some(path) = args.config.clone() {
Expand Down Expand Up @@ -150,7 +159,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// YAML; the legacy single-chain CLI path stays single-URL.
eth_rpc_fallback_urls: Vec::new(),
archiver_url: args.archiver_url,
block_confirmation_depth: args.block_confirmation_depth,
// Per-chain cache sizing is expressed in the YAML config only, like
// `eth_rpc_fallback_urls`. Legacy single-chain mode takes the defaults.
cache: proof_gen_api_server::config::ChainCacheConfig::default(),
Expand Down
11 changes: 3 additions & 8 deletions proof-gen-api-server/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,9 @@ chains:
- chain_key: 2
eth_rpc_url: "http://localhost:8545"
# archiver_url: "http://localhost:8080" # optional per chain
# block_confirmation_depth: 32 # Fixed number of blocks to lag behind the chain tip.
# # OMIT THIS (recommended) to derive it from the chain's on-chain
# # MaturityStrategy -- the same value the attestors act on, so
# # this process cannot disagree with them. Offset strategies
# # (EvmSafe, FixedDelay: n, ...) become this depth; RpcSafe /
# # RpcFinalized make the prover confirm blocks by the source
# # node's `safe` / `finalized` block tag instead. Set it only to
# # deliberately pin a value; startup warns if it disagrees.
# block_confirmation_depth is deprecated and ignored. Whether a height can be served is
# decided against the attested set on Creditcoin (the attestors' maturity decision), never
# against this process's own view of the source tip. Remove it if present.
#
# Ordered fallback RPC URLs (optional).
# The eth client always tries `eth_rpc_url` (primary) first for every
Expand Down
55 changes: 18 additions & 37 deletions proof-gen-api-server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,6 @@ pub struct ChainConfig {
/// and you keep a more expensive "archive" endpoint for old data.
pub eth_rpc_fallback_urls: Vec<String>,
pub archiver_url: Option<String>,
/// Reorg-protection depth override, in blocks.
///
/// `None` (the default when the field is omitted) means: derive it at startup from the
/// chain's on-chain `MaturityStrategy` in the supported-chains pallet -- the same value the
/// attestors use, so this process cannot disagree with them. `Some(n)` pins an explicit
/// value; startup logs a WARN if it differs from the on-chain depth, and refuses to start if
/// the chain follows a block tag (`RpcSafe` / `RpcFinalized`), which no fixed depth can
/// reproduce.
///
/// This used to be a plain `u64` defaulting to `0`, so *omitting* it silently disabled reorg
/// protection. That is the failure mode this change removes.
/// See [`continuity::ContinuityConfig::block_confirmation_depth`].
pub block_confirmation_depth: Option<u64>,
/// Per-chain cache sizing. Defaults reproduce the historical behavior.
pub cache: ChainCacheConfig,
}
Expand Down Expand Up @@ -134,8 +121,6 @@ impl Config {
eth_rpc_url: "http://mock".to_string(),
eth_rpc_fallback_urls: Vec::new(),
archiver_url: None,
// Mock config has no chain to resolve against; pin explicitly.
block_confirmation_depth: Some(0),
cache: ChainCacheConfig::default(),
}],
max_batch_size: DEFAULT_MAX_BATCH_SIZE,
Expand Down Expand Up @@ -194,10 +179,10 @@ pub struct ChainConfigFile {
pub eth_rpc_fallback_urls: Vec<String>,
#[serde(default)]
pub archiver_url: Option<String>,
/// Reorg-protection depth override. **Omit it** to derive the depth from the chain's on-chain
/// `MaturityStrategy` (recommended -- matches the attestors by construction). Set it only to
/// deliberately pin a value; startup warns if it disagrees with the chain and fails if the
/// chain follows a block tag (`RpcSafe` / `RpcFinalized`).
/// Deprecated and ignored. Whether a height may be served is decided against the attested set
/// on Creditcoin, not against this process's own reading of the source chain, so there is no
/// reorg window to configure. Still accepted so existing YAML keeps parsing; a set value is
/// logged at startup and otherwise does nothing.
#[serde(default)]
pub block_confirmation_depth: Option<u64>,
/// Optional per-chain cache sizing. Omit the whole block to keep the defaults.
Expand Down Expand Up @@ -317,12 +302,20 @@ impl ConfigFile {
let eth_rpc_fallback_urls =
validate_fallback_urls(c.chain_key, c.eth_rpc_fallback_urls)?;
let cache = resolve_cache_config(c.chain_key, c.cache)?;
if let Some(depth) = c.block_confirmation_depth {
tracing::warn!(
chain_key = c.chain_key,
block_confirmation_depth = depth,
"block_confirmation_depth is deprecated and ignored: heights are confirmed \
against the attested set, not against this process's view of the source tip. \
Remove it from the config."
);
}
chains.push(ChainConfig {
chain_key: c.chain_key,
eth_rpc_url: c.eth_rpc_url,
eth_rpc_fallback_urls,
archiver_url: c.archiver_url,
block_confirmation_depth: c.block_confirmation_depth,
cache,
});
}
Expand Down Expand Up @@ -514,22 +507,10 @@ chains:
assert!(cfg.chains[0].eth_rpc_fallback_urls.is_empty());
}

/// The key is deprecated but must keep parsing: a chart that still renders it cannot take a
/// whole fleet down on the image bump that removed the behaviour.
#[test]
fn yaml_without_depth_is_none_not_zero() {
// Regression guard: omitting the field must mean "derive from chain", never "0".
let yaml = r#"
bind_host: "0.0.0.0"
bind_port: 3100
chains:
- chain_key: 8
eth_rpc_url: "http://localhost:8545"
"#;
let cfg = parse(yaml).expect("yaml should parse");
assert_eq!(cfg.chains[0].block_confirmation_depth, None);
}

#[test]
fn yaml_with_explicit_depth_is_some() {
fn yaml_with_deprecated_depth_still_parses() {
let yaml = r#"
bind_host: "0.0.0.0"
bind_port: 3100
Expand All @@ -538,8 +519,8 @@ chains:
eth_rpc_url: "http://localhost:8545"
block_confirmation_depth: 64
"#;
let cfg = parse(yaml).expect("yaml should parse");
assert_eq!(cfg.chains[0].block_confirmation_depth, Some(64));
let cfg = parse(yaml).expect("deprecated key must still parse");
assert_eq!(cfg.chains.len(), 1);
}

#[test]
Expand Down
Loading
Loading