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: 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),
}
48 changes: 47 additions & 1 deletion crates/aggchain-proof-service/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ use alloy_primitives::B256;
use futures::FutureExt as _;
use proposer_client::FepProposerRequest;
use proposer_service::ProposerService;
use sp1_sdk::HashableKey as _;
use tower::{util::BoxCloneService, Service as _, ServiceExt as _};
use tracing::debug;
use tracing::{debug, info};
use unified_bridge::AggchainProofPublicValues;

use crate::{
Expand Down Expand Up @@ -88,6 +89,51 @@ 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)?;

// Report the op-succinct verification keys in effect, and whether each came
// from a config override or the embedded op-succinct-elfs default, so the
// active keys can be confirmed at runtime.
let source = |overridden: bool| {
if overridden {
"config override"
} else {
"embedded (op-succinct-elfs)"
}
};
let aggregation_vkey_hash = format!(
"0x{}",
alloy_primitives::hex::encode(proposer_elfs::aggregation::vkey().bytes32_raw())
);
let range_vkey_commitment = format!(
"0x{}",
alloy_primitives::hex::encode(proposer_elfs::range::commitment())
);
info!(
aggregation_vkey_source = source(config.op_succinct.aggregation_vkey.is_some()),
%aggregation_vkey_hash,
range_vkey_commitment_source = source(config.op_succinct.range_vkey_commitment.is_some()),
%range_vkey_commitment,
"Resolved op-succinct verification keys",
);

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
Loading
Loading