Skip to content
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
95ee4d3
feat: add f3 cert actor
karlem Sep 25, 2025
fbe0db6
feat: add fetching from parent
karlem Oct 20, 2025
4c6bf4e
feat: add extra checks and tests
karlem Oct 7, 2025
5f3eb8a
feat: multiple epochs in certificate
karlem Oct 9, 2025
fbee757
fix: clippy
karlem Oct 20, 2025
8a5533e
feat: fix comments
karlem Oct 27, 2025
59ab3bc
feat: fix comment
karlem Oct 28, 2025
e69da3f
fix: e2e tests
karlem Oct 28, 2025
d0c1e99
feat: implement coments changes
karlem Oct 29, 2025
0cb4a42
feat: add proofs service skeleton
karlem Oct 9, 2025
784b599
feat: rebase
karlem Oct 20, 2025
2ed6931
feat: add persistence and include proofs libraryr
karlem Oct 20, 2025
d25d646
feat: add perstance, real libraries, wather
karlem Oct 21, 2025
9df2716
feat: implement cache e2e
karlem Oct 23, 2025
6cfde38
feat: add missing feature
karlem Oct 23, 2025
bc8076d
feat: debug issues + make functional
karlem Oct 24, 2025
8495902
feat: prepare for review, add debug tooling, add observibility
karlem Oct 27, 2025
e2a7de6
feat: remove dead code
karlem Oct 27, 2025
d59aece
feat: fix clippy and bug
karlem Oct 27, 2025
3b84809
fix: clippy
karlem Oct 27, 2025
279aacd
fix: ci clippy
karlem Oct 28, 2025
e174661
feat: fix comment
karlem Oct 28, 2025
ac527de
feat: rebase main
karlem Nov 4, 2025
25a029a
feat: fix test
karlem Nov 4, 2025
221baa3
feat: comments
karlem Nov 5, 2025
d77f568
feat: fix comments
karlem Nov 6, 2025
495d7f0
fix: comments
karlem Nov 25, 2025
2fe7b60
feat: fix based on comments and improvements
karlem Nov 26, 2025
a4e27bd
feat: fix verifier
karlem Nov 28, 2025
1747f78
feat: fixed cmd
karlem Nov 28, 2025
be9c28e
feat: fix most of comments
karlem Dec 2, 2025
0e9612e
feat: re-work cache, generates proofs for epoch, certify child tipssets
karlem Dec 3, 2025
ab2ba27
feat: cleanup code, optimize logic
karlem Dec 4, 2025
d40e5f4
feat: simplify proofs generation and simplify cache
karlem Dec 8, 2025
7192140
feat: fix minor comments
karlem Dec 9, 2025
7aef2f2
fix: CI
karlem Dec 15, 2025
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,316 changes: 933 additions & 383 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ members = [
"fendermint/testing/*-test",
"fendermint/tracing",
"fendermint/vm/*",
"fendermint/vm/topdown/proof-service",
"fendermint/actors",
"fendermint/actors-custom-car",
"fendermint/actors-builtin-car",
Expand Down Expand Up @@ -184,6 +185,8 @@ tracing-appender = "0.2.3"
text-tables = "0.3.1"
url = { version = "2.4.1", features = ["serde"] }
zeroize = "1.6"
parking_lot = "0.12"
humantime-serde = "1.1"

# Vendored for cross-compilation, see https://github.com/cross-rs/cross/wiki/Recipes#openssl
# Make sure every top level build target actually imports this dependency, and don't end up
Expand Down
1 change: 1 addition & 0 deletions contracts/contracts/lib/LibGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ library LibGateway {

event MembershipUpdated(Membership);
/// @dev subnet refers to the next "down" subnet that the `envelope.message.to` should be forwarded to.
// Keep in sync with the event signature in the proof-service: fendermint/vm/topdown/proof-service/src/assembler.rs:NEW_TOPDOWN_MESSAGE_SIGNATURE
event NewTopDownMessage(address indexed subnet, IpcEnvelope message, bytes32 indexed id);
/// @dev event emitted when there is a new bottom-up message added to the batch.
/// @dev there is no need to emit the message itself, as the message is included in batch.
Expand Down
1 change: 1 addition & 0 deletions contracts/contracts/lib/LibGatewayActorStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct GatewayActorStorage {
uint64 bottomUpNonce;
/// @notice AppliedNonces keep track of the next nonce of the message to be applied.
/// This prevents potential replay attacks.
// Keep in sync with the storage slot offset in the proof-service: fendermint/vm/topdown/proof-service/src/assembler.rs:TOPDOWN_NONCE_STORAGE_OFFSET
uint64 appliedTopDownNonce;
/// @notice Number of active subnets spawned from this one
uint64 totalSubnets;
Expand Down
1 change: 1 addition & 0 deletions contracts/contracts/lib/LibPowerChangeLog.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {PowerChangeLog, PowerChange, PowerOperation} from "../structs/Subnet.sol

/// The util library for `PowerChangeLog`
library LibPowerChangeLog {
// Keep in sync with the event signature in the proof-service: fendermint/vm/topdown/proof-service/src/assembler.rs:NEW_POWER_CHANGE_REQUEST_SIGNATURE
event NewPowerChangeRequest(PowerOperation op, address validator, bytes payload, uint64 configurationNumber);

/// @notice Validator request to update its metadata
Expand Down
1 change: 1 addition & 0 deletions contracts/contracts/structs/Subnet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ struct PowerChangeRequest {
/// @notice The collection of staking changes.
struct PowerChangeLog {
/// @notice The next configuration number to assign to new changes.
// Keep in sync with the storage slot offset in the proof-service: fendermint/vm/topdown/proof-service/src/assembler.rs:NEXT_CONFIG_NUMBER_STORAGE_SLOT
uint64 nextConfigurationNumber;
/// @notice The starting configuration number stored.
uint64 startConfigurationNumber;
Expand Down
1 change: 1 addition & 0 deletions fendermint/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ fendermint_vm_message = { path = "../vm/message" }
fendermint_vm_resolver = { path = "../vm/resolver" }
fendermint_vm_snapshot = { path = "../vm/snapshot" }
fendermint_vm_topdown = { path = "../vm/topdown" }
fendermint_vm_topdown_proof_service = { path = "../vm/topdown/proof-service" }

ipc_actors_abis = { path = "../../contract-bindings" }
ethers = {workspace = true}
Expand Down
8 changes: 6 additions & 2 deletions fendermint/app/options/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use fvm_shared::address::Network;
use lazy_static::lazy_static;

use self::{
eth::EthArgs, genesis::GenesisArgs, key::KeyArgs, materializer::MaterializerArgs, rpc::RpcArgs,
run::RunArgs,
eth::EthArgs, genesis::GenesisArgs, key::KeyArgs, materializer::MaterializerArgs,
proof_cache::ProofCacheArgs, rpc::RpcArgs, run::RunArgs,
};

pub mod config;
Expand All @@ -20,6 +20,7 @@ pub mod eth;
pub mod genesis;
pub mod key;
pub mod materializer;
pub mod proof_cache;
pub mod rpc;
pub mod run;

Expand Down Expand Up @@ -150,6 +151,9 @@ pub enum Commands {
/// Subcommands related to the Testnet Materializer.
#[clap(aliases = &["mat", "matr", "mate"])]
Materializer(MaterializerArgs),
/// Inspect and debug F3 proof cache.
#[clap(name = "proof-cache")]
ProofCache(ProofCacheArgs),
}

#[cfg(test)]
Expand Down
44 changes: 44 additions & 0 deletions fendermint/app/options/src/proof_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2022-2025 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT

use clap::{Args, Subcommand};
use std::path::PathBuf;

#[derive(Debug, Args)]
#[command(name = "proof-cache", about = "Inspect and debug F3 proof cache")]
pub struct ProofCacheArgs {
#[command(subcommand)]
pub command: ProofCacheCommands,
}

#[derive(Debug, Subcommand)]
pub enum ProofCacheCommands {
/// Inspect cache contents
Inspect {
/// Database path
#[arg(long, env = "FM_PROOF_CACHE_DB")]
db_path: PathBuf,
},
/// Show cache statistics
Stats {
/// Database path
#[arg(long, env = "FM_PROOF_CACHE_DB")]
db_path: PathBuf,
},
/// Get specific proof by instance ID
Get {
/// Database path
#[arg(long, env = "FM_PROOF_CACHE_DB")]
db_path: PathBuf,

/// Instance ID to fetch
#[arg(long)]
instance_id: u64,
},
/// Clear the cache
Clear {
/// Database path
#[arg(long, env = "FM_PROOF_CACHE_DB")]
db_path: PathBuf,
},
}
2 changes: 2 additions & 0 deletions fendermint/app/src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub mod eth;
pub mod genesis;
pub mod key;
pub mod materializer;
pub mod proof_cache;
pub mod rpc;
pub mod run;

Expand Down Expand Up @@ -69,6 +70,7 @@ macro_rules! cmd {
/// Execute the command specified in the options.
pub async fn exec(opts: Arc<Options>) -> anyhow::Result<()> {
match &opts.command {
Commands::ProofCache(args) => args.exec(()).await,
Commands::Config(args) => args.exec(opts.clone()).await,
Commands::Debug(args) => {
let _trace_file_guard = set_global_tracing_subscriber(&TracingSettings::default());
Expand Down
194 changes: 194 additions & 0 deletions fendermint/app/src/cmd/proof_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// Copyright 2022-2025 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT

use crate::cmd;
use crate::options::proof_cache::{ProofCacheArgs, ProofCacheCommands};
use fendermint_vm_topdown_proof_service::persistence::ProofCachePersistence;
use std::path::Path;

cmd! {
ProofCacheArgs(self) {
handle_proof_cache_command(self)
}
}

fn handle_proof_cache_command(args: &ProofCacheArgs) -> anyhow::Result<()> {
match &args.command {
ProofCacheCommands::Inspect { db_path } => inspect_cache(db_path),
ProofCacheCommands::Stats { db_path } => show_stats(db_path),
ProofCacheCommands::Get {
db_path,
instance_id,
} => get_proof(db_path, *instance_id),
ProofCacheCommands::Clear { db_path } => clear_cache(db_path),
}
}

fn inspect_cache(db_path: &Path) -> anyhow::Result<()> {
println!("=== Proof Cache Inspection ===");
println!("Database: {}", db_path.display());
println!();

let persistence = ProofCachePersistence::open(db_path)?;
let entries = persistence.load_all_entries()?;

if entries.is_empty() {
println!("\nCache is empty.");
return Ok(());
}

println!("\nEntries:");
println!(
"{:<12} {:<20} {:<15} {:<15}",
"Instance ID", "Epochs", "Proof Size", "Signers"
);
println!("{}", "-".repeat(70));

for entry in &entries {
let proof_size = fvm_ipld_encoding::to_vec(&entry.proof_bundle)
.map(|v| v.len())
.unwrap_or(0);

println!(
"{:<12} {:<20?} {:<15} {:<15}",
entry.certificate.gpbft_instance,
entry.certificate.ec_chain.suffix(),
format!("{} bytes", proof_size),
format!("{} signers", entry.certificate.signers.len())
);
}

Ok(())
}

fn show_stats(db_path: &Path) -> anyhow::Result<()> {
println!("=== Proof Cache Statistics ===");
println!("Database: {}", db_path.display());
println!();

let persistence = ProofCachePersistence::open(db_path)?;
let entries = persistence.load_all_entries()?;

if entries.is_empty() {
println!("Cache is empty.");
return Ok(());
}

println!("Count: {}", entries.len());
println!(
"Instances: {} - {}",
entries
.first()
.map(|e| e.certificate.gpbft_instance)
.unwrap_or(0),
entries
.last()
.map(|e| e.certificate.gpbft_instance)
.unwrap_or(0)
);
println!();

// Proof size statistics
let total_proof_size: usize = entries
.iter()
.map(|e| {
fvm_ipld_encoding::to_vec(&e.proof_bundle)
.map(|v| v.len())
.unwrap_or(0)
})
.sum();
let avg_proof_size = total_proof_size / entries.len();

println!("Proof Bundle Statistics:");
println!(
" Total Size: {} bytes ({:.2} MB)",
total_proof_size,
total_proof_size as f64 / 1024.0 / 1024.0
);
println!(
" Average Size: {} bytes ({:.2} KB)",
avg_proof_size,
avg_proof_size as f64 / 1024.0
);

Ok(())
}

fn get_proof(db_path: &Path, instance_id: u64) -> anyhow::Result<()> {
println!("=== Get Proof for Instance {} ===", instance_id);
println!("Database: {}", db_path.display());
println!();

let persistence = ProofCachePersistence::open(db_path)?;
let entries = persistence.load_all_entries()?;

if entries.is_empty() {
println!("Cache is empty.");
return Ok(());
}

let entry = entries
.iter()
.find(|e| e.certificate.gpbft_instance == instance_id);

if let Some(entry) = entry {
println!("Found proof for instance {}", instance_id);
println!();

// Certificate Details
println!("F3 Certificate:");
println!(" Instance ID: {}", entry.certificate.gpbft_instance);
println!(
" Finalized Epochs: {:?}",
&entry.certificate.ec_chain.suffix()
);
println!(
" BLS Signature: {} bytes",
entry.certificate.signature.len()
);
println!(" Signers: {} validators", entry.certificate.signers.len());
println!();

// Proof Bundle Summary
let proof_bundle_size = fvm_ipld_encoding::to_vec(&entry.proof_bundle)
.map(|v| v.len())
.unwrap_or(0);
println!("Proof Bundle:");
println!(
" Total Size: {} bytes ({:.2} KB)",
proof_bundle_size,
proof_bundle_size as f64 / 1024.0
);

if let Some(proof_bundle) = &entry.proof_bundle {
println!(" Storage Proofs: {}", proof_bundle.storage_proofs.len());
println!(" Event Proofs: {}", proof_bundle.event_proofs.len());
println!(" Witness Blocks: {}", proof_bundle.blocks.len());
println!();
} else {
println!(" No proof bundle found");
}

// Metadata
println!("Metadata:");
println!(" Generated At: {:?}", entry.generated_at);
println!(" Source RPC: {}", entry.source_rpc);
} else {
println!("No proof found for instance {}", instance_id);
println!();
println!("Available instances: {:?}", entries.len());
Copy link

Choose a reason for hiding this comment

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

Bug: Misleading CLI output shows count instead of instance IDs

In the get_proof function, when a proof is not found, the error message displays "Available instances: X" where X is entries.len() (the count of entries). This is misleading because the label "Available instances" suggests the user will see a list of instance IDs they can query, but instead they only see a number. Compare this to show_stats which correctly labels the count as "Count" and shows actual instance IDs separately. Users who see "Available instances: 5" would reasonably expect to see something like [100, 101, 102, 103, 104] so they know which specific instances to query.

Fix in Cursor Fix in Web

}

Ok(())
}

fn clear_cache(db_path: &Path) -> anyhow::Result<()> {
println!("=== Clear Cache ===");
println!("Database: {}", db_path.display());
println!();

let persistence = ProofCachePersistence::open(db_path)?;
persistence.clear_all_entries()?;

Ok(())
}
Loading
Loading