filestorage: couple storage nodes to staking identity + reserve collateral#452
filestorage: couple storage nodes to staking identity + reserve collateral#452adamkrellenstein wants to merge 1 commit into
Conversation
…teral The storage-slash mechanism (λ_slash, k_f, get_failed_challenges) was inert: a challenge's prover_id was a free-form node_id with no authenticated link to a slashable stake. Add the contract-side coupling: - join_agreement binds each membership to the joining signer's staking identity (x-only pubkey / Holder), stored in AgreementNodes.owners. A failed challenge's prover_id now resolves to a stakeable identity. leave verifies the leaver owns the membership. - Per-identity collateral reservation: node_reservations tracks Σ k_f over an identity's memberships, incremented on join, released on leave, so a node's storage commitments are accounted against one bond. - Views get_node_owner / get_node_reservation expose both for the slasher. Design decisions: - Unified collateral pool: storage providers are validators; their stake backs storage obligations and counts as consensus voting power. No separate storage bond. - The solvency check (staking stake ≥ reservation) and the per-block slash (get_failed_challenges → owner → staking::slash) are deferred to the reactor (core-context), where slash firing already lives, because both require reading staking state / calling staking from filestorage and there is NO native-contract→native-contract view-call precedent in the codebase. The contract owns the per-identity accounting; the reactor owns cross-contract enforcement. Adds a lite test for the owner binding + reservation accumulate/release.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3e8892d. Configure here.
| .node_reservations() | ||
| .set(&owner_str, reserved.add(k_f)?); | ||
| nodes_state.owners().set(&node_id, owner_str); | ||
|
|
There was a problem hiding this comment.
Stale Owner Overwrite Causes Wrong Identity Slashed
Medium Severity
leave_agreement never clears owners[node_id], and join_agreement unconditionally overwrites it with the new joiner's identity at line 328. The active-membership guard (line 296) only blocks re-join if the node is currently active, so after a departure any other identity can claim the same node_id label, silently replacing the owners entry. Any pending failed challenges from the previous owner's tenure then resolve through get_node_owner to the new identity, causing the Phase 2 slasher to slash the wrong staker for failures it had no part in.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 3e8892d. Configure here.
|
Heads-up for whenever this lands: it's built on the filestorage contract surface from before #483 (in-contract registry on append-only kontor-crypto 0.3.0). Once #483 merges, this branch needs a rebase onto the updated filestorage contract (the file-registry / verify-proof WIT and the slot-bearing agreement record changed). |


Stacked on #441 (storage economics). Makes the storage-slash mechanism real: until now
λ_slash,k_f, andget_failed_challenges()existed but a challenge'sprover_idwas a free-formnode_idwith no authenticated link to a slashable stake — so nothing could actually be slashed. This adds the contract-side coupling.What it does
join_agreementbinds each membership to the joining signer's staking identity (x-only pubkey /Holder), stored inAgreementNodes.owners. A failed challenge'sprover_id(=node_id) now resolves to a stakeable identity.leave_agreementverifies the leaver owns the membership.node_reservationstracksΣ k_fover an identity's memberships — incremented on join, released on leave — so a node's storage commitments are accounted against one bond.get_node_owner/get_node_reservationexpose both for the slasher.Design decisions
Unified collateral pool. Storage collateral is consensus stake — storage providers are validators, and a node's stake both backs its storage obligations and counts as its consensus voting power. There is no separate storage bond.
Solvency check + slash are reactor-side, not in the contract. The join-time solvency check (
staking stake ≥ reservation) and the per-block slash (get_failed_challenges → owner → staking::slash(λ_slash·k_f)) are deferred to the reactor (core-context), where the slash firing already lives. Why: both require filestorage to read staking state / call staking, and there is no native-contract→native-contract view-call precedent in the codebase — every existing cross-contract call targets a proc/core function (token::transfer,nft→filestorage::create_agreement);file_registryis a host builtin, not a contract. Rather than pioneer an unvalidated cross-contract view path inside a user-facingjoin, this contract owns the per-identity accounting, and the reactor owns the cross-contract enforcement (it can read bothstakingstake and these reservations each block). If/when native→native view calls are validated, the solvency check can move intojoindirectly.node_idstays a label, bound to the Holder (rather thannode_id == pubkey). This keeps one operator able to run multiple labeled nodes under one stake (each reserving its ownk_f), and avoids breaking the existing multi-node-from-one-signer test/usage pattern, while still authenticating the binding.Scope / follow-ups
Note
Medium Risk
Join/leave semantics and economic accounting change (identity binding and collateral reservations); actual stake checks and slashing remain reactor-side, but mistakes here would break slash resolution or over-commit storage without on-chain solvency in join.
Overview
Adds the contract-side link between storage
node_idlabels and slashable staking identities, so a failed PoR challenge’sprover_idcan resolve to an owner before reactor-sidestaking::slash.On
join_agreement, each membership is bound to the joining signer’s x-only pubkey (AgreementNodes.owners), and that agreement’s per-nodek_fis added tonode_reservations(Σ k_fper identity).leave_agreementnow requires the same identity that joined, and releases that membership’sk_ffrom the reservation (alongside existing leave-fee / replication rules).New views
get_node_ownerandget_node_reservation(WIT + impl) expose the mapping and committed collateral for Phase 2 slashing/solvency wiring in the reactor—not enforced inside this contract yet.A regtest
filestorage_node_staking_couplingtest checks owner binding, reservation growth across two nodes, and release on leave.Reviewed by Cursor Bugbot for commit 3e8892d. Bugbot is set up for automated code reviews on this repo. Configure here.
Merge order: #452 and #456 both modify
core/indexer/tests/contracts/file_storage_tests/native_filestorage_contract.rs. Merge this impl PR (#452) before the test PR #456; #456 rebases on top afterward.