Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
42 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
93f317d
fix: clippy
karlem Dec 17, 2025
3cc0376
fix: fmt
karlem Dec 17, 2025
6914245
fix: clippy
karlem Dec 17, 2025
9be81e8
fix: clippy
karlem Dec 17, 2025
4e168f0
fix: failing unit test
karlem Dec 18, 2025
b0213ec
fix: test
karlem Dec 18, 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
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ async fn run_service(
.await?;
Copy link

Choose a reason for hiding this comment

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

Bug: Hardcoded F3 network name in development testing tool

The development tool accepts arbitrary RPC URLs via --rpc-url but hardcodes "calibrationnet" as the F3 network name. The F3 light client uses this network name as part of the BLS signing domain, so if a user points the tool at a mainnet or other network RPC endpoint, certificate validation will silently fail with cryptographic errors rather than a clear message about the network mismatch. The network name parameter is missing from the CLI arguments despite the tool being designed to work with arbitrary RPC endpoints.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

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

Bug: Hardcoded network name prevents multi-network usage

The F3 network name is hardcoded to "calibrationnet" when creating the F3Client, but the binary accepts --rpc-url as a user-configurable parameter implying multi-network support. If a user provides a mainnet or other network RPC URL, the F3 client would be initialized with the wrong network name, causing certificate validation failures or incorrect behavior. The network name parameter is missing from the CLI arguments.

Fix in Cursor Fix in Web


// Get the power table
let current_state = temp_client.get_state().await;
let power_table = current_state.power_table;
let current_state = temp_client.get_state();
let power_table = current_state.power_table.clone();

println!("Power table fetched: {} entries", power_table.0.len());
println!(
Expand All @@ -146,10 +146,9 @@ async fn run_service(
polling_interval: Duration::from_secs(poll_interval),
cache_config: CacheConfig {
lookahead_instances: lookahead,
retention_instances: 2,
retention_epochs: 2,
},
parent_rpc_url: rpc_url,
fallback_rpc_urls: vec![],
gateway_id: GatewayId::EthAddress(gateway_address),
};

Expand Down
126 changes: 40 additions & 86 deletions fendermint/vm/topdown/proof-service/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
use crate::config::CacheConfig;
use crate::observe::{ProofCached, CACHE_HIT_TOTAL, CACHE_SIZE};
use crate::persistence::ProofCachePersistence;
use crate::types::{CertificateEntry, EpochProofEntry, EpochProofWithCertificates};
use crate::types::{CertificateEntry, EpochProofEntry, EpochProofWithCertificate};
use anyhow::{Context, Result};
use fvm_shared::clock::ChainEpoch;
use ipc_observability::emit;
Expand Down Expand Up @@ -75,34 +75,6 @@ impl ProofCache {
) -> Result<Self> {
let persistence = ProofCachePersistence::open(db_path)?;

// Load committed state from persistence, use higher of persisted vs provided
let (last_committed_epoch, last_committed_instance) =
match persistence.load_committed_state()? {
Some((persisted_epoch, persisted_instance)) => {
// Use the higher values (in case chain state is ahead of persistence or vice versa)
let epoch = persisted_epoch.max(initial_committed_epoch);
let instance = persisted_instance.max(initial_committed_instance);
tracing::info!(
persisted_epoch,
persisted_instance,
initial_committed_epoch,
initial_committed_instance,
using_epoch = epoch,
using_instance = instance,
"Loaded committed state from persistence"
);
(epoch, instance)
}
None => {
tracing::info!(
initial_committed_epoch,
initial_committed_instance,
"No persisted committed state, using initial values"
);
(initial_committed_epoch, initial_committed_instance)
}
};

// Load certificates
let cert_entries = persistence
.load_all_certificates()
Expand All @@ -128,14 +100,14 @@ impl ProofCache {
let cache = Self {
certificates: Arc::new(RwLock::new(certificates)),
epoch_proofs: Arc::new(RwLock::new(epoch_proofs)),
last_committed_epoch: Arc::new(AtomicI64::new(last_committed_epoch)),
last_committed_instance: Arc::new(AtomicU64::new(last_committed_instance)),
last_committed_epoch: Arc::new(AtomicI64::new(initial_committed_epoch)),
last_committed_instance: Arc::new(AtomicU64::new(initial_committed_instance)),
config,
persistence: Some(Arc::new(persistence)),
};

// Cleanup old entries
cache.cleanup_old_epochs(last_committed_epoch)?;
cache.cleanup_old_epochs(initial_committed_epoch)?;

Ok(cache)
}
Expand Down Expand Up @@ -210,8 +182,8 @@ impl ProofCache {

/// Get proof for a specific epoch
///
/// Returns the proof entry without the certificates.
/// Use `get_epoch_proof_with_certificates` for full data.
/// Returns the proof entry without the certificate.
/// Use `get_epoch_proof_with_certificate` for full data.
pub fn get_epoch_proof(&self, epoch: ChainEpoch) -> Option<EpochProofEntry> {
let result = self.epoch_proofs.read().get(&epoch).cloned();

Expand All @@ -223,24 +195,18 @@ impl ProofCache {
result
}

/// Get proof for a specific epoch with its certificates
/// Get proof for a specific epoch with its certificate
///
/// This is the main query method for consumers. Returns everything
/// needed for verification, including the pre-merged tipset list.
pub fn get_epoch_proof_with_certificates(
/// needed for verification, including the finalized tipsets.
pub fn get_epoch_proof_with_certificate(
&self,
epoch: ChainEpoch,
) -> Option<EpochProofWithCertificates> {
) -> Option<EpochProofWithCertificate> {
let proof_entry = self.get_epoch_proof(epoch)?;
let cert = self.get_certificate(proof_entry.cert_instance)?;

let parent_cert = self.get_certificate(proof_entry.parent_cert_instance)?;
let child_cert = self.get_certificate(proof_entry.child_cert_instance)?;

Some(EpochProofWithCertificates::new(
&proof_entry,
&parent_cert,
&child_cert,
))
Some(EpochProofWithCertificate::new(&proof_entry, &cert))
}

/// Check if an epoch proof exists
Expand Down Expand Up @@ -273,7 +239,6 @@ impl ProofCache {
"Updated last committed epoch and instance"
);

self.with_persistence(|p| p.save_committed_state(epoch, instance))?;
self.cleanup_old_epochs(epoch)
}

Expand Down Expand Up @@ -320,11 +285,11 @@ impl ProofCache {
return Ok(());
}

let referenced_certs = self.find_referenced_certs(&epochs_to_remove);
let certs_to_remove = self.collect_unreferenced_certs(&referenced_certs);

// Remove proofs first, then cleanup orphaned certificates
self.remove_epoch_proofs(&epochs_to_remove);
let certs_to_remove = self.collect_unreferenced_certs();
self.remove_certificates(&certs_to_remove);

self.persist_deletions(&epochs_to_remove, &certs_to_remove)?;

CACHE_SIZE.set(self.epoch_proofs.read().len() as i64);
Expand All @@ -348,19 +313,15 @@ impl ProofCache {
.collect()
}

fn find_referenced_certs(
&self,
epochs_to_remove: &[ChainEpoch],
) -> std::collections::HashSet<u64> {
self.epoch_proofs
/// Find certificates not referenced by any remaining proofs
fn collect_unreferenced_certs(&self) -> Vec<u64> {
let referenced: std::collections::HashSet<u64> = self
.epoch_proofs
.read()
.values()
.filter(|p| !epochs_to_remove.contains(&p.epoch))
.flat_map(|p| [p.parent_cert_instance, p.child_cert_instance])
.collect()
}
.map(|p| p.cert_instance)
.collect();

fn collect_unreferenced_certs(&self, referenced: &std::collections::HashSet<u64>) -> Vec<u64> {
self.certificates
.read()
.keys()
Expand Down Expand Up @@ -466,20 +427,15 @@ mod tests {
CertificateEntry::try_from(serializable).expect("valid certificate entry")
}

fn create_test_epoch_proof(
epoch: ChainEpoch,
parent_cert: u64,
child_cert: u64,
) -> EpochProofEntry {
fn create_test_epoch_proof(epoch: ChainEpoch, cert_instance: u64) -> EpochProofEntry {
EpochProofEntry::new(
epoch,
UnifiedProofBundle {
storage_proofs: vec![],
event_proofs: vec![],
blocks: vec![],
},
parent_cert,
child_cert,
cert_instance,
)
}

Expand Down Expand Up @@ -508,9 +464,9 @@ mod tests {

// Insert epoch proofs
let proofs = vec![
create_test_epoch_proof(100, 5, 6),
create_test_epoch_proof(101, 5, 6),
create_test_epoch_proof(102, 5, 6),
create_test_epoch_proof(100, 5),
create_test_epoch_proof(101, 5),
create_test_epoch_proof(102, 5),
];
cache.insert_epoch_proofs(proofs).unwrap();

Expand All @@ -521,7 +477,7 @@ mod tests {
}

#[test]
fn test_get_epoch_proof_with_certificates() {
fn test_get_epoch_proof_with_certificate() {
let config = CacheConfig {
lookahead_instances: 10,
retention_epochs: 5,
Expand All @@ -536,19 +492,17 @@ mod tests {
cache.insert_certificate(cert2).unwrap();

// Insert epoch proof
let proof = create_test_epoch_proof(101, 5, 6);
let proof = create_test_epoch_proof(101, 5);
cache.insert_epoch_proofs(vec![proof]).unwrap();

// Get with certificates
let result = cache.get_epoch_proof_with_certificates(101);
// Get with certificate
let result = cache.get_epoch_proof_with_certificate(101);
assert!(result.is_some());

let entry = result.unwrap();
assert_eq!(entry.epoch, 101);
assert_eq!(entry.parent_certificate.gpbft_instance, 5);
assert_eq!(entry.child_certificate.gpbft_instance, 6);
// Merged tipsets should include epochs from both certs
assert!(!entry.merged_tipsets.is_empty());
assert_eq!(entry.certificate.gpbft_instance, 5);
assert!(!entry.finalized_tipsets.is_empty());
}

#[test]
Expand All @@ -570,11 +524,11 @@ mod tests {

// Insert epoch proofs
let proofs = vec![
create_test_epoch_proof(100, 5, 6),
create_test_epoch_proof(101, 5, 6),
create_test_epoch_proof(102, 5, 6),
create_test_epoch_proof(103, 6, 7),
create_test_epoch_proof(104, 6, 7),
create_test_epoch_proof(100, 5),
create_test_epoch_proof(101, 5),
create_test_epoch_proof(102, 5),
create_test_epoch_proof(103, 6),
create_test_epoch_proof(104, 6),
];
cache.insert_epoch_proofs(proofs).unwrap();

Expand Down Expand Up @@ -615,17 +569,17 @@ mod tests {
.unwrap();

cache
.insert_epoch_proofs(vec![create_test_epoch_proof(100, 5, 6)])
.insert_epoch_proofs(vec![create_test_epoch_proof(100, 5)])
.unwrap();
assert_eq!(cache.highest_cached_epoch(), Some(100));

cache
.insert_epoch_proofs(vec![create_test_epoch_proof(105, 5, 6)])
.insert_epoch_proofs(vec![create_test_epoch_proof(105, 5)])
.unwrap();
assert_eq!(cache.highest_cached_epoch(), Some(105));

cache
.insert_epoch_proofs(vec![create_test_epoch_proof(103, 5, 6)])
.insert_epoch_proofs(vec![create_test_epoch_proof(103, 5)])
.unwrap();
assert_eq!(cache.highest_cached_epoch(), Some(105));
}
Expand Down
Loading
Loading