Skip to content

feat: gate unstake behind a fork and discover activations in the worker - #188

Closed
prajwolrg wants to merge 9 commits into
upgradability-params-specfrom
upgradability-fork-activation
Closed

feat: gate unstake behind a fork and discover activations in the worker#188
prajwolrg wants to merge 9 commits into
upgradability-params-specfrom
upgradability-fork-activation

Conversation

@prajwolrg

@prajwolrg prajwolrg commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Description

Stacked on #187, which defined the fork schedule/params types but left them unconsumed. This PR is the first consumer: it puts the worker through a real fork boundary, using unstake as the example feature. The fork keeps its ForkId::Fork1 placeholder name — the bridge gates unstake txs on it, the admin AsmStfVkUpdate action carries the raw id of the fork the new proving artifact implements (rendered into the signing message, so the multisig authorizes the fork id alongside the predicate hash), the emitted AsmStfUpdate log forwards it, and the worker activates the named fork from the next block on, persisting the activation. The chain is the single authority on which fork an upgrade activates: there is no per-operator config to distribute, so a worker cannot silently diverge from the network by missing or misconfiguring one.

The fork id travels as a raw u16 rather than the ForkId enum because the upgrade that activates a fork is enacted by the artifact that predates it — the wire format cannot require knowing the fork. The worker maps ids it knows; an update naming an already-active fork is a logic-neutral VK rotation, and one naming an unknown id is skipped loudly (a binary that cannot apply the fork's rules cannot meaningfully activate it either).

The subtle part is crash/reorg safety, which is carried entirely by ordering: the activation record is written before the enacting block's anchor commit (a committed anchor can never lack its activation), and every sync rebase prunes activations above the base before re-processing (a reorged-out enactment cannot leak into the new branch — its blocks re-discover whatever survives). The activation height is derived, not stored: it is always the block after the enacting one, so ForkActivation records only (enacting_height, fork).

Deliberately out of scope: the prover side (switching proving artifacts across VK upgrades) stays in #183. At the worker level nothing verifies proofs; the new predicate is carried opaquely.

Default behavior is unchanged: unstake remains active since genesis in production params, so a VK update naming Fork1 there acts as a rotation.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature/Enhancement (non-breaking change which adds functionality or enhances an existing one)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactor
  • New or updated tests
  • Dependency update
  • Security fix

Notes to Reviewers

Suggested entry point: the fork id on the action in crates/subprotocols/admin/txs/src/actions/updates/asm_stf_vk.rs (and what it renders for signers), then the activation discovery and rollback in crates/worker/src/state.rs, then the bridge gate in crates/subprotocols/bridge-v1/subprotocol/src/handler.rs — note the gate decides identically in both tx-processing phases, so pre-fork the aux request is never made and aux request/consumption stay in lockstep.

Breaking surface: AsmStfVkUpdate and the AsmStfUpdate log gain a mandatory raw fork id, changing their SSZ layout and the rendered signing message (nothing deployed consumes the old layout).

Tested at two levels, no prover involved: unit tests for the gate boundary and the worker's discovery (activation of the named fork, already-active forks treated as logic-neutral rotations, unknown fork ids skipped, restart resume, rebase rollback), and a worker-level regtest integration test driving the full choreography — genuine musig2-signed unstake ignored pre-fork, admin VK upgrade enactment activates the fork at H+1, the same unstake then removes the operator — plus the reorg path where abandoning the submission block rolls the activation back until the new branch re-enacts it.

Checklist

  • I have performed a self-review of my code.
  • I have commented my code where necessary.
  • I have updated the documentation if needed.
  • My changes do not introduce new warnings.
  • I have added tests that prove my changes are effective or that my feature works.
  • New and existing tests pass with my changes.

Related Issues

Part of #183 (fork-based ASM upgradeability), stacked on #187.

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.84979% with 24 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/storage/src/sled/fork_activation.rs 88.88% 10 Missing ⚠️
bin/asm-runner/src/worker_context.rs 70.58% 5 Missing ⚠️
...tocols/admin/txs/src/actions/updates/asm_stf_vk.rs 63.63% 4 Missing ⚠️
crates/worker/src/test_utils.rs 86.36% 3 Missing ⚠️
.../subprotocols/bridge-v1/subprotocol/src/handler.rs 98.14% 2 Missing ⚠️
Files with missing lines Coverage Δ
bin/asm-runner/src/bootstrap.rs 99.14% <100.00%> (ø)
bin/asm-runner/src/storage.rs 100.00% <100.00%> (ø)
crates/common/src/fork.rs 98.93% <100.00%> (ø)
...tes/extensions/prover/worker/src/backend/native.rs 72.72% <100.00%> (ø)
crates/logs/src/asm_stf.rs 100.00% <100.00%> (ø)
crates/proof/statements/src/program.rs 85.41% <100.00%> (ø)
crates/storage/src/sled/mod.rs 100.00% <ø> (ø)
...ates/subprotocols/admin/subprotocol/src/handler.rs 92.57% <100.00%> (ø)
...protocols/bridge-v1/subprotocol/src/subprotocol.rs 91.60% <100.00%> (ø)
crates/worker/src/builder.rs 91.30% <ø> (ø)
... and 8 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Commit: 57e4fb2
SP1 Execution Results

program cycles gas
asm-stf 136,419,144 134,136,555
moho 5,223,535 5,525,318

@prajwolrg
prajwolrg force-pushed the upgradability-fork-activation branch 2 times, most recently from e24be6d to 4c08b69 Compare July 7, 2026 09:36
@prajwolrg
prajwolrg force-pushed the upgradability-params-spec branch from d2d1a3f to 23a773a Compare July 7, 2026 09:58
@prajwolrg
prajwolrg force-pushed the upgradability-fork-activation branch 2 times, most recently from 1f2c3f1 to f0371ba Compare July 8, 2026 06:00
@prajwolrg
prajwolrg force-pushed the upgradability-params-spec branch from 23a773a to f39db77 Compare July 8, 2026 06:00
@prajwolrg
prajwolrg force-pushed the upgradability-fork-activation branch from f0371ba to 8d64daf Compare July 8, 2026 06:31
@prajwolrg
prajwolrg force-pushed the upgradability-params-spec branch from f39db77 to 3b5a0f0 Compare July 8, 2026 06:31
prajwolrg added 6 commits July 8, 2026 15:44
The bridge gates unstake txs on ForkId::Fork1 in BOTH phases: pre-fork
the aux request is never made and the handler rejects before touching
aux data, keeping aux request/consumption in lockstep.

Behavior is unchanged for now — every executor still runs with the fork
active since genesis (StfParams::all_forks_enabled, replacing the
all-disabled Default at executor call sites).
Persistence for the fork activations the worker will discover from ASM
VK upgrade logs; the worker-side consumer lands in a follow-up commit.

Each record carries the predicate the upgrade enacted: enactment only
surfaces the new VK in the emitted log, so these records are the
worker's durable copy of the key each boundary switched to.
ForkActivationStore joins WorkerContext as its fifth concern, backed by
the sled store in the runner and an in-memory impl in tests. Plumbing
only; the discovery logic that populates it lands next.
Name the fork a VK upgrade activates in the signed action itself: the
chain is the single authority on what an upgrade activates, and the
fork id renders into the signing message so the multisig authorizes it
alongside the predicate hash. The emitted AsmStfUpdate log forwards it
to observers.

The admin does not validate the id: each update is expected to name
the fork it newly activates, and upholding that is on the multisig
when authoring the action. The action carries a raw u16 rather than
the ForkId enum because the upgrade that activates a fork is enacted
by the artifact that predates it, so the wire format cannot require
knowing the fork — and the id's representation may still change.
When a processed block's manifest carries an AsmStfUpdate, the worker
activates the fork the log names from the next block on and persists
the activation — with the predicate the upgrade enacted — keyed by
(enacting height, fork).

Ordering carries the guarantees: the record is written before the
enacting block's anchor commit so a committed anchor can never lack its
activation (crash-replay rewrites the same record), and every sync
rebase prunes activations above the base before re-processing so a
reorged-out enactment cannot leak into the new branch — its blocks
re-discover any activation that survives.

An upgrade naming an already-active fork is an operational flaw on the
authoring side; it is skipped loudly and leaves the schedule untouched
so it cannot retro-raise an activation height. One naming a fork id
unknown to this binary is skipped too, since a binary that cannot apply
the fork's rules cannot meaningfully activate it either.
Unit level: the bridge gate rejects valid unstakes pre-fork (without
touching aux data — the lockstep property that keeps the aux panic
unreachable), flips exactly at the activation height, and the worker's
discovery activates the fork an upgrade names (recording the enacted
VK alongside it), skips upgrades naming an already-active fork (an
authoring flaw that must not retro-raise the activation), skips
unknown fork ids, resumes persisted activations on restart, and rolls
back on rebase.

Integration level: the full choreography against regtest — genuine
musig2-signed unstake ignored pre-fork, admin VK upgrade enactment
activates the fork at H+1, unstake then removes the operator; plus the
reorg path where abandoning the submission block rolls the activation
back until the re-mined update re-enacts on the new branch. The harness
gains a fork schedule knob, a genuine-unstake builder (real N/N key,
full validation passes — only the fork gate decides), and a reorg
helper.
@prajwolrg
prajwolrg force-pushed the upgradability-params-spec branch from 3b5a0f0 to 32bd1d3 Compare July 8, 2026 10:14
@prajwolrg
prajwolrg force-pushed the upgradability-fork-activation branch from 8d64daf to d867438 Compare July 8, 2026 10:14
@prajwolrg
prajwolrg requested review from bewakes and evgenyzdanovich and removed request for evgenyzdanovich July 8, 2026 10:29
prajwolrg added 3 commits July 8, 2026 16:50
Scanning a block's logs for enacted upgrades and persisting the
resulting activations are different concerns. Keep discovery a pure
read that returns the activations to enact, and move the write side
(activation record + in-memory schedule update) behind
apply_fork_activations, separating validation from persistence.
Warn-and-skip left the situation ambiguous: an enacted update naming an
already-active fork is a flawed upgrade with no safe reading — applying
it would retro-raise the activation height, ignoring it silently
diverges from the predicate the chain enacted. Refuse to commit the
enacting block instead, via a dedicated RedundantForkActivation error.

The check also catches two updates naming one fork within a single
block, which the schedule alone cannot see: the first takes effect at
H+1, above the enacting height H, and checking at H+1 instead would
false-fire on crash-replay.

Drop discovery-local error logs along the way: both variants carry the
full context in their Display, and the sync shutdown handler already
logs every fatal sync error.
@prajwolrg

Copy link
Copy Markdown
Collaborator Author

Superseded by #204, which reworks this on top of the spec-versioning model from #202 (base: asm-upgradability). The fork terminology is gone — the admin action now carries the raw spec version (u16) its artifact implements, V0 is the genesis spec — and the unstake gate is dropped since nothing consumes the schedule yet.

@prajwolrg prajwolrg closed this Jul 24, 2026
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.

1 participant