Skip to content

refactor(featureset): thread &'static FeatureSet instead of Arc - #652

Merged
varex83 merged 5 commits into
mainfrom
feat/fix-616
Sep 3, 2026
Merged

refactor(featureset): thread &'static FeatureSet instead of Arc#652
varex83 merged 5 commits into
mainfrom
feat/fix-616

Conversation

@varex83agent

Copy link
Copy Markdown
Collaborator

Closes #616

FeatureSet is resolved exactly once — after the cluster lock's fork version feeds the gnosis hotfix — and never mutated, so it lives for the whole process. This replaces the Arc<FeatureSet> threading with a Copy &'static FeatureSet obtained by leaking once at the resolution point.

Changes

  • Resolution point (node/mod.rs): let feature_set: &'static FeatureSet = Box::leak(Box::new(resolve_feature_set(...)?));. All three Arc::clones threading it into the consensus controller, p2p behaviour, and core workflow are dropped (&'static is Copy).
  • Fields drop the Arc: ConsensusController::Config, qbft::Consensus + its Config, TrackerService, InclusionCore, InclusionChecker, WireInputs, WireP2PParams.
  • Timers: get_round_timer_func and the three with_duty constructors take &'static FeatureSet. The new()/Default paths no longer allocate a throwaway FeatureSet::new() — the field is now Option<&'static FeatureSet> (None until bound to a duty, which is the only reader via the round-one proposal-timeout override).
  • Single convention: the second &FeatureSet convention in tracker/analysis.rs (9 signatures) and infosync is collapsed onto &'static FeatureSet.
  • Derived set: tracker_feature_set (wire.rs) leaks its own small masked static instead of deep-cloning the whole set into a second Arc.
  • Docs: the featureset crate module doc now mandates the &'static convention (leak once at startup).
  • Tests: each construct and leak their own set (Box::leak(Box::new(...))), equally cheap per the issue.

Acceptance

  • No Arc<FeatureSet> in the workspace:
    $ rg 'Arc<FeatureSet>' crates/
    (no matches)
    
  • No FeatureSet::new() throwaway allocations for filling fields (timers use Option<&'static> = None).
  • Single passing convention (&'static FeatureSet).
  • The #[allow(clippy::too_many_arguments)] on Tracker::start is retained: even without feature_set, start has 8 parameters, so clippy still requires it (clippy -D warnings passes with it in place).

Verification (all pass)

  • cargo build --workspace --all-features
  • cargo +nightly fmt --all
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test for touched crates (pluto-featureset, pluto-consensus, pluto-core, pluto-infosync, pluto-app) — all green, including the wiring.rs Tier-1 tests.

🤖 Generated with Claude Code

FeatureSet is resolved exactly once (after the cluster lock's fork
version feeds the gnosis hotfix) and never mutated, so it lives for the
whole process. Leak it once at the resolution point and thread a Copy
&'static FeatureSet everywhere it previously flowed as Arc<FeatureSet>:

- node/mod.rs leaks once via Box::leak; drops all Arc::clones threading
  it into the consensus controller, p2p behaviour, and core workflow.
- Fields on ConsensusController::Config, qbft::Consensus (+ its Config),
  TrackerService, InclusionCore, InclusionChecker, WireInputs, and
  WireP2PParams lose the Arc.
- get_round_timer_func and the three round-timer with_duty constructors
  take &'static; their new()/Default no longer allocate a throwaway
  FeatureSet::new() — the field is now Option<&'static FeatureSet>
  (None until bound to a duty, which is the only reader).
- Collapses the second &FeatureSet convention in tracker/analysis.rs and
  infosync onto &'static, so there is a single convention.
- tracker_feature_set (wire.rs) leaks its own small masked static instead
  of deep-cloning into a second Arc.
- Rewrites the featureset crate module doc to mandate &'static.
- Tests construct and leak their own set per test (Box::leak), equally
  cheap.

Closes #616

Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com>
@emlautarom1

Copy link
Copy Markdown
Collaborator

Could you please fix the conflicts and merge main?

@emlautarom1

Copy link
Copy Markdown
Collaborator

Could you please fix the conflicts and merge main?

@varex83

varex83agent and others added 2 commits August 31, 2026 11:11
# Conflicts:
#	crates/core/src/tracker/inclusion.rs
rustdoc rejects intra-doc links to the private report_missed and
report_att_inclusion functions from the public InclusionCore::new docs
under -D warnings. Use plain code spans instead.

Co-Authored-By: Bohdan Ohorodnii <varex83@users.noreply.github.com>

@emlautarom1 emlautarom1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, no semantic changes. Before merging check if we can remove explicit 'static annotations (run your agent against the diff and make it check).

Comment thread crates/core/src/tracker/analysis.rs Outdated
Comment on lines +620 to +621
fn default_feature_set() -> &'static FeatureSet {
Box::leak(Box::new(FeatureSet::new()))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In general, the're are multiple places where we can drop the specific 'static lifetime annotation. This is one case, and by dropping it we can remove the need for this function altogether. Check for all signatures with 'static and see if we can drop it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in a932bf7. I let the compiler decide: stripped 'static from every &FeatureSet occurrence in the workspace, then added it back one error at a time until it compiled. What the borrow checker actually requires is only struct fields, the constructors that feed them, and the timer closures get_round_timer_func builds.

Dropped from: the eight tracker::analysis functions (including this one), infosync::Component::new, and wire::tracker_feature_set (now elided, tying the derived set's lifetime to the input). A &'static FeatureSet coerces on the way in, so no caller changed.

And yes — default_feature_set() is gone; the tests borrow a local &FeatureSet::new(). I also recorded the narrowed rule in the featureset module doc so it doesn't drift back: spell 'static out only where the reference is stored.

Comment thread crates/consensus/src/timer.rs Outdated
feature_set: Arc<FeatureSet>,
/// `None` until bound to a duty; the round-one proposal-timeout override is
/// the only thing that reads it, and it never fires without a duty.
feature_set: Option<&'static FeatureSet>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We could try to merge this field with the duty since they're both either None or Some at the same time. Not major, but removes the need for comments and an implicit dependency.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Merged in a932bf7. All three timers now hold a single duty: Option<BoundDuty>:

struct BoundDuty {
    duty: Duty,
    feature_set: &'static FeatureSet,
}

The implicit dependency and both explanatory comments are gone, and proposal_timeout_duration drops a parameter along with its is_some_and dance:

fn proposal_timeout_duration(bound: Option<&BoundDuty>, round: i64) -> Option<Duration> {
    let bound = bound?;
    if round == 1 && is_proposer(&bound.duty) && bound.feature_set.enabled(Feature::ProposalTimeout)

varex83agent and others added 2 commits September 3, 2026 13:47
Review follow-up on #652.

- Drop `'static` from every `&FeatureSet` signature that only reads the
  set during the call: the eight `tracker::analysis` functions,
  `infosync::Component::new`, and `wire::tracker_feature_set` (its
  lifetime is now elided, tying the derived set to the input). A
  `&'static FeatureSet` coerces on the way in, so callers are unchanged.
  The remaining annotations are exactly the ones the borrow checker
  requires: struct fields, the constructors that feed them, and the
  timer closures built by `get_round_timer_func`.
- The `analysis` tests no longer need a leaking `default_feature_set()`
  helper — they borrow a local `&FeatureSet::new()`.
- Merge each round timer's `feature_set: Option<&'static FeatureSet>`
  into its `duty` field as a single `Option<BoundDuty>`. Both were `None`
  or `Some` together, so the implicit dependency (and the comments
  explaining it) is gone, and `proposal_timeout_duration` loses a
  parameter plus its `is_some_and` dance.
- Document the narrowed convention in the `featureset` module doc.

Co-authored-by: varex83 <bohdan.ohorodnii@nethermind.io>
@varex83
varex83 merged commit 94cca2b into main Sep 3, 2026
16 checks passed
@varex83
varex83 deleted the feat/fix-616 branch September 3, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Thread &'static FeatureSet (leak once at resolution) instead of Arc

3 participants