Skip to content

Commit 5bd1d1e

Browse files
committed
feat: update based on next comments
1 parent 6958080 commit 5bd1d1e

File tree

15 files changed

+343
-82
lines changed

15 files changed

+343
-82
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fendermint/actors/f3-light-client/src/lib.rs

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2021-2024 Protocol Labs
22
// SPDX-License-Identifier: Apache-2.0, MIT
33

4-
use crate::state::State;
4+
use crate::state::{PowerEntryValue, State};
55
use crate::types::{ConstructorParams, GetStateResponse, PowerEntry, UpdateStateParams};
66
use fil_actors_runtime::builtin::singletons::SYSTEM_ACTOR_ADDR;
77
use fil_actors_runtime::runtime::{ActorCode, Runtime};
@@ -58,6 +58,38 @@ impl F3LightClient for F3LightClientActor {
5858
rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?;
5959

6060
rt.transaction(|st: &mut State, rt| {
61+
// Basic monotonicity checks to prevent accidental rewinds or no-op updates.
62+
//
63+
// Note: multiple epochs can be proven under the same certificate instance, so
64+
// `latest_instance_id` may stay the same across updates, but it must never go
65+
// backwards and must not jump by more than 1.
66+
//
67+
// Also, we allow re-applying the same update (idempotency) by permitting equality.
68+
if params.latest_finalized_height < st.light_client_state.latest_finalized_height {
69+
return Err(actor_error!(
70+
illegal_argument,
71+
"latest_finalized_height went backwards: {} < {}",
72+
params.latest_finalized_height,
73+
st.light_client_state.latest_finalized_height
74+
));
75+
}
76+
if params.latest_instance_id < st.light_client_state.latest_instance_id {
77+
return Err(actor_error!(
78+
illegal_argument,
79+
"latest_instance_id went backwards: {} < {}",
80+
params.latest_instance_id,
81+
st.light_client_state.latest_instance_id
82+
));
83+
}
84+
if params.latest_instance_id > st.light_client_state.latest_instance_id + 1 {
85+
return Err(actor_error!(
86+
illegal_argument,
87+
"latest_instance_id jumped: {} > {}",
88+
params.latest_instance_id,
89+
st.light_client_state.latest_instance_id + 1
90+
));
91+
}
92+
6193
st.update_state(
6294
rt,
6395
params.latest_instance_id,
@@ -77,15 +109,19 @@ impl F3LightClient for F3LightClientActor {
77109

78110
// Materialize the current power table for convenience.
79111
let power_table = {
80-
let m = fil_actors_runtime::Map2::<_, u64, PowerEntry>::load(
112+
let m = fil_actors_runtime::Map2::<_, u64, PowerEntryValue>::load(
81113
rt.store(),
82114
&lc.power_table_root,
83115
fil_actors_runtime::DEFAULT_HAMT_CONFIG,
84116
"f3_power_table",
85117
)?;
86118
let mut out = Vec::new();
87-
m.for_each(|_k, v| {
88-
out.push(v.clone());
119+
m.for_each(|id, v| {
120+
out.push(PowerEntry {
121+
id,
122+
public_key: v.public_key.clone(),
123+
power_be: v.power_be.clone(),
124+
});
89125
Ok(())
90126
})?;
91127
out.sort_by_key(|e| e.id);
@@ -128,16 +164,25 @@ mod tests {
128164

129165
/// Helper function to create test power entries
130166
fn create_test_power_entries() -> Vec<PowerEntry> {
167+
fn u64_to_power_be(x: u64) -> Vec<u8> {
168+
if x == 0 {
169+
return Vec::new();
170+
}
171+
let bytes = x.to_be_bytes();
172+
let first = bytes.iter().position(|b| *b != 0).unwrap_or(bytes.len());
173+
bytes[first..].to_vec()
174+
}
175+
131176
vec![
132177
PowerEntry {
133178
id: 1,
134179
public_key: vec![1, 2, 3],
135-
power: 100,
180+
power_be: u64_to_power_be(100),
136181
},
137182
PowerEntry {
138183
id: 2,
139184
public_key: vec![4, 5, 6],
140-
power: 200,
185+
power_be: u64_to_power_be(200),
141186
},
142187
]
143188
}
@@ -200,7 +245,7 @@ mod tests {
200245

201246
let update_params = UpdateStateParams {
202247
latest_instance_id: 1,
203-
latest_finalized_height: 10,
248+
latest_finalized_height: 11,
204249
power_table: create_test_power_entries(),
205250
};
206251

@@ -219,12 +264,12 @@ mod tests {
219264
fn test_update_state_non_advancing_height() {
220265
let rt = construct_and_verify(1, create_test_power_entries(), 10);
221266

222-
// First update to set the finalized height to 102
267+
// First update to advance the finalized height.
223268
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
224269
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
225270
let initial_params = UpdateStateParams {
226271
latest_instance_id: 1,
227-
latest_finalized_height: 10,
272+
latest_finalized_height: 11,
228273
power_table: create_test_power_entries(),
229274
};
230275
rt.call::<F3LightClientActor>(
@@ -239,7 +284,7 @@ mod tests {
239284
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
240285
let update_params = UpdateStateParams {
241286
latest_instance_id: 1,
242-
latest_finalized_height: 10,
287+
latest_finalized_height: 11,
243288
power_table: create_test_power_entries(),
244289
};
245290

@@ -248,10 +293,8 @@ mod tests {
248293
IpldBlock::serialize_cbor(&update_params).unwrap(),
249294
);
250295

251-
// Should fail with illegal argument
252-
assert!(result.is_err());
253-
let err = result.unwrap_err();
254-
assert_eq!(err.exit_code(), ExitCode::USR_ILLEGAL_ARGUMENT);
296+
// Allowed (idempotency): equality is ok, only rewinds are rejected.
297+
assert!(result.is_ok());
255298
}
256299

257300
#[test]
@@ -329,16 +372,24 @@ mod tests {
329372
// Update with a different power table.
330373
rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR);
331374
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
375+
fn u64_to_power_be(x: u64) -> Vec<u8> {
376+
if x == 0 {
377+
return Vec::new();
378+
}
379+
let bytes = x.to_be_bytes();
380+
let first = bytes.iter().position(|b| *b != 0).unwrap_or(bytes.len());
381+
bytes[first..].to_vec()
382+
}
332383
let new_power_table = vec![
333384
PowerEntry {
334385
id: 1,
335386
public_key: vec![1, 2, 3],
336-
power: 999,
387+
power_be: u64_to_power_be(999),
337388
},
338389
PowerEntry {
339390
id: 3,
340391
public_key: vec![7, 8, 9],
341-
power: 333,
392+
power_be: u64_to_power_be(333),
342393
},
343394
];
344395
let update_params = UpdateStateParams {
@@ -412,7 +463,7 @@ mod tests {
412463
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
413464
let initial_params = UpdateStateParams {
414465
latest_instance_id: 100,
415-
latest_finalized_height: 10,
466+
latest_finalized_height: 11,
416467
power_table: create_test_power_entries(),
417468
};
418469
rt.call::<F3LightClientActor>(
@@ -427,7 +478,7 @@ mod tests {
427478
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
428479
let update_params = UpdateStateParams {
429480
latest_instance_id: 101,
430-
latest_finalized_height: 10,
481+
latest_finalized_height: 12,
431482
power_table: create_test_power_entries(),
432483
};
433484

@@ -447,7 +498,7 @@ mod tests {
447498
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
448499
let initial_params = UpdateStateParams {
449500
latest_instance_id: 100,
450-
latest_finalized_height: 10,
501+
latest_finalized_height: 11,
451502
power_table: create_test_power_entries(),
452503
};
453504
rt.call::<F3LightClientActor>(
@@ -462,7 +513,7 @@ mod tests {
462513
rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]);
463514
let update_params = UpdateStateParams {
464515
latest_instance_id: 102,
465-
latest_finalized_height: 10,
516+
latest_finalized_height: 12,
466517
power_table: create_test_power_entries(),
467518
};
468519

@@ -476,16 +527,16 @@ mod tests {
476527
}
477528

478529
#[test]
479-
fn test_empty_epochs_rejected() {
530+
fn test_height_rewind_rejected() {
480531
let rt = construct_and_verify(1, create_test_power_entries(), 10);
481532

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

485-
// Try to update with empty finalized_epochs
536+
// Try to update with a lower finalized height (rewind).
486537
let update_params = UpdateStateParams {
487538
latest_instance_id: 1,
488-
latest_finalized_height: 10,
539+
latest_finalized_height: 9,
489540
power_table: create_test_power_entries(),
490541
};
491542

fendermint/actors/f3-light-client/src/state.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::types::{LightClientState, PowerEntry};
1010
use fil_actors_runtime::runtime::Runtime;
1111
use fil_actors_runtime::{ActorError, Map2, DEFAULT_HAMT_CONFIG};
1212
use fvm_ipld_blockstore::Blockstore;
13+
use fvm_ipld_encoding::tuple::{Deserialize_tuple, Serialize_tuple};
1314
use fvm_shared::clock::ChainEpoch;
1415
use serde::{Deserialize, Serialize};
1516

@@ -24,7 +25,16 @@ pub struct State {
2425
pub light_client_state: LightClientState,
2526
}
2627

27-
pub(crate) type PowerTable<BS> = Map2<BS, u64, PowerEntry>;
28+
/// Stored HAMT value for power table entries.
29+
///
30+
/// The key of the HAMT is the validator ID, so storing `id` in the value would be redundant.
31+
#[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone, PartialEq, Eq)]
32+
pub(crate) struct PowerEntryValue {
33+
pub public_key: Vec<u8>,
34+
pub power_be: Vec<u8>,
35+
}
36+
37+
pub(crate) type PowerTable<BS> = Map2<BS, u64, PowerEntryValue>;
2838

2939
impl State {
3040
/// Create a new F3 light client state
@@ -38,7 +48,13 @@ impl State {
3848
let mut m = PowerTable::empty(store, DEFAULT_HAMT_CONFIG, "f3_power_table");
3949
for pe in power_table {
4050
let id = pe.id;
41-
m.set(&id, pe)?;
51+
m.set(
52+
&id,
53+
PowerEntryValue {
54+
public_key: pe.public_key,
55+
power_be: pe.power_be,
56+
},
57+
)?;
4258
}
4359
m.flush()?
4460
};
@@ -65,7 +81,13 @@ impl State {
6581
let mut m = PowerTable::empty(rt.store(), DEFAULT_HAMT_CONFIG, "f3_power_table");
6682
for pe in power_table {
6783
let id = pe.id;
68-
m.set(&id, pe)?;
84+
m.set(
85+
&id,
86+
PowerEntryValue {
87+
public_key: pe.public_key,
88+
power_be: pe.power_be,
89+
},
90+
)?;
6991
}
7092
m.flush()?
7193
};

fendermint/actors/f3-light-client/src/types.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ pub struct PowerEntry {
3838
pub id: u64,
3939
/// Public key of the validator
4040
pub public_key: Vec<u8>,
41-
/// Voting power of the validator
42-
pub power: u64,
41+
/// Voting power of the validator, encoded as unsigned big-endian bytes.
42+
///
43+
/// Filecoin power values can exceed 64 bits; storing bytes avoids lossy conversions.
44+
/// `[]` represents zero.
45+
pub power_be: Vec<u8>,
4346
}
4447

4548
/// Constructor parameters for the F3 light client actor

fendermint/app/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ libp2p = { workspace = true }
2222
libp2p-bitswap = { workspace = true }
2323
multiaddr = { workspace = true }
2424
num-traits = { workspace = true }
25+
num-bigint = { workspace = true }
2526
openssl = { workspace = true }
2627
paste = { workspace = true }
2728
prometheus = { workspace = true }

fendermint/app/src/cmd/genesis.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -398,19 +398,24 @@ async fn fetch_f3_params_from_parent(
398398
// Get base power table for the specified instance
399399
let power_table_response = lotus_client.f3_get_power_table(instance_id).await?;
400400

401-
// Convert power entries
401+
// Convert power entries (power can exceed 64 bits; store as big-endian bytes).
402402
let power_table: anyhow::Result<Vec<_>> = power_table_response
403403
.iter()
404404
.map(|entry| {
405405
// Decode base64 public key
406406
let public_key_bytes =
407407
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &entry.pub_key)?;
408-
// Parse the power string to u64
409-
let power = entry.power.parse::<u64>()?;
408+
// Parse the power string as BigInt (decimal) and encode as unsigned big-endian bytes.
409+
let power = num_bigint::BigInt::parse_bytes(entry.power.as_bytes(), 10)
410+
.ok_or_else(|| anyhow::anyhow!("invalid power string '{}'", entry.power))?;
411+
let (sign, power_be) = power.to_bytes_be();
412+
if sign == num_bigint::Sign::Minus {
413+
anyhow::bail!("negative power for participant id {}", entry.id);
414+
}
410415
Ok(types::PowerEntry {
411416
id: entry.id,
412417
public_key: public_key_bytes,
413-
power,
418+
power_be,
414419
})
415420
})
416421
.collect();

fendermint/vm/interpreter/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ arc-swap = "1.6"
4444
base64 = { workspace = true }
4545
ethers = { workspace = true }
4646
hex = { workspace = true }
47+
num-bigint = { workspace = true }
4748
num-traits = { workspace = true }
4849
serde = { workspace = true }
4950
serde_with = { workspace = true }

0 commit comments

Comments
 (0)