Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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.

31 changes: 20 additions & 11 deletions crates/aggchain-proof-builder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@ impl<ContractsClient> AggchainProofBuilder<ContractsClient> {
pub async fn new(
config: &AggchainProofBuilderConfig,
contracts_client: Arc<ContractsClient>,
aggregation_vkey_override: Option<Arc<SP1VerifyingKey>>,
range_vkey_commitment_override: Option<Digest>,
) -> eyre::Result<Self>
where
ContractsClient: L1OpSuccinctConfigFetcher,
Expand All @@ -342,14 +344,21 @@ 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
{
let retrieved = sp1_fast(|| VKeyHash::from_vkey(&aggregation_vkey))
// Resolve the aggregation vkey and range vkey commitment, preferring the
// configured overrides and falling back to the values embedded from
// op-succinct-elfs at build time.
let override_active = aggregation_vkey_override.is_some();
let aggregation_vkey = aggregation_vkey_override
.unwrap_or_else(|| Arc::new(proposer_elfs::aggregation::VKEY.vkey().clone()));
let range_vkey_commitment =
range_vkey_commitment_override.unwrap_or(Digest(RANGE_VKEY_COMMITMENT));

// Check mismatch on the aggregation vkey embedded from op-succinct-elfs.
// Skipped when an override is supplied: the override is intentionally a
// different (newer) key, and is instead validated against the on-chain
// op-succinct config below.
if !override_active {
let retrieved = sp1_fast(|| VKeyHash::from_vkey(aggregation_vkey.as_ref()))
.context("Computing VKey hash")?;
let expected = AGGREGATION_VKEY_HASH;

Expand All @@ -371,7 +380,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 +389,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 +538,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 @@ -13,6 +13,7 @@ aggchain-proof-builder.workspace = true
aggchain-proof-contracts.workspace = true
aggchain-proof-core.workspace = true
aggchain-proof-types.workspace = true
aggkit-prover-types = { workspace = true, features = ["sp1"] }
eyre.workspace = true
proposer-client.workspace = true
proposer-service.workspace = true
Expand Down
67 changes: 67 additions & 0 deletions crates/aggchain-proof-service/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,71 @@ 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.
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
#[serde(rename_all = "kebab-case")]
pub struct OpSuccinctVkeyConfig {
/// Bincode-serialized aggregation `SP1VerifyingKey`, hex-encoded (`0x`
/// prefix optional). Must be produced with the same codec the prover uses
/// to decode it (see
/// `aggkit_prover_types::vkey::decode_verifying_key`); the
/// `op-succinct-vkey` CLI subcommand emits a value in this format.
#[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 super::*;

#[test]
fn op_succinct_overrides_parse_from_hex() {
let config: OpSuccinctVkeyConfig = serde_json::from_str(
r#"{
"aggregation-vkey": "0xdeadbeef",
"range-vkey-commitment":
"0x0000000000000000000000000000000000000000000000000000000000000001"
}"#,
)
.expect("parsing op-succinct overrides");

assert_eq!(
config.aggregation_vkey,
Some(alloy_primitives::Bytes::from_static(&[
0xde, 0xad, 0xbe, 0xef
]))
);
assert_eq!(config.range_vkey_commitment.expect("commitment").0[31], 1);
}

#[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] aggkit_prover_types::vkey::VKeyDecodeError),
}
36 changes: 30 additions & 6 deletions crates/aggchain-proof-service/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,20 @@ pub struct AggchainProofService {
impl AggchainProofService {
pub async fn new(config: &AggchainProofServiceConfig) -> Result<Self, Error> {
debug!("Initializing AggchainProofService");

// Decode the optional op-succinct aggregation vkey override exactly once so
// the proposer service (host-side verification) and the proof builder
// (recursive verification) are guaranteed to use the identical key. When
// absent, each consumer falls back to the embedded op-succinct-elfs value.
let aggregation_vkey_override: Option<Arc<sp1_sdk::SP1VerifyingKey>> = config
.op_succinct
.aggregation_vkey
.as_ref()
.map(|bytes| aggkit_prover_types::vkey::decode_verifying_key(bytes))
.transpose()
.map_err(Error::OpSuccinctVkeyDecode)?
.map(Arc::new);

let client = prover_alloy::AlloyProvider::new(
&config.proposer_service.l1_rpc_endpoint.url,
prover_alloy::DEFAULT_HTTP_RPC_NODE_INITIAL_BACKOFF_MS,
Expand All @@ -110,17 +124,25 @@ impl AggchainProofService {
let proposer_service = if config.proposer_service.mock {
tower::ServiceBuilder::new()
.service(
ProposerService::new_mock(&config.proposer_service, l1_rpc_client)
.await
.map_err(Error::ProposerServiceInitFailed)?,
ProposerService::new_mock(
&config.proposer_service,
l1_rpc_client,
aggregation_vkey_override.clone(),
)
.await
.map_err(Error::ProposerServiceInitFailed)?,
)
.boxed_clone()
} else {
tower::ServiceBuilder::new()
.service(
ProposerService::new_network(&config.proposer_service, l1_rpc_client)
.await
.map_err(Error::ProposerServiceInitFailed)?,
ProposerService::new_network(
&config.proposer_service,
l1_rpc_client,
aggregation_vkey_override.clone(),
)
.await
.map_err(Error::ProposerServiceInitFailed)?,
)
.boxed_clone()
};
Expand All @@ -131,6 +153,8 @@ impl AggchainProofService {
AggchainProofBuilder::new(
&config.aggchain_proof_builder,
contract_l1_client.clone(),
aggregation_vkey_override,
config.op_succinct.range_vkey_commitment,
)
.await
.map_err(Error::AggchainProofBuilderInitFailed)?,
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
30 changes: 30 additions & 0 deletions crates/proposer-elfs/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,33 @@ fn snap_vkey_hash(#[case] name: &'static str, #[case] vkey: &LazyVerifyingKey) {

insta::assert_snapshot!(format!("{name}_vkey"), snap);
}

/// The config-override codec must round-trip with the embedded representation:
/// the bytes emitted by `encode_verifying_key` (used by the `op-succinct-vkey`
/// CLI) must decode back into the same key the runtime falls back to.
#[rstest::rstest]
#[case::agg(&aggregation::VKEY)]
#[case::range(&range::VKEY)]
fn vkey_codec_round_trip(#[case] vkey: &LazyVerifyingKey) {
use aggkit_prover_types::vkey::{decode_verifying_key, encode_verifying_key};

let decoded = decode_verifying_key(&vkey.as_bytes()).expect("decoding embedded vkey");
assert_eq!(
VKeyHash::from_vkey(&decoded),
VKeyHash::from_vkey(vkey.vkey())
);

let encoded = encode_verifying_key(&decoded).expect("encoding vkey");
assert_eq!(encoded.as_slice(), vkey.as_bytes().as_ref());
}

/// Decoding incomplete vkey bytes must fail rather than silently producing a
/// bogus key, so a malformed config override is rejected at startup.
#[test]
fn decode_rejects_truncated_vkey() {
let bytes = aggregation::VKEY.as_bytes();
let full: &[u8] = bytes.as_ref();
let truncated = &full[..full.len() / 2];

assert!(aggkit_prover_types::vkey::decode_verifying_key(truncated).is_err());
}
Loading
Loading