Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1fa0693
feat: add f3 cert actor
karlem Sep 25, 2025
264e69d
feat: add fetching from parent
karlem Oct 20, 2025
0c436bd
feat: add extra checks and tests
karlem Oct 7, 2025
07160fd
feat: multiple epochs in certificate
karlem Oct 9, 2025
236feab
fix: clippy
karlem Oct 20, 2025
1b5ac3b
feat: fix comments
karlem Oct 27, 2025
d7935f5
feat: fix comment
karlem Oct 28, 2025
f9ac821
fix: e2e tests
karlem Oct 28, 2025
993153e
feat: implement coments changes
karlem Oct 29, 2025
6d5734b
feat: add proofs service skeleton
karlem Oct 9, 2025
0736fa6
feat: add persistence and include proofs libraryr
karlem Oct 20, 2025
506de2a
feat: add perstance, real libraries, wather
karlem Oct 21, 2025
ad80adb
feat: implement cache e2e
karlem Oct 23, 2025
25f5d1c
feat: debug issues + make functional
karlem Oct 24, 2025
8a276da
feat: prepare for review, add debug tooling, add observibility
karlem Oct 27, 2025
e83f7a3
feat: remove dead code
karlem Oct 27, 2025
3bebd91
feat: fix clippy and bug
karlem Oct 27, 2025
5027313
feat: comments
karlem Nov 5, 2025
e8f4448
feat: add f3 cert actor
karlem Sep 25, 2025
bb684ff
feat: add fetching from parent
karlem Oct 20, 2025
b1c9aa2
feat: make fetch functional
karlem Oct 7, 2025
7c4910e
feat: add extra checks and tests
karlem Oct 7, 2025
918aa3d
feat: adressed comments and fixed tests
karlem Oct 20, 2025
7680fe5
fix: ci issue
karlem Oct 20, 2025
87c5d44
fix: clippy
karlem Oct 20, 2025
fae9664
feat: fix comments
karlem Oct 27, 2025
1e41560
feat: fix clippy
karlem Oct 28, 2025
a7051da
fix: e2e tests
karlem Oct 28, 2025
1e4a297
fix: clippy
karlem Oct 28, 2025
41ffc97
feat: implement coments changes
karlem Oct 29, 2025
6e5f8a9
feat: add persistence and include proofs libraryr
karlem Oct 20, 2025
b9a05aa
feat: add perstance, real libraries, wather
karlem Oct 21, 2025
02493a1
feat: implement cache e2e
karlem Oct 23, 2025
b7f7e30
feat: debug issues + make functional
karlem Oct 24, 2025
5d54fea
feat: prepare for review, add debug tooling, add observibility
karlem Oct 27, 2025
9c26724
feat: init lifecycle
karlem Oct 28, 2025
bfb692c
feat: progress with top down manager
karlem Oct 29, 2025
9970708
fix: revert genesis and manifest changes to match f3-proofs-cache bas…
karlem Nov 4, 2025
d5396a2
feat: finish implementing e2e
karlem Nov 5, 2025
fbefdde
feat: makes changes after rebase
karlem Dec 1, 2025
801c388
feat: rebase cache
karlem Dec 15, 2025
c493897
fix: after rebase
karlem Dec 19, 2025
3901b96
feat: introduce generialised approach with local f3 cache
karlem Jan 13, 2026
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
340 changes: 183 additions & 157 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ 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 @@ -95,6 +94,7 @@ gcra = "0.6.0"
hex = "0.4"
hex-literal = "0.4.1"
http = "0.2.12"
humantime-serde = "1.1"
im = "15.1.0"
integer-encoding = { version = "3.0.3", default-features = false }
jsonrpc-v2 = { version = "0.11", default-features = false, features = [
Expand Down Expand Up @@ -137,6 +137,7 @@ num-bigint = "0.4"
num-derive = "0.4"
num-traits = "0.2"
num_enum = "0.7.2"
parking_lot = "0.12"
paste = "1"
pin-project = "1.1.2"
prometheus = { version = "0.13", features = ["process"] }
Expand Down Expand Up @@ -185,8 +186,6 @@ 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
185 changes: 153 additions & 32 deletions fendermint/actors/f3-light-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ impl F3LightClientActor {
rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?;

let state = State::new(
params.instance_id,
params.latest_instance_id,
params.latest_finalized_height,
params.power_table,
params.finalized_epochs,
)?;

rt.create(&state)?;
Expand Down Expand Up @@ -70,8 +70,8 @@ impl F3LightClient for F3LightClientActor {
let lc = &state.light_client_state;

Ok(GetStateResponse {
instance_id: lc.instance_id,
finalized_epochs: lc.finalized_epochs.clone(),
latest_instance_id: lc.latest_instance_id,
latest_finalized_height: lc.latest_finalized_height,
power_table: lc.power_table.clone(),
})
}
Expand Down Expand Up @@ -99,17 +99,18 @@ mod tests {
use fil_actors_runtime::SYSTEM_ACTOR_ADDR;
use fvm_ipld_encoding::ipld_block::IpldBlock;
use fvm_shared::address::Address;
use fvm_shared::clock::ChainEpoch;
use fvm_shared::error::ExitCode;

/// Helper function to create test light client state
fn create_test_state(
instance_id: u64,
finalized_epochs: Vec<i64>,
current_instance_id: u64,
latest_finalized_epoch: Option<ChainEpoch>,
power_table: Vec<PowerEntry>,
) -> LightClientState {
LightClientState {
instance_id,
finalized_epochs,
latest_instance_id: current_instance_id,
latest_finalized_height: latest_finalized_epoch,
power_table,
}
}
Expand All @@ -130,9 +131,9 @@ mod tests {

/// Construct the actor and verify initialization
pub fn construct_and_verify(
instance_id: u64,
current_instance_id: u64,
power_table: Vec<PowerEntry>,
finalized_epochs: Vec<i64>,
latest_finalized_epoch: Option<ChainEpoch>,
) -> MockRuntime {
let rt = MockRuntime {
receiver: Address::new_id(10),
Expand All @@ -144,9 +145,9 @@ mod tests {
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);

let constructor_params = ConstructorParams {
instance_id,
latest_instance_id: current_instance_id,
latest_finalized_height: latest_finalized_epoch,
power_table,
finalized_epochs,
};

let result = rt
Expand All @@ -165,33 +166,26 @@ mod tests {

#[test]
fn test_constructor_empty_power_table() {
let _rt = construct_and_verify(0, vec![], vec![]);
let _rt = construct_and_verify(0, vec![], Some(10));
// Constructor test passed if we get here without panicking
}

#[test]
fn test_constructor_with_power_table() {
let power_entries = create_test_power_entries();
let _rt = construct_and_verify(1, power_entries, vec![]);
// Constructor test passed if we get here without panicking
}

#[test]
fn test_constructor_with_finalized_epochs() {
let power_entries = create_test_power_entries();
let _rt = construct_and_verify(1, power_entries, vec![100, 101, 102]);
let _rt = construct_and_verify(1, power_entries, Some(10));
// Constructor test passed if we get here without panicking
}

#[test]
fn test_update_state_success() {
let rt = construct_and_verify(1, create_test_power_entries(), vec![]);
let rt = construct_and_verify(1, create_test_power_entries(), Some(10));

// Set caller to system actor
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);

let new_state = create_test_state(1, vec![100, 101, 102], create_test_power_entries());
let new_state = create_test_state(1, Some(10), create_test_power_entries());
let update_params = UpdateStateParams {
state: new_state.clone(),
};
Expand All @@ -207,16 +201,53 @@ mod tests {
rt.verify();
}

#[test]
fn test_update_state_non_advancing_height() {
let rt = construct_and_verify(1, create_test_power_entries(), Some(10));

// First update to set the finalized height to 102
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
let initial_state = create_test_state(1, Some(10), create_test_power_entries());
let initial_params = UpdateStateParams {
state: initial_state,
};
rt.call::<F3LightClientActor>(
Method::UpdateState as u64,
IpldBlock::serialize_cbor(&initial_params).unwrap(),
)
.unwrap();
rt.reset();

// Try to update with same height
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
let same_height_state = create_test_state(1, Some(10), create_test_power_entries());
let update_params = UpdateStateParams {
state: same_height_state,
};

let result = rt.call::<F3LightClientActor>(
Method::UpdateState as u64,
IpldBlock::serialize_cbor(&update_params).unwrap(),
);

// Should fail with illegal argument
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.exit_code(), ExitCode::USR_ILLEGAL_ARGUMENT);
}

#[test]
fn test_update_state_unauthorized_caller() {
let rt = construct_and_verify(1, create_test_power_entries(), vec![]);
let rt = construct_and_verify(1, create_test_power_entries(), Some(10));

// Set caller to non-system actor
let unauthorized_caller = Address::new_id(999);
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, unauthorized_caller);
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);

let new_state = create_test_state(1, vec![100, 101, 102], create_test_power_entries());
let new_state = create_test_state(1, Some(11), create_test_power_entries());
let update_params = UpdateStateParams { state: new_state };

let result = rt.call::<F3LightClientActor>(
Expand All @@ -233,12 +264,12 @@ mod tests {
#[test]
fn test_get_state() {
let power_entries = create_test_power_entries();
let rt = construct_and_verify(42, power_entries.clone(), vec![]);
let rt = construct_and_verify(42, power_entries.clone(), Some(10));

// Update state first
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
let new_state = create_test_state(42, vec![100, 101, 102], power_entries.clone());
let new_state = create_test_state(42, Some(11), power_entries.clone());
let update_params = UpdateStateParams { state: new_state };
rt.call::<F3LightClientActor>(
Method::UpdateState as u64,
Expand All @@ -255,19 +286,19 @@ mod tests {
.unwrap();

let response = result.deserialize::<GetStateResponse>().unwrap();
assert_eq!(response.instance_id, 42);
assert_eq!(response.finalized_epochs, vec![100, 101, 102]);
assert_eq!(response.latest_instance_id, 42);
assert_eq!(response.latest_finalized_height, Some(11));
assert_eq!(response.power_table, power_entries);
}

#[test]
fn test_state_progression() {
let rt = construct_and_verify(1, create_test_power_entries(), vec![]);
let rt = construct_and_verify(1, create_test_power_entries(), Some(10));

// Update with first state
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
let state1 = create_test_state(1, vec![100, 101, 102], create_test_power_entries());
let state1 = create_test_state(1, Some(100), create_test_power_entries());
let params1 = UpdateStateParams { state: state1 };
rt.call::<F3LightClientActor>(
Method::UpdateState as u64,
Expand All @@ -279,12 +310,102 @@ mod tests {
// Update with second state (higher height)
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
let state2 = create_test_state(1, vec![200, 201, 202], create_test_power_entries());
let state2 = create_test_state(1, Some(200), create_test_power_entries());
let params2 = UpdateStateParams { state: state2 };
let result = rt.call::<F3LightClientActor>(
Method::UpdateState as u64,
IpldBlock::serialize_cbor(&params2).unwrap(),
);
assert!(result.is_ok());
}

#[test]
fn test_instance_id_progression_next_instance() {
let rt = construct_and_verify(100, create_test_power_entries(), Some(10));

// First state at instance 100
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
let initial_state = create_test_state(100, Some(10), create_test_power_entries());
let initial_params = UpdateStateParams {
state: initial_state,
};
rt.call::<F3LightClientActor>(
Method::UpdateState as u64,
IpldBlock::serialize_cbor(&initial_params).unwrap(),
)
.unwrap();
rt.reset();

// Update to next instance (100 -> 101) should succeed
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
let next_instance_state = create_test_state(101, Some(10), create_test_power_entries());
let update_params = UpdateStateParams {
state: next_instance_state,
};

let result = rt.call::<F3LightClientActor>(
Method::UpdateState as u64,
IpldBlock::serialize_cbor(&update_params).unwrap(),
);
assert!(result.is_ok());
}

#[test]
fn test_instance_id_skip_rejected() {
let rt = construct_and_verify(100, create_test_power_entries(), Some(10));

// First state at instance 100
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
let initial_state = create_test_state(100, Some(10), create_test_power_entries());
let initial_params = UpdateStateParams {
state: initial_state,
};
rt.call::<F3LightClientActor>(
Method::UpdateState as u64,
IpldBlock::serialize_cbor(&initial_params).unwrap(),
)
.unwrap();
rt.reset();

// Try to skip instance (100 -> 102) should fail
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
let skipped_state = create_test_state(102, Some(10), create_test_power_entries());
let update_params = UpdateStateParams {
state: skipped_state,
};

let result = rt.call::<F3LightClientActor>(
Method::UpdateState as u64,
IpldBlock::serialize_cbor(&update_params).unwrap(),
);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.exit_code(), ExitCode::USR_ILLEGAL_ARGUMENT);
}

#[test]
fn test_empty_epochs_rejected() {
let rt = construct_and_verify(1, create_test_power_entries(), Some(10));

rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);

// Try to update with empty finalized_epochs
let invalid_state = create_test_state(1, Some(10), create_test_power_entries());
let update_params = UpdateStateParams {
state: invalid_state,
};

let result = rt.call::<F3LightClientActor>(
Method::UpdateState as u64,
IpldBlock::serialize_cbor(&update_params).unwrap(),
);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.exit_code(), ExitCode::USR_ILLEGAL_ARGUMENT);
}
}
13 changes: 5 additions & 8 deletions fendermint/actors/f3-light-client/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use crate::types::{LightClientState, PowerEntry};
use fil_actors_runtime::runtime::Runtime;
use fil_actors_runtime::ActorError;
use fvm_shared::clock::ChainEpoch;
use serde::{Deserialize, Serialize};

/// State of the F3 light client actor.
Expand All @@ -25,25 +26,21 @@ pub struct State {
impl State {
/// Create a new F3 light client state
pub fn new(
instance_id: u64,
latest_instance_id: u64,
latest_finalized_height: Option<ChainEpoch>,
power_table: Vec<PowerEntry>,
finalized_epochs: Vec<fvm_shared::clock::ChainEpoch>,
) -> Result<State, ActorError> {
let state = State {
light_client_state: LightClientState {
instance_id,
finalized_epochs,
latest_instance_id,
latest_finalized_height,
power_table,
},
};
Ok(state)
}

/// Update light client state
///
/// This method should only be called from consensus code path which
/// contains the lightclient verifier. No additional validation is
/// performed here as it's expected to be done by the verifier.
pub fn update_state(
&mut self,
_rt: &impl Runtime,
Expand Down
Loading
Loading