Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion .config/forest.dic
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
296
299
ABI
Algorand/M
API's
Expand Down Expand Up @@ -235,6 +235,7 @@ signable
Skellam
skippable
Sqlx
SRA
statediff
stateful
stateroots
Expand All @@ -244,6 +245,7 @@ struct/SM
subcall/S
subcommand/S
submodule/S
SWA
swappiness
synchronizer
syscall/S
Expand All @@ -254,6 +256,7 @@ teardown
Terraform
testnet
TiB
timelock/S
tipset/SM
tipsetkey/S
TLS
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@

- [#7552](https://github.com/ChainSafe/forest/issues/7552): `Filecoin.StateGetNetworkParams` now reports `UpgradeSolsticeHeight` (NV29) in place of the `UpgradeXxHeight` placeholder, matching Lotus. The upgrade is not scheduled on any network yet.

- [#7599](https://github.com/ChainSafe/forest/pull/7599): `Filecoin.StateMinerInitialPledgeCollateral` returns an error from NV29 (FIP-0118), matching Lotus: a pre-commit no longer describes a pledge. Use `Filecoin.StateMinerInitialPledgeForSector` with the full sector size as the verified size.

### Removed

### Fixed
Expand Down
9 changes: 8 additions & 1 deletion src/rpc/methods/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,7 @@ impl RpcMethod<3> for StateMinerInitialPledgeCollateral {
const PARAM_NAMES: [&'static str; 3] = ["minerAddress", "sectorPreCommitInfo", "tipsetKey"];
const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
const PERMISSION: Permission = Permission::Read;
const DESCRIPTION: &'static str = "Returns the initial pledge collateral for the specified miner's sector. Deprecated: from NV29 (FIP-0118) every sector gets maximum quality-adjusted power regardless of its deals, so the value is far too low; use StateMinerInitialPledgeForSector instead.";
const DESCRIPTION: &'static str = "Returns the initial pledge collateral for the specified miner's sector. From NV29 (FIP-0118) it returns an error: every sector gets maximum quality-adjusted power regardless of its deals, so a pre-commit no longer describes a pledge. Use StateMinerInitialPledgeForSector instead.";

type Params = (Address, SectorPreCommitInfo, ApiTipsetKey);
type Ok = TokenAmount;
Expand All @@ -1120,6 +1120,13 @@ impl RpcMethod<3> for StateMinerInitialPledgeCollateral {
) -> Result<Self::Ok, ServerError> {
let ts = ctx.chain_store().load_required_tipset_or_heaviest(&tsk)?;

if ctx.state_manager.get_network_version(ts.epoch()) >= NetworkVersion::V29 {
return Err(anyhow::anyhow!(
"StateMinerInitialPledgeCollateral is unsupported from network version 29 (FIP-0118): use StateMinerInitialPledgeForSector"
)
.into());
}
Comment on lines +1123 to +1128

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

will this fail the RPC calibnet tests once NV29 activates on calibnet? Not sure what policy we put there, but should be that if both nodes fail with similar errors then it's okay.


let sector_size = pci
.seal_proof
.sector_size()
Expand Down
69 changes: 69 additions & 0 deletions src/rpc/methods/state/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,75 @@ async fn initial_pledge_collateral_matches_the_sector_pledge() {
);
}

/// Puts nv29 at [`FIXTURE_EPOCH`], because no network schedules Solstice yet.
fn solstice_at_fixture_epoch(mut config: ChainConfig) -> ChainConfig {
config

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you might want to sort this collection, just in case - the ordering there is important.

.height_infos
.get_mut(&Height::Solstice)
.expect("every network lists Solstice")
.epoch = FIXTURE_EPOCH;
config
}

async fn initial_pledge_collateral(
config: ChainConfig,
epoch: ChainEpoch,
) -> Result<TokenAmount, ServerError> {
let (ctx, _) = ctx_at(config, epoch, &Default::default());
// Default seal proof is `Invalid`, so that field has to be set; the rest go unread.
let pre_commit = SectorPreCommitInfo::from(fil_actor_miner_state::v18::SectorPreCommitInfo {
seal_proof: RegisteredSealProofV4::StackedDRG32GiBV1P1,
expiration: epoch + 1_000,
..Default::default()
});
StateMinerInitialPledgeCollateral::handle(
ctx,
(Address::new_id(1000), pre_commit, ApiTipsetKey(None)),
&Default::default(),
)
.await
}

/// From NV29 a pre-commit no longer describes a pledge, so the collateral RPC refuses.
#[rstest]
#[case::mainnet(ChainConfig::mainnet())]
#[case::calibnet(ChainConfig::calibnet())]
#[case::butterflynet(ChainConfig::butterflynet())]
#[case::devnet(ChainConfig::devnet())]
#[tokio::test]
async fn initial_pledge_collateral_is_retired_from_nv29(#[case] config: ChainConfig) {
let config = solstice_at_fixture_epoch(config);
let activation = first_epoch_of(&config, Height::Solstice);

let error = initial_pledge_collateral(config, activation)
.await
.unwrap_err();

assert!(
error
.to_string()
.contains("unsupported from network version 29"),
"{error}"
);
}

#[rstest]
#[case::mainnet(ChainConfig::mainnet())]
#[case::calibnet(ChainConfig::calibnet())]
#[case::butterflynet(ChainConfig::butterflynet())]
#[case::devnet(ChainConfig::devnet())]
#[tokio::test]
async fn initial_pledge_collateral_answers_until_nv29(#[case] config: ChainConfig) {
let config = solstice_at_fixture_epoch(config);
let activation = first_epoch_of(&config, Height::Solstice);

let pledge = initial_pledge_collateral(config, activation - 1)
.await
.unwrap();

assert!(pledge.is_positive());
}

#[rstest]
#[case::no_power_actor(Address::POWER_ACTOR)]
#[case::no_reward_actor(Address::REWARD_ACTOR)]
Expand Down
2 changes: 1 addition & 1 deletion src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions src/shim/clock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
// SPDX-License-Identifier: Apache-2.0, MIT

pub use super::fvm_shared_latest::clock::ChainEpoch;
pub use fil_actors_shared::v19::EPOCHS_IN_DAY;
pub use fil_actors_shared::v19::EPOCHS_IN_HOUR;
pub use fvm_shared3::ALLOWABLE_CLOCK_DRIFT;
pub use fvm_shared3::BLOCKS_PER_EPOCH;
pub use fvm_shared3::clock::EPOCH_DURATION_SECONDS;

pub const SECONDS_IN_DAY: i64 = 86400;
pub const EPOCHS_IN_DAY: i64 = SECONDS_IN_DAY / EPOCH_DURATION_SECONDS;
14 changes: 14 additions & 0 deletions src/state_migration/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ pub(in crate::state_migration) struct ActorMigrationInput {
pub cache: MigrationCache,
}

#[cfg(test)]
impl ActorMigrationInput {
/// Input for migrating one actor head in unit tests; the other fields are placeholders.
pub(in crate::state_migration) fn for_head(head: Cid) -> Self {
Self {
address: Address::new_id(0),
balance: TokenAmount::default(),
head,
prior_epoch: 0,
cache: MigrationCache::new(nonzero_ext::nonzero!(1usize)),
}
}
}

/// Output of actor migration job.
pub(in crate::state_migration) struct ActorMigrationOutput {
/// New CID for the actor
Expand Down
1 change: 1 addition & 0 deletions src/state_migration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod nv25;
mod nv26fix;
mod nv27;
mod nv28;
mod nv29;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
mod type_migrations;

type RunMigration<DB> = fn(&ChainConfig, &DB, &Cid, ChainEpoch) -> anyhow::Result<Cid>;
Expand Down
95 changes: 95 additions & 0 deletions src/state_migration/nv29/market.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2019-2026 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

//! Market actor migration for FIP-0118: re-encodes the state without
//! `pending_deal_allocation_ids`.

use crate::state_migration::common::{
ActorMigration, ActorMigrationInput, ActorMigrationOutput, TypeMigration, TypeMigrator,
};
use crate::utils::db::CborStoreExt as _;
use cid::Cid;
use fil_actor_market_state::v18::State as MarketStateOld;
use fil_actor_market_state::v19::State as MarketStateNew;
use fvm_ipld_blockstore::Blockstore;

pub struct MarketMigrator {
pub new_code_cid: Cid,
}

impl<BS: Blockstore> ActorMigration<BS> for MarketMigrator {
fn migrate_state(
&self,
store: &BS,
input: ActorMigrationInput,
) -> anyhow::Result<Option<ActorMigrationOutput>> {
let in_state: MarketStateOld = store.get_cbor_required(&input.head)?;
let out_state: MarketStateNew = TypeMigrator::migrate_type(in_state, store)?;
let new_head = store.put_cbor_default(&out_state)?;
Ok(Some(ActorMigrationOutput {
new_code_cid: self.new_code_cid,
new_head,
}))
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::db::MemoryDB;
use crate::utils::cid::CidCborExt as _;
use fvm_ipld_encoding::CborStore as _;
use fvm_shared4::econ::TokenAmount;

// The dropped field sits mid-tuple, so a shifted field would show up as a wrong value
// rather than a missing one.
#[test]
fn drops_pending_deal_allocation_ids_and_keeps_every_other_field() {
let store = MemoryDB::default();
let distinct_cid = |tag: u64| store.put_cbor_default(&tag).unwrap();
let in_state = MarketStateOld {
proposals: distinct_cid(1),
states: distinct_cid(2),
pending_proposals: distinct_cid(3),
escrow_table: distinct_cid(4),
locked_table: distinct_cid(5),
next_id: 1234,
deal_ops_by_epoch: distinct_cid(6),
last_cron: 5678,
total_client_locked_collateral: TokenAmount::from_atto(11),
total_provider_locked_collateral: TokenAmount::from_atto(22),
total_client_storage_fee: TokenAmount::from_atto(33),
pending_deal_allocation_ids: distinct_cid(7),
provider_sectors: distinct_cid(8),
};
let head = store.put_cbor_default(&in_state).unwrap();
let new_code_cid = Cid::from_cbor_blake2b256(&"market v19 code").unwrap();

let output = MarketMigrator { new_code_cid }
.migrate_state(&store, ActorMigrationInput::for_head(head))
.unwrap()
.unwrap();

assert_eq!(output.new_code_cid, new_code_cid);
let out_state: MarketStateNew = store.get_cbor_required(&output.new_head).unwrap();
let expected = MarketStateNew {
proposals: in_state.proposals,
states: in_state.states,
pending_proposals: in_state.pending_proposals,
escrow_table: in_state.escrow_table,
locked_table: in_state.locked_table,
next_id: in_state.next_id,
deal_ops_by_epoch: in_state.deal_ops_by_epoch,
last_cron: in_state.last_cron,
total_client_locked_collateral: in_state.total_client_locked_collateral.clone(),
total_provider_locked_collateral: in_state.total_provider_locked_collateral.clone(),
total_client_storage_fee: in_state.total_client_storage_fee.clone(),
provider_sectors: in_state.provider_sectors,
};
// `State` has no `PartialEq`.
assert_eq!(format!("{out_state:?}"), format!("{expected:?}"));
Comment on lines +89 to +90

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

that's annoying - can we derive it? not a blocker but a general good practice for the APIs is to have those derived (even if builtin-actors don't do it - they're not a library themselves after all)

Refer to https://rust-lang.github.io/api-guidelines/interoperability.html

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I will add this in the fil-actor-state for the new release, so we can have this here.

assert_eq!(store.put_cbor_default(&out_state).unwrap(), output.new_head);
// The v19 tuple is one field shorter, so it no longer decodes as v18.
assert!(store.get_cbor::<MarketStateOld>(&output.new_head).is_err());
}
}
Loading
Loading