Skip to content

Commit 0763b89

Browse files
committed
feat: multiple epochs in certificate
1 parent 48de186 commit 0763b89

File tree

3 files changed

+135
-30
lines changed

3 files changed

+135
-30
lines changed

fendermint/actors/f3-cert-manager/src/lib.rs

Lines changed: 99 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,13 @@ mod tests {
153153
use multihash::{Code, MultihashDigest};
154154

155155
/// Helper function to create a mock F3 certificate
156-
fn create_test_certificate(instance_id: u64, epoch: i64) -> F3Certificate {
156+
fn create_test_certificate(instance_id: u64, finalized_epochs: Vec<i64>) -> F3Certificate {
157157
// Create a dummy CID for power table
158158
let power_table_cid = Cid::new_v1(0x55, Code::Blake2b256.digest(b"test_power_table"));
159159

160160
F3Certificate {
161161
instance_id,
162-
epoch,
162+
finalized_epochs,
163163
power_table_cid,
164164
signature: vec![1, 2, 3, 4], // Dummy signature
165165
certificate_data: vec![5, 6, 7, 8], // Dummy certificate data
@@ -224,7 +224,7 @@ mod tests {
224224
#[test]
225225
fn test_constructor_with_genesis_data() {
226226
let power_entries = create_test_power_entries();
227-
let genesis_cert = create_test_certificate(1, 100);
227+
let genesis_cert = create_test_certificate(1, vec![100, 101, 102]);
228228

229229
let _rt = construct_and_verify(1, power_entries, Some(genesis_cert));
230230
// Constructor test passed if we get here without panicking
@@ -238,7 +238,7 @@ mod tests {
238238
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
239239
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
240240

241-
let new_cert = create_test_certificate(1, 200);
241+
let new_cert = create_test_certificate(1, vec![200, 201, 202]);
242242
let update_params = UpdateCertificateParams {
243243
certificate: new_cert.clone(),
244244
};
@@ -258,14 +258,14 @@ mod tests {
258258

259259
#[test]
260260
fn test_update_certificate_non_advancing_height() {
261-
let genesis_cert = create_test_certificate(1, 100);
261+
let genesis_cert = create_test_certificate(1, vec![100, 101, 102]);
262262
let rt = construct_and_verify(1, vec![], Some(genesis_cert));
263263

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

267-
// Try to update with same or lower height
268-
let same_height_cert = create_test_certificate(1, 100); // Same height
267+
// Try to update with same or lower height (highest epoch is 102, try with 102 or lower)
268+
let same_height_cert = create_test_certificate(1, vec![100, 101, 102]); // Same highest
269269
let update_params = UpdateCertificateParams {
270270
certificate: same_height_cert,
271271
};
@@ -290,7 +290,7 @@ mod tests {
290290
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, unauthorized_caller);
291291
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
292292

293-
let new_cert = create_test_certificate(1, 200);
293+
let new_cert = create_test_certificate(1, vec![200, 201, 202]);
294294
let update_params = UpdateCertificateParams {
295295
certificate: new_cert,
296296
};
@@ -325,7 +325,7 @@ mod tests {
325325

326326
#[test]
327327
fn test_get_certificate_with_data() {
328-
let genesis_cert = create_test_certificate(1, 100);
328+
let genesis_cert = create_test_certificate(1, vec![100, 101, 102]);
329329
let rt = construct_and_verify(1, vec![], Some(genesis_cert.clone()));
330330

331331
rt.expect_validate_caller_any();
@@ -337,7 +337,7 @@ mod tests {
337337

338338
let response = result.deserialize::<GetCertificateResponse>().unwrap();
339339
assert_eq!(response.certificate, Some(genesis_cert));
340-
assert_eq!(response.latest_finalized_height, 100);
340+
assert_eq!(response.latest_finalized_height, 102); // Highest epoch
341341
}
342342

343343
#[test]
@@ -366,7 +366,7 @@ mod tests {
366366
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
367367
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
368368

369-
let cert1 = create_test_certificate(1, 100);
369+
let cert1 = create_test_certificate(1, vec![100, 101, 102]);
370370
let update_params1 = UpdateCertificateParams {
371371
certificate: cert1.clone(),
372372
};
@@ -382,7 +382,7 @@ mod tests {
382382
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
383383
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
384384

385-
let cert2 = create_test_certificate(1, 200);
385+
let cert2 = create_test_certificate(1, vec![200, 201, 202]);
386386
let update_params2 = UpdateCertificateParams {
387387
certificate: cert2.clone(),
388388
};
@@ -398,14 +398,14 @@ mod tests {
398398

399399
#[test]
400400
fn test_instance_id_progression_next_instance() {
401-
let genesis_cert = create_test_certificate(100, 50);
401+
let genesis_cert = create_test_certificate(100, vec![50, 51, 52]);
402402
let rt = construct_and_verify(100, vec![], Some(genesis_cert));
403403

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

407407
// Update to next instance (100 -> 101) should succeed
408-
let next_instance_cert = create_test_certificate(101, 10); // Epoch can be any value
408+
let next_instance_cert = create_test_certificate(101, vec![10, 11, 12]); // Epoch can be any value
409409
let update_params = UpdateCertificateParams {
410410
certificate: next_instance_cert,
411411
};
@@ -420,14 +420,14 @@ mod tests {
420420

421421
#[test]
422422
fn test_instance_id_skip_rejected() {
423-
let genesis_cert = create_test_certificate(100, 50);
423+
let genesis_cert = create_test_certificate(100, vec![50, 51, 52]);
424424
let rt = construct_and_verify(100, vec![], Some(genesis_cert));
425425

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

429429
// Try to skip instance (100 -> 102) should fail
430-
let skipped_cert = create_test_certificate(102, 100);
430+
let skipped_cert = create_test_certificate(102, vec![100, 101, 102]);
431431
let update_params = UpdateCertificateParams {
432432
certificate: skipped_cert,
433433
};
@@ -444,14 +444,14 @@ mod tests {
444444

445445
#[test]
446446
fn test_instance_id_backward_rejected() {
447-
let genesis_cert = create_test_certificate(100, 50);
447+
let genesis_cert = create_test_certificate(100, vec![50, 51, 52]);
448448
let rt = construct_and_verify(100, vec![], Some(genesis_cert));
449449

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

453453
// Try to go backward (100 -> 99) should fail
454-
let backward_cert = create_test_certificate(99, 100);
454+
let backward_cert = create_test_certificate(99, vec![100, 101, 102]);
455455
let update_params = UpdateCertificateParams {
456456
certificate: backward_cert,
457457
};
@@ -475,7 +475,7 @@ mod tests {
475475
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
476476

477477
// First certificate must match genesis_instance_id (50) or be next (51)
478-
let matching_cert = create_test_certificate(50, 100);
478+
let matching_cert = create_test_certificate(50, vec![100, 101, 102]);
479479
let update_params = UpdateCertificateParams {
480480
certificate: matching_cert,
481481
};
@@ -497,7 +497,7 @@ mod tests {
497497
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
498498

499499
// First certificate can also be genesis + 1 (51)
500-
let next_instance_cert = create_test_certificate(51, 100);
500+
let next_instance_cert = create_test_certificate(51, vec![100, 101, 102]);
501501
let update_params = UpdateCertificateParams {
502502
certificate: next_instance_cert,
503503
};
@@ -509,4 +509,83 @@ mod tests {
509509

510510
assert!(result.is_ok());
511511
}
512+
513+
#[test]
514+
fn test_certificate_with_multiple_epochs() {
515+
let rt = construct_and_verify(1, vec![], None);
516+
517+
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
518+
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
519+
520+
// Certificate covering epochs 100-110
521+
let multi_epoch_cert = create_test_certificate(
522+
1,
523+
vec![100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110],
524+
);
525+
let update_params = UpdateCertificateParams {
526+
certificate: multi_epoch_cert,
527+
};
528+
529+
let result = rt.call::<F3CertManagerActor>(
530+
Method::UpdateCertificate as u64,
531+
IpldBlock::serialize_cbor(&update_params).unwrap(),
532+
);
533+
534+
assert!(result.is_ok());
535+
rt.reset();
536+
537+
// Query to verify latest_finalized_height is the highest epoch
538+
rt.expect_validate_caller_any();
539+
let result = rt
540+
.call::<F3CertManagerActor>(Method::GetCertificate as u64, None)
541+
.unwrap()
542+
.unwrap();
543+
544+
let response = result.deserialize::<GetCertificateResponse>().unwrap();
545+
assert_eq!(response.latest_finalized_height, 110); // Highest epoch
546+
}
547+
548+
#[test]
549+
fn test_certificate_empty_epochs_rejected() {
550+
let rt = construct_and_verify(1, vec![], None);
551+
552+
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
553+
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
554+
555+
// Try to update with empty finalized_epochs
556+
let invalid_cert = create_test_certificate(1, vec![]);
557+
let update_params = UpdateCertificateParams {
558+
certificate: invalid_cert,
559+
};
560+
561+
let result = rt.call::<F3CertManagerActor>(
562+
Method::UpdateCertificate as u64,
563+
IpldBlock::serialize_cbor(&update_params).unwrap(),
564+
);
565+
566+
assert!(result.is_err());
567+
let err = result.unwrap_err();
568+
assert_eq!(err.exit_code(), ExitCode::USR_ILLEGAL_ARGUMENT);
569+
}
570+
571+
#[test]
572+
fn test_certificate_single_epoch() {
573+
let rt = construct_and_verify(1, vec![], None);
574+
575+
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
576+
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
577+
578+
// Certificate with only one epoch should work
579+
let single_epoch_cert = create_test_certificate(1, vec![100]);
580+
let update_params = UpdateCertificateParams {
581+
certificate: single_epoch_cert,
582+
};
583+
584+
let result = rt.call::<F3CertManagerActor>(
585+
Method::UpdateCertificate as u64,
586+
IpldBlock::serialize_cbor(&update_params).unwrap(),
587+
);
588+
589+
assert!(result.is_ok());
590+
}
512591
}

fendermint/actors/f3-cert-manager/src/state.rs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ impl State {
2929
) -> Result<State, ActorError> {
3030
let latest_finalized_height = genesis_certificate
3131
.as_ref()
32-
.map(|cert| cert.epoch)
32+
.and_then(|cert| cert.finalized_epochs.iter().max().copied())
3333
.unwrap_or(0);
3434

3535
let state = State {
@@ -47,6 +47,13 @@ impl State {
4747
_rt: &impl Runtime,
4848
certificate: F3Certificate,
4949
) -> Result<(), ActorError> {
50+
// Validate finalized_epochs is not empty
51+
if certificate.finalized_epochs.is_empty() {
52+
return Err(ActorError::illegal_argument(
53+
"Certificate must have at least one finalized epoch".to_string(),
54+
));
55+
}
56+
5057
// Determine current instance ID from latest certificate or genesis
5158
let current_instance_id = self
5259
.latest_certificate
@@ -56,11 +63,16 @@ impl State {
5663

5764
// Validate instance progression
5865
if certificate.instance_id == current_instance_id {
59-
// Same instance: epoch must advance
60-
if certificate.epoch <= self.latest_finalized_height {
66+
// Same instance: highest epoch must advance
67+
let new_highest = certificate
68+
.finalized_epochs
69+
.iter()
70+
.max()
71+
.expect("finalized_epochs validated as non-empty");
72+
if *new_highest <= self.latest_finalized_height {
6173
return Err(ActorError::illegal_argument(format!(
62-
"Certificate epoch {} must be greater than current finalized height {}",
63-
certificate.epoch, self.latest_finalized_height
74+
"Certificate highest epoch {} must be greater than current finalized height {}",
75+
new_highest, self.latest_finalized_height
6476
)));
6577
}
6678
} else if certificate.instance_id == current_instance_id + 1 {
@@ -73,8 +85,12 @@ impl State {
7385
)));
7486
}
7587

76-
// Update state - the transaction will handle persisting this
77-
self.latest_finalized_height = certificate.epoch;
88+
// Update state - set latest_finalized_height to the highest epoch
89+
self.latest_finalized_height = *certificate
90+
.finalized_epochs
91+
.iter()
92+
.max()
93+
.expect("finalized_epochs validated as non-empty");
7894
self.latest_certificate = Some(certificate);
7995

8096
Ok(())
@@ -99,4 +115,13 @@ impl State {
99115
pub fn get_latest_finalized_height(&self) -> ChainEpoch {
100116
self.latest_finalized_height
101117
}
118+
119+
/// Check if a specific parent epoch has been finalized
120+
pub fn is_epoch_finalized(&self, epoch: ChainEpoch) -> bool {
121+
if let Some(cert) = &self.latest_certificate {
122+
cert.finalized_epochs.contains(&epoch)
123+
} else {
124+
false
125+
}
126+
}
102127
}

fendermint/actors/f3-cert-manager/src/types.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ use fvm_shared::clock::ChainEpoch;
1010
pub struct F3Certificate {
1111
/// F3 instance ID
1212
pub instance_id: u64,
13-
/// Epoch/height this certificate finalizes
14-
pub epoch: ChainEpoch,
13+
/// All epochs finalized by this certificate (from ECChain)
14+
/// Must contain at least one epoch
15+
pub finalized_epochs: Vec<ChainEpoch>,
1516
/// CID of the power table used for this certificate
1617
pub power_table_cid: Cid,
1718
/// Aggregated signature from F3 participants
1819
pub signature: Vec<u8>,
19-
/// Raw certificate data for verification
20+
/// Raw certificate data for verification (full Lotus cert with ECChain)
2021
pub certificate_data: Vec<u8>,
2122
}
2223

0 commit comments

Comments
 (0)