Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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: 1 addition & 0 deletions Cargo.lock

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

32 changes: 18 additions & 14 deletions crates/aggchain-proof-builder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,21 +342,25 @@ impl<ContractsClient> AggchainProofBuilder<ContractsClient> {

let prover = Buffer::new(executor, MAX_CONCURRENT_REQUESTS);

// Retrieve the entire aggregation vkey and the range vkey commitment from the
// ELF
let aggregation_vkey = proposer_elfs::aggregation::VKEY.vkey().clone();
let range_vkey_commitment = Digest(proposer_elfs::range::VKEY_COMMITMENT);

// Check mismatch on aggregation vkey
// Resolve the aggregation vkey and range vkey commitment. These use the
// configured op-succinct override when one was installed at startup (see
// `proposer_elfs::install_overrides`), otherwise the values embedded from
// op-succinct-elfs at build time.
let aggregation_vkey = Arc::new(proposer_elfs::aggregation::vkey().clone());
let range_vkey_commitment = Digest(proposer_elfs::range::commitment());

// Sanity-check that the embedded op-succinct-elfs vkey constants are
// internally consistent. The resolved (possibly overridden) key is
// validated against the on-chain op-succinct config below instead.
{
let retrieved = sp1_fast(|| VKeyHash::from_vkey(&aggregation_vkey))
.context("Computing VKey hash")?;
let expected = AGGREGATION_VKEY_HASH;
let retrieved =
sp1_fast(|| VKeyHash::from_vkey(proposer_elfs::aggregation::VKEY.vkey()))
.context("Computing VKey hash")?;

if retrieved != expected {
if retrieved != AGGREGATION_VKEY_HASH {
return Err(eyre::Report::from(Error::MismatchAggregationElfVkeyHash {
got: retrieved,
expected,
expected: AGGREGATION_VKEY_HASH,
}));
}
}
Expand All @@ -371,7 +375,7 @@ impl<ContractsClient> AggchainProofBuilder<ContractsClient> {
// Validate that the OpSuccinct config keys match expected values
validate_op_succinct_config_keys(
&op_succinct_config,
&aggregation_vkey,
aggregation_vkey.as_ref(),
&range_vkey_commitment,
)?;

Expand All @@ -380,7 +384,7 @@ impl<ContractsClient> AggchainProofBuilder<ContractsClient> {
contracts_client,
prover,
network_id: config.network_id,
aggregation_vkey: Arc::new(aggregation_vkey),
aggregation_vkey,
range_vkey_commitment,
static_call_caller_address: config.contracts.static_call_caller_address,
})
Expand Down Expand Up @@ -529,7 +533,7 @@ impl<ContractsClient> AggchainProofBuilder<ContractsClient> {
l1_info_tree_leaf,
l1_head_inclusion_proof: request.aggchain_proof_inputs.l1_info_tree_merkle_proof,
aggregation_vkey_hash: KoalaBearDigest(aggregation_vkey.hash_u32()),
range_vkey_commitment: RANGE_VKEY_COMMITMENT,
range_vkey_commitment: range_vkey_commitment.0,
};

{
Expand Down
1 change: 1 addition & 0 deletions crates/aggchain-proof-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ aggchain-proof-core.workspace = true
aggchain-proof-types.workspace = true
eyre.workspace = true
proposer-client.workspace = true
proposer-elfs.workspace = true
proposer-service.workspace = true
prover-alloy.workspace = true
unified-bridge.workspace = true
Expand Down
71 changes: 69 additions & 2 deletions crates/aggchain-proof-service/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::fmt::Debug;

use aggchain_proof_builder::config::AggchainProofBuilderConfig;
use proposer_service::config::ProposerServiceConfig;
use serde::{Deserialize, Serialize};
Expand All @@ -10,4 +8,73 @@ use serde::{Deserialize, Serialize};
pub struct AggchainProofServiceConfig {
pub aggchain_proof_builder: AggchainProofBuilderConfig,
pub proposer_service: ProposerServiceConfig,

/// Optional overrides for the op-succinct verification key material.
///
/// When unset, the values embedded from `op-succinct-elfs` at build time
/// are used. Supplying them here lets an op-succinct upgrade be rolled out
/// without rebuilding the aggkit-prover image.
#[serde(default, skip_serializing_if = "OpSuccinctVkeyConfig::is_empty")]
pub op_succinct: OpSuccinctVkeyConfig,
}

/// Optional overrides of the op-succinct verification key material derived from
/// a given op-succinct release. Each field independently falls back to the
/// value embedded from `op-succinct-elfs` when absent.
///
/// Both values are hex strings handled by their types' existing serde; the
/// aggregation vkey bytes are turned into a real `SP1VerifyingKey` with the
/// same bincode codec the prover uses elsewhere. Produce them with the
/// `op-succinct-vkey` CLI subcommand.
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
#[serde(rename_all = "kebab-case")]
pub struct OpSuccinctVkeyConfig {
/// Bincode-serialized aggregation `SP1VerifyingKey`, hex-encoded (`0x`
/// prefix optional).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub aggregation_vkey: Option<alloy_primitives::Bytes>,

/// Range vkey commitment, hex-encoded 32 bytes (`0x` prefix optional).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub range_vkey_commitment: Option<agglayer_interop::types::Digest>,
}

impl OpSuccinctVkeyConfig {
/// Returns `true` when no override is set, so the section can be omitted
/// from serialized configuration.
fn is_empty(&self) -> bool {
self.aggregation_vkey.is_none() && self.range_vkey_commitment.is_none()
}
}

#[cfg(test)]
mod tests {
use proposer_elfs::{Sp1VKeyHash as _, VKeyHash};

use super::*;

#[test]
fn op_succinct_aggregation_vkey_round_trips_through_config() {
// `Bytes` carries the hex value through serde, and the existing bincode
// codec turns it back into the real verifying key. Use a real serialized
// vkey: the one embedded from op-succinct-elfs.
let encoded = alloy_primitives::hex::encode(proposer_elfs::aggregation::VKEY.as_bytes());
let config: OpSuccinctVkeyConfig =
serde_json::from_str(&format!(r#"{{ "aggregation-vkey": "0x{encoded}" }}"#))
.expect("parsing aggregation vkey");

let bytes = config.aggregation_vkey.expect("aggregation vkey present");
let vkey = proposer_elfs::decode_verifying_key(bytes.as_ref()).expect("decodes");
assert_eq!(
VKeyHash::from_vkey(&vkey),
VKeyHash::from_vkey(proposer_elfs::aggregation::VKEY.vkey()),
);
}

#[test]
fn op_succinct_overrides_default_to_none() {
let config: OpSuccinctVkeyConfig =
serde_json::from_str("{}").expect("parsing empty overrides");
assert!(config.is_empty());
}
}
3 changes: 3 additions & 0 deletions crates/aggchain-proof-service/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,7 @@ pub enum Error {

#[error("Unable to resolve aggchain proof vkey")]
AggchainProofVkeyResolveFailed(#[source] aggchain_proof_contracts::Error),

#[error("Unable to decode the configured op-succinct aggregation verification key")]
OpSuccinctVkeyDecode(#[source] proposer_elfs::VKeyDecodeError),
}
19 changes: 19 additions & 0 deletions crates/aggchain-proof-service/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,25 @@ pub struct AggchainProofService {
impl AggchainProofService {
pub async fn new(config: &AggchainProofServiceConfig) -> Result<Self, Error> {
debug!("Initializing AggchainProofService");

// Install the optional op-succinct vkey overrides from configuration before
// constructing the services, so the proposer service (host-side
// verification) and the proof builder (recursive verification) both read
// the same in-effect values via `proposer_elfs`. When absent, the values
// embedded from op-succinct-elfs are used.
proposer_elfs::install_overrides(
config
.op_succinct
.aggregation_vkey
.as_ref()
.map(|vkey| vkey.as_ref()),
config
.op_succinct
.range_vkey_commitment
.map(|digest| digest.0),
)
.map_err(Error::OpSuccinctVkeyDecode)?;

let client = prover_alloy::AlloyProvider::new(
&config.proposer_service.l1_rpc_endpoint.url,
prover_alloy::DEFAULT_HTTP_RPC_NODE_INITIAL_BACKOFF_MS,
Expand Down
31 changes: 31 additions & 0 deletions crates/aggkit-prover-types/src/vkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,37 @@ impl UpperHex for LazyVerifyingKey {
}
}

/// Error returned when a configured verifying key cannot be decoded.
#[derive(Debug, thiserror::Error)]
#[error("failed to decode SP1 verifying key from the configured bytes: {0}")]
pub struct VKeyDecodeError(String);

/// Error returned when a verifying key cannot be encoded.
#[derive(Debug, thiserror::Error)]
#[error("failed to encode SP1 verifying key: {0}")]
pub struct VKeyEncodeError(String);

/// Decode a bincode-encoded [`SP1VerifyingKey`], as produced by the build-time
/// `prover_elf_utils::ElfInfo::emit_vkey_bytes` /
/// [`LazyVerifyingKey::as_bytes`].
///
/// This must use the exact same codec as [`LazyVerifyingKey::vkey`] so that a
/// configured override and the embedded fallback decode identically.
pub fn decode_verifying_key(bytes: &[u8]) -> Result<SP1VerifyingKey, VKeyDecodeError> {
prover_elf_utils::elf_info::bincode_codec()
.deserialize(bytes)
.map_err(|error| VKeyDecodeError(error.to_string()))
}

/// Encode an [`SP1VerifyingKey`] into the bincode representation accepted by
/// [`decode_verifying_key`]. Uses the same codec as [`decode_verifying_key`],
/// so the two are guaranteed to round-trip.
pub fn encode_verifying_key(vkey: &SP1VerifyingKey) -> Result<Vec<u8>, VKeyEncodeError> {
prover_elf_utils::elf_info::bincode_codec()
.serialize(vkey)
.map_err(|error| VKeyEncodeError(error.to_string()))
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
2 changes: 1 addition & 1 deletion crates/aggkit-prover/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ tracing.workspace = true
aggchain-proof-service.workspace = true
aggchain-proof-types.workspace = true
aggkit-prover-config.workspace = true
aggkit-prover-types.workspace = true
aggkit-prover-types = { workspace = true, features = ["sp1"] }
agglayer-interop = { workspace = true, features = ["grpc-compat"] }
proposer-client.workspace = true
proposer-service.workspace = true
Expand Down
10 changes: 10 additions & 0 deletions crates/aggkit-prover/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,14 @@ pub enum Commands {

/// Proof verification key selector.
VkeySelector,

/// Derive the op-succinct vkey override config values from a directory
/// containing the op-succinct ELFs (`aggregation-elf` and
/// `range-elf-embedded`). Prints a ready-to-paste
/// `[aggchain-proof-service.op-succinct]` section.
OpSuccinctVkey {
/// Path to the directory holding the op-succinct ELF binaries.
#[arg(long, value_hint = ValueHint::DirPath)]
elf_dir: PathBuf,
},
}
44 changes: 44 additions & 0 deletions crates/aggkit-prover/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,50 @@ fn main() -> eyre::Result<()> {
let vkey_selector_hex = hex::encode(AGGCHAIN_VKEY_SELECTOR.to_be_bytes());
println!("0x{vkey_selector_hex}");
}

aggkit_prover::cli::Commands::OpSuccinctVkey { elf_dir } => {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(async move {
// The CLI is short-lived, so leaking the ELF bytes to satisfy the
// `'static` bound of `compute_program_vkey` is acceptable.
let read_elf = |name: &str| -> eyre::Result<&'static [u8]> {
let path = elf_dir.join(name);
let bytes = std::fs::read(&path)
.with_context(|| format!("Reading ELF {}", path.display()))?;
let leaked: &'static [u8] = Box::leak(bytes.into_boxed_slice());
Ok(leaked)
};

let aggregation_vkey = prover_executor::Executor::compute_program_vkey(
read_elf("aggregation-elf")?,
)
.await?;
let range_vkey = prover_executor::Executor::compute_program_vkey(read_elf(
"range-elf-embedded",
)?)
.await?;

let aggregation_vkey_bytes =
aggkit_prover_types::vkey::encode_verifying_key(&aggregation_vkey)?;

println!("[aggchain-proof-service.op-succinct]");
println!(
"aggregation-vkey = \"0x{}\"",
hex::encode(&aggregation_vkey_bytes)
);
println!(
"range-vkey-commitment = \"0x{}\"",
hex::encode(range_vkey.hash_bytes())
);
println!(
"# aggregation on-chain vkey hash (bytes32): 0x{}",
hex::encode(aggregation_vkey.bytes32_raw())
);
Ok::<(), eyre::Report>(())
})?;
}
}

Ok(())
Expand Down
54 changes: 52 additions & 2 deletions crates/proposer-elfs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pub use aggkit_prover_types::{
vkey::LazyVerifyingKey,
vkey::{decode_verifying_key, LazyVerifyingKey, SP1VerifyingKey, VKeyDecodeError},
vkey_hash::{HashU32, Sp1VKeyHash, VKeyHash},
};

Expand All @@ -8,19 +8,33 @@ mod vkeys_raw {
}

pub mod aggregation {
use std::sync::OnceLock;

pub use op_succinct_elfs::AGGREGATION_ELF as ELF;

use crate::{vkeys_raw, HashU32, LazyVerifyingKey, VKeyHash};
use crate::{vkeys_raw, HashU32, LazyVerifyingKey, SP1VerifyingKey, VKeyHash};

pub static VKEY: LazyVerifyingKey =
LazyVerifyingKey::from_unparsed_bytes(vkeys_raw::aggregation::VKEY_BYTES);

pub const VKEY_HASH_U32: HashU32 = vkeys_raw::aggregation::VKEY_HASH;

pub const VKEY_HASH: VKeyHash = VKeyHash::from_hash_u32(VKEY_HASH_U32);

/// Configured override, installed once at startup via
/// [`crate::install_overrides`].
pub(crate) static VKEY_OVERRIDE: OnceLock<SP1VerifyingKey> = OnceLock::new();

/// The aggregation verifying key in effect: the configured override when
/// set, otherwise the key embedded from op-succinct-elfs at build time.
pub fn vkey() -> &'static SP1VerifyingKey {
VKEY_OVERRIDE.get().unwrap_or_else(|| VKEY.vkey())
}
}

pub mod range {
use std::sync::OnceLock;

pub use op_succinct_elfs::RANGE_ELF_EMBEDDED as ELF;
pub use vkeys_raw::range::VKEY_COMMITMENT;

Expand All @@ -32,6 +46,42 @@ pub mod range {
pub const VKEY_HASH_U32: HashU32 = vkeys_raw::range::VKEY_HASH;

pub const VKEY_HASH: VKeyHash = VKeyHash::from_hash_u32(VKEY_HASH_U32);

/// Configured override, installed once at startup via
/// [`crate::install_overrides`].
pub(crate) static VKEY_COMMITMENT_OVERRIDE: OnceLock<[u8; 32]> = OnceLock::new();

/// The range vkey commitment in effect: the configured override when set,
/// otherwise the value embedded from op-succinct-elfs at build time.
pub fn commitment() -> [u8; 32] {
VKEY_COMMITMENT_OVERRIDE
.get()
.copied()
.unwrap_or(VKEY_COMMITMENT)
}
}

/// Install optional op-succinct vkey overrides parsed from configuration.
///
/// The values are process-global and intended to be installed once at startup,
/// before the proof services are constructed; a repeated install keeps the
/// first value. When an override is absent, the value embedded from
/// `op-succinct-elfs` at build time is used instead. See [`aggregation::vkey`]
/// and [`range::commitment`] for the resolved accessors the services read.
pub fn install_overrides(
aggregation_vkey: Option<&[u8]>,
range_vkey_commitment: Option<[u8; 32]>,
) -> Result<(), VKeyDecodeError> {
// An `Err` from `set` means the value was already installed, which is
// expected on repeated startup-time calls (e.g. multiple service
// constructions in tests); the first installed value is kept.
if let Some(bytes) = aggregation_vkey {
let _ = aggregation::VKEY_OVERRIDE.set(decode_verifying_key(bytes)?);
}
if let Some(commitment) = range_vkey_commitment {
let _ = range::VKEY_COMMITMENT_OVERRIDE.set(commitment);
Comment on lines +79 to +82

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject conflicting vkey override installs

When the same process constructs more than one service with different op-succinct override config, OnceLock::set returns Err here but the error is discarded, so the second service keeps using the first service's vkey/commitment and can reject otherwise-valid proofs or fail against the second chain's on-chain config. This also violates /workspace/provers/AGENTS.md guidance to never silently discard errors with let _ = on fallible operations; please either error on conflicting repeats or verify the existing value matches the requested override.

Useful? React with 👍 / 👎.

}
Ok(())
}

#[cfg(test)]
Expand Down
Loading
Loading