feat: range reduction breaks paired claims / unclaims - #345
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a safety validation to detect when a proposer-reduced end_block would cause an import/unclaim pair to be split (and therefore incorrectly treated as a valid claim), plus a helper script to generate a locally-runnable config from a Kurtosis enclave.
Changes:
- Add
validate_no_broken_pairsto detect broken (imported_bridge_exit, unclaim) pairs under range reduction, with unit tests. - Wire the validation into
AggchainProofServiceand introduce a dedicated error variant. - Add a script to download the Kurtosis config artifact and rewrite internal endpoints/paths for local execution.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/generate_config_based_kurtosis_to_run_locally.sh | Downloads config/genesis from Kurtosis and rewrites endpoints/paths for local runs. |
| crates/aggchain-proof-service/src/validation.rs | Implements broken-pair detection logic with a comprehensive test suite. |
| crates/aggchain-proof-service/src/service.rs | Calls the new validation when the proposer returns a different end_block. |
| crates/aggchain-proof-service/src/lib.rs | Registers the new validation module. |
| crates/aggchain-proof-service/src/error.rs | Adds an error variant for broken import/unclaim pairs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0545b620d6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
545e56b to
9f96677
Compare
hadjiszs
left a comment
There was a problem hiding this comment.
Thanks! Here is a first pass of review
| /// import falls within the new block range but its matching unclaim does not, | ||
| /// meaning the import would appear as valid in the proof even though it was | ||
| /// intended to be cancelled. | ||
| pub(crate) fn validate_no_broken_pairs( |
There was a problem hiding this comment.
I asked claude to try to make this function more readable
it suggested something like below (not tested nor properly reviewed), wdyt? Happy to review another version if you find better 👌
but basically trying to have dedicated structs if possible, and splitting this function into "build pairs into well-suited data structures" followed by "check the pairs"
#[derive(Debug)]
struct ImportUnclaimPair {
global_index: U256,
import_block: u64,
unclaim_block: u64,
}
pub(crate) fn validate_no_broken_pairs(
imports: &[ImportedBridgeExitWithBlockNumber],
unclaims: &[UnclaimWithBlockNumber],
new_end_block: u64,
) -> Result<(), Error> {
for ImportUnclaimPair {
global_index,
import_block,
unclaim_block,
} in pair_imports_with_unclaims(imports, unclaims)
{
if import_block <= new_end_block && unclaim_block > new_end_block {
return Err(Error::BrokenImportUnclaimPair {
global_index,
import_block,
unclaim_block,
new_end_block,
});
}
}
Ok(())
}
/// Pairs the k-th import event for each `global_index` with the k-th unclaim
/// for the same `global_index`, both in (block_number, log_index) order.
/// Excess events on either side are dropped — they correspond to imports
/// not yet cancelled, or unclaims targeting imports outside this witness.
fn pair_imports_with_unclaims(
imports: &[ImportedBridgeExitWithBlockNumber],
unclaims: &[UnclaimWithBlockNumber],
) -> Vec<ImportUnclaimPair> {
#[derive(Default)]
struct EventLists {
imports: Vec<(u64, u64)>,
unclaims: Vec<(u64, u64)>,
}
let mut by_gi: HashMap<U256, EventLists> = HashMap::new();
for ev in imports {
by_gi
.entry(ev.global_index.into())
.or_default()
.imports
.push((ev.block_number, ev.log_index));
}
for ev in unclaims {
by_gi
.entry(ev.global_index)
.or_default()
.unclaims
.push((ev.block_number, ev.log_index));
}
by_gi
.into_iter()
.flat_map(|(global_index, mut events)| {
events.imports.sort_unstable();
events.unclaims.sort_unstable();
events
.imports
.into_iter()
.zip(events.unclaims)
.map(move |((import_block, _), (unclaim_block, _))| ImportUnclaimPair {
global_index,
import_block,
unclaim_block,
})
})
.collect()
}There was a problem hiding this comment.
Thanks! Good suggestion — I went ahead and applied essentially this exact refactor in refactor(validation): split pairing from broken-pair check.
The current version now has:
- a dedicated
ImportUnclaimPairstruct, validate_no_broken_pairsreduced to just iterating the pairs and checking the broken-pair condition,- the pairing logic extracted into
pair_imports_with_unclaims(with theEventListshelper).
Only differences vs. your snippet are cosmetic: full-word variable names (imported_bridge_exits, events_by_global_index) to match the repo style, and slightly reworded doc comments. Let me know if you'd like anything else tweaked 👌
505eac3 to
7a2ab8f
Compare
Add Bash 4.2+ version check (macOS ships 3.2), a portable sedi() function to handle sed -i differences between Linux and macOS, and a portable_realpath() fallback for systems without GNU coreutils. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tonic::Status is 176 bytes and is part of the Proofs trait signature generated by tonic — we have no control over its size in test closures or in the mockall::mock! expansion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…order Events with the same global_index in the same block were only sorted by block_number, causing the k-th import to be paired with the wrong k-th unclaim and either missing a real split or raising a false positive. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract a `pair_imports_with_unclaims` helper returning `ImportUnclaimPair` values, leaving `validate_no_broken_pairs` as a flat check loop. Pairing now uses `zip`, which drops excess events on either side (imports not cancelled, or unclaims targeting imports outside this witness) as before. Addresses PR review feedback on readability. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7a2ab8f to
ef8ef1b
Compare
hadjiszs
left a comment
There was a problem hiding this comment.
lgtm 👍
nit: we may get some issues if aggsender submits some unclaim that are prior to the last_proven_block, due to the order in which we sort and filter, but I guess aggsender already enforce that when generating its request, so I'm fine not bothering too much there
| aggregation_vkey: &SP1VerifyingKey, | ||
| expected_range_vkey_commitment: &Digest, | ||
| ) -> Result<(), Error> { | ||
| let ignore_mismatch = std::env::var("IGNORE_VKEY_MISMATCH").is_ok(); |
There was a problem hiding this comment.
nit: otherwise IGNORE_VKEY_MISMATCH=false would enable it
| let ignore_mismatch = std::env::var("IGNORE_VKEY_MISMATCH").is_ok(); | |
| let ignore_mismatch = prover_utils::from_env_or_default("IGNORE_VKEY_MISMATCH", false); |
Summary
This PR adds two independent safety features:
Fix: #343 Detect broken import/unclaim pairs after range reduction
When the proposer reduces the
end_blockof a certificate (range reduction), it is possible that animported_bridge_exitfalls within the new range while its matchingunclaimdoes not. This would cause the import to appear as valid in the proof even though it was intentionally cancelled.A new
validate_no_broken_pairscheck is added inaggchain-proof-servicethat, whenever the proposer returns a differentend_blockthan the one originally requested, verifies that no(imported_bridge_exit, unclaim)pair is split by the new boundary. Pairs are matched byglobal_indexin chronological order (k-th import ↔ k-th unclaim). If a broken pair is found, proof generation is aborted with a descriptive error.Fix: #346 full error chain returned to gRPC clients
Previously, gRPC error responses only included the top-level error message (
error.to_string()), silently dropping all nested cause messages. A newformat_error_chainhelper walks the fullsource()chain and concatenates all messages, so clients now receive the complete error description.Chore: IGNORE_VKEY_MISMATCH environment variable to be able to develop/debug locally
Adds an opt-in escape hatch for development/testing environments: when
IGNORE_VKEY_MISMATCHis set, vkey hash and range vkey commitment mismatches between the on-chain op-succinct config and the local ELF are logged as warnings instead of returning an error, allowing the server to continue running. The mismatch is still clearly surfaced in the logs. This flag should never be set in production.Chore: scrtipt automates the setup needed to run the aggkit-prover locally against a live Kurtosis enclave (a local multi-service test environment).
Changes
aggchain-proof-service/src/validation.rs— new module withvalidate_no_broken_pairsand unit testsaggchain-proof-service/src/service.rs— calls the validation after receiving a proposer response with a reduced end blockaggchain-proof-service/src/error.rs— newBrokenImportUnclaimPairerror variantaggchain-proof-builder/src/lib.rs—IGNORE_VKEY_MISMATCHsupport for aggregation vkey hash and range vkey commitment checksaggkit-prover/src/rpc.rs—format_error_chainhelper; full error chain now returned to gRPC clientsproposer-client/src/rpc/mod.rs— minor error wrapping fixscripts/generate_config_based_kurtosis_to_run_locally.sh— new helper script for local Kurtosis-based testingFixes #343, #346