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
5 changes: 3 additions & 2 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ jobs:
name: Validate PR Title
permissions:
contents: read
pull-requests: read
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Check PR Title
uses: amannn/action-semantic-pull-request@v5
Expand Down Expand Up @@ -48,14 +50,13 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

check-changelog:
needs: validate
permissions:
contents: read
pull-requests: write
issues: write
name: Check Changelog Entry
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down
23 changes: 21 additions & 2 deletions crates/common/src/da.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,18 @@ use tracing::{debug, error, info};

use crate::cache::L1BlockCache;

/// Wrapper for proofs that includes Bitcoin location for efficient storage
#[derive(Clone)]
pub struct ProofWithLocation {
pub proof: Proof,
/// Bitcoin block height where this proof was found
pub bitcoin_block_height: u64,
/// Transaction index within the Bitcoin block
pub bitcoin_tx_index: u32,
}

pub enum ProofOrCommitment {
Proof(Proof),
Proof(ProofWithLocation),
Commitment(SequencerCommitment),
}

Expand Down Expand Up @@ -134,11 +144,20 @@ pub async fn extract_zk_proofs_and_sequencer_commitments<Da: DaService>(
prover_da_pub_key: &[u8],
sequencer_da_pub_key: &[u8],
) -> Vec<ProofOrCommitment> {
let bitcoin_block_height = l1_block.header().height();

let proofs = da_service
.extract_relevant_zk_proofs(l1_block, prover_da_pub_key)
.await
.into_iter()
.map(|(idx, proof)| (idx, ProofOrCommitment::Proof(proof)));
.map(|(tx_index, proof)| {
let proof_with_location = ProofWithLocation {
proof,
bitcoin_block_height,
bitcoin_tx_index: tx_index as u32,
};
(tx_index, ProofOrCommitment::Proof(proof_with_location))
});

let commitments = da_service
.as_ref()
Expand Down
106 changes: 92 additions & 14 deletions crates/fullnode/src/da_block_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::time::{Duration, Instant};
use anyhow::{anyhow, bail};
use citrea_common::backup::BackupManager;
use citrea_common::cache::L1BlockCache;
use citrea_common::da::{extract_zk_proofs_and_sequencer_commitments, sync_l1, ProofOrCommitment};
use citrea_common::da::{extract_zk_proofs_and_sequencer_commitments, sync_l1, ProofOrCommitment, ProofWithLocation};
use citrea_common::utils::{
exceeded_stop_height, get_tangerine_activation_height_non_zero, shutdown_requested,
};
Expand Down Expand Up @@ -591,17 +591,19 @@ where
&self,
current_l1_block_height: u64,
found_in_l1_block_height: u64,
proof: Proof,
proof_with_location: ProofWithLocation,
proof_source: ProofSource,
) -> Result<ProcessingResult, ProcessingError> {
let proof = &proof_with_location.proof;

tracing::info!(
"Processing zk proof at height: {}",
found_in_l1_block_height
);
tracing::trace!("ZK proof: {:?}", proof);

// Extract and verify the proof using the appropriate ZKVM
let Ok(batch_proof_output) = Vm::extract_output::<BatchProofCircuitOutput>(&proof) else {
let Ok(batch_proof_output) = Vm::extract_output::<BatchProofCircuitOutput>(proof) else {
return Ok(ProcessingResult::Discarded);
};

Expand Down Expand Up @@ -635,7 +637,7 @@ where
current_l1_block_height,
found_in_l1_block_height,
batch_proof_output.initial_state_root(),
proof,
proof_with_location,
batch_proof_output,
proof_source,
)
Expand All @@ -648,7 +650,7 @@ where
/// * `current_l1_block_height` - Current L1 block being processed
/// * `found_in_l1_block_height` - L1 block where proof was found
/// * `initial_state_root` - Initial state root for verification
/// * `raw_proof` - The raw ZK proof
/// * `proof_with_location` - The ZK proof with Bitcoin location metadata
/// * `batch_proof_output` - The batch proof circuit output
/// * `proof_source` - Whether the proof came from L1 or pending retries
///
Expand All @@ -659,7 +661,7 @@ where
current_l1_block_height: u64,
found_in_l1_block_height: u64,
initial_state_root: [u8; 32],
raw_proof: Proof,
proof_with_location: ProofWithLocation,
batch_proof_output: BatchProofCircuitOutput,
proof_source: ProofSource,
) -> Result<ProcessingResult, ProcessingError> {
Expand Down Expand Up @@ -733,15 +735,16 @@ where
if proof_is_pending {
if proof_source == ProofSource::FromL1 {
info!(
"Proof is pending for commitment index range {}-{}. Storing proof as pending.",
"Proof is pending for commitment index range {}-{}. Storing proof location instead of full proof to reduce disk usage.",
sequencer_commitment_index_range.0, sequencer_commitment_index_range.1
);
self.ledger_db.store_pending_proof(
sequencer_commitment_index_range.0,
sequencer_commitment_index_range.1,
raw_proof,
proof_with_location.bitcoin_block_height,
proof_with_location.bitcoin_tx_index,
found_in_l1_block_height,
)?;
)?
} else {
info!(
"Proof is pending for commitment index range {}-{}. Keeping existing pending proof without rewrite.",
Expand Down Expand Up @@ -773,7 +776,7 @@ where
if sequencer_commitment_index_range.0 > proven_height.commitment_index + 1 {
if proof_source == ProofSource::FromL1 {
info!(
"First commitment in range is not strictly increasing. Expected index {}, got {}. Storing proof as pending for commitment range {}-{}",
"First commitment in range is not strictly increasing. Expected index {}, got {}. Storing proof location for commitment range {}-{}",
proven_height.commitment_index + 1,
sequencer_commitment_index_range.0,
sequencer_commitment_index_range.0,
Expand All @@ -782,9 +785,10 @@ where
self.ledger_db.store_pending_proof(
sequencer_commitment_index_range.0,
sequencer_commitment_index_range.1,
raw_proof,
proof_with_location.bitcoin_block_height,
proof_with_location.bitcoin_tx_index,
found_in_l1_block_height,
)?;
)?
} else {
info!(
"First commitment in range is not strictly increasing. Expected index {}, got {}. Keeping existing pending proof for commitment range {}-{} without rewrite",
Expand All @@ -797,6 +801,7 @@ where
return Ok(ProcessingResult::Pending);
}

let raw_proof = proof_with_location.proof.clone();
// store in ledger db
self.ledger_db.update_verified_proof_data(
found_in_l1_block_height,
Expand Down Expand Up @@ -915,12 +920,27 @@ where
let pending_proofs = self.ledger_db.get_pending_proofs()?;

for item in pending_proofs {
let ((min_index, max_index), (proof, found_in_l1_height)) = item?.into_tuple();
let ((min_index, max_index), (proof_location, found_in_l1_height)) = item?.into_tuple();

// Fetch the proof from Bitcoin using the stored location
let proof_with_location = match self.fetch_proof_from_bitcoin(proof_location).await {
Ok(p) => p,
Err(e) => {
warn!(
"Failed to fetch proof from Bitcoin for index {min_index}-{max_index} at location block={}, tx_idx={}, found_in_l1_height={}: {e:?}",
proof_location.block_height, proof_location.tx_index, found_in_l1_height
);
// Keep this entry pending for future retry but don't block processing of later proofs.
// Transient RPC failures may succeed on next cycle.
continue;
}
};

Comment on lines +924 to +938

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

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

On fetch_proof_from_bitcoin failure you break out of the loop but keep the (potentially permanently broken) pending-proof entry in the DB. If the failure is non-transient (e.g., tx index not found due to reorg/pruning), this can prevent all subsequent pending proofs from ever being retried/cleared. Consider distinguishing transient RPC failures vs permanent “not found” cases (e.g., remove/discard the pending entry on permanent failure, or at least continue to allow later ranges to be processed).

Suggested change
// Fetch the proof from Bitcoin using the stored location
let proof_with_location = match self.fetch_proof_from_bitcoin(proof_location, found_in_l1_height).await {
Ok(p) => p,
Err(e) => {
warn!(
"Failed to fetch proof from Bitcoin for index {min_index}-{max_index} at location block={}, tx_idx={}: {e:?}",
proof_location.block_height, proof_location.tx_index
);
break;
}
};
// Fetch the proof from Bitcoin using the stored location
let proof_with_location = match self
.fetch_proof_from_bitcoin(proof_location, found_in_l1_height)
.await
{
Ok(p) => p,
Err(e) => {
warn!(
"Failed to fetch proof from Bitcoin for index {min_index}-{max_index} at location block={}, tx_idx={}: {e:?}",
proof_location.block_height, proof_location.tx_index
);
// Keep this entry pending for a future retry, but do not let one failed
// fetch block processing of later pending proofs.
continue;
}
};

Copilot uses AI. Check for mistakes.
match self
.process_zk_proof(
current_l1_block_height,
found_in_l1_height,
proof,
proof_with_location,
ProofSource::FromPendingRetry,
)
.await
Expand Down Expand Up @@ -995,4 +1015,62 @@ where
}
Ok(sequencer_commitment.l2_end_block_number)
}

/// Fetches a proof from Bitcoin using stored location information
///
/// This method retrieves a proof that was previously identified by its Bitcoin block height
/// and transaction index, reducing the need to store full proofs in the pending proofs table.
///
/// # Arguments
/// * `proof_location` - Bitcoin block height and transaction index where proof is located
/// * `l1_height` - L1 block height for context
///
/// # Returns
/// A ProofWithLocation containing the fetched proof and location metadata
async fn fetch_proof_from_bitcoin(
&self,
proof_location: sov_db::schema::types::BitcoinProofLocation,
) -> Result<ProofWithLocation, ProcessingError> {
Comment on lines +1030 to +1033

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

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

fetch_proof_from_bitcoin takes l1_height but doesn’t use it, which makes the API misleading and can trigger unused-parameter lints in some configurations. Either remove the parameter or incorporate it into the error context/logging so callers can correlate the fetch failure with the original L1 inclusion height.

Copilot uses AI. Check for mistakes.
// Fetch the Bitcoin block at the specified height
let block = self.da_service
.get_block_at(proof_location.block_height)
.await
.map_err(|e| {
ProcessingError::SkippableError(
SkippableError::Proof(
ProofError::ProofFetchFailed(format!(
"Failed to fetch Bitcoin block at height {}: {}",
proof_location.block_height, e
))
)
)
})?;

// Extract proofs from the block
let proofs = self.da_service
.extract_relevant_zk_proofs(&block, &self.prover_da_pub_key)
.await;

// Find the proof at the specified transaction index
let proof = proofs
.into_iter()
.find(|(tx_idx, _)| *tx_idx == proof_location.tx_index as usize)
.map(|(_, proof)| proof)
.ok_or_else(|| {
ProcessingError::SkippableError(
SkippableError::Proof(
ProofError::ProofFetchFailed(format!(
"Proof not found at Bitcoin block {} tx index {}",
proof_location.block_height, proof_location.tx_index
))
)
)
})?;

Ok(ProofWithLocation {
proof,
bitcoin_block_height: proof_location.block_height,
bitcoin_tx_index: proof_location.tx_index,
})
}
}
3 changes: 3 additions & 0 deletions crates/fullnode/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub enum ProofError {
/// Error when the sequencer commitment hash doesn't match the expected value
#[error("Proof verification: For a known and verified sequencer commitment. Hash mismatch - expected 0x{0} but got 0x{1}. Skipping proof.")]
SequencerCommitmentHashMismatch(String, String),
/// Error when fetching a proof from Bitcoin fails
#[error("Failed to fetch proof from Bitcoin: {0}")]
ProofFetchFailed(String),
/// Other general errors that may occur during proof processing
#[error("{0}")]
Other(#[from] anyhow::Error),
Expand Down
11 changes: 8 additions & 3 deletions crates/sovereign-sdk/full-node/db/sov-db/src/ledger_db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use crate::schema::types::light_client_proof::{
StoredLightClientProof, StoredLightClientProofOutput,
};
use crate::schema::types::{
BonsaiSession, BoundlessSession, L2BlockNumber, L2HeightAndIndex, L2HeightRange,
BitcoinProofLocation, BonsaiSession, BoundlessSession, L2BlockNumber, L2HeightAndIndex, L2HeightRange,
L2HeightStatus, SlotNumber,
};

Expand Down Expand Up @@ -979,13 +979,18 @@ impl NodeLedgerOps for LedgerDB {
&self,
min_commitment_index: u32,
max_commitment_index: u32,
proof: Proof,
bitcoin_block_height: u64,
bitcoin_tx_index: u32,
found_in_l1_height: u64,
) -> anyhow::Result<()> {
let mut schema_batch = SchemaBatch::new();
let proof_location = BitcoinProofLocation {
block_height: bitcoin_block_height,
tx_index: bitcoin_tx_index,
};
schema_batch.put::<PendingProofs>(
&(min_commitment_index, max_commitment_index),
&(proof, found_in_l1_height),
&(proof_location, found_in_l1_height),
)?;
Comment on lines +987 to 994

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

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

PendingProofs table schema still needs to be updated to store (BitcoinProofLocation, L1Height) instead of (Proof, L1Height). As-is, this put::<PendingProofs>(..., &(proof_location, found_in_l1_height)) won’t type-check against the current table definition, and even once compiled you’ll need to handle DB compatibility (e.g., drop/migrate the existing CF or add a migration that clears/rewrites old pending-proof entries).

Copilot uses AI. Check for mistakes.
self.db.write_schemas(schema_batch)?;
Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,14 @@ pub trait NodeLedgerOps: SharedLedgerOps + Send + Sync {
fn remove_pending_commitment(&self, index: u32) -> Result<()>;

/// Store an out of order proof by commitment index range for later processing
/// Instead of storing the full proof, we store the Bitcoin block height and transaction index
/// to reduce disk usage. The proof will be fetched from Bitcoin via RPC when retrying.
fn store_pending_proof(
&self,
min_commitment_index: u32,
max_commitment_index: u32,
proof: Proof,
bitcoin_block_height: u64,
bitcoin_tx_index: u32,
found_in_l1_height: u64,
) -> Result<()>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use super::types::batch_proof::{StoredBatchProof, StoredVerifiedProof};
use super::types::l2_block::StoredL2Block;
use super::types::light_client_proof::StoredLightClientProof;
use super::types::{
AccessoryKey, AccessoryStateValue, BonsaiSession, BoundlessSession, DbHash, JmtValue, L1Height,
AccessoryKey, AccessoryStateValue, BitcoinProofLocation, BonsaiSession, BoundlessSession, DbHash, JmtValue, L1Height,
L2BlockNumber, L2HeightAndIndex, L2HeightRange, L2HeightStatus, SlotNumber, StateKey,
};

Expand Down Expand Up @@ -566,8 +566,8 @@ define_table_with_seek_key_codec!(
);

define_table_with_seek_key_codec!(
/// Out of order proofs
(PendingProofs) (u32, u32) => (Proof, L1Height)
/// Out of order proofs - stores Bitcoin location (block height + tx index) for lazy fetching
(PendingProofs) (u32, u32) => (BitcoinProofLocation, L1Height)
);

#[cfg(test)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,30 @@ pub type L2HeightRange = (L2BlockNumber, L2BlockNumber);
/// L1 height
pub type L1Height = u64;

/// Location of a proof on Bitcoin for lazy fetching
/// Stores the Bitcoin block height and transaction index to retrieve the proof via RPC
/// instead of keeping the full proof in memory
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
::borsh::BorshDeserialize,
::borsh::BorshSerialize,
::serde::Serialize,
::serde::Deserialize,
)]
pub struct BitcoinProofLocation {
/// Bitcoin block height where the proof is stored
pub block_height: u64,
/// Transaction index within the Bitcoin block
pub tx_index: u32,
}

/// The output of the pending proofs table
pub type PendingProofsOutput = ((u32, u32), Proof, L1Height);
/// Stores commit index range, Bitcoin proof location, and L1 height
pub type PendingProofsOutput = ((u32, u32), BitcoinProofLocation, L1Height);

/// Height and index of a sequencer commitment
#[derive(
Expand Down