Skip to content

serviceability: halt and resume a feed - #4307

Merged
bgm-malbeclabs merged 4 commits into
mainfrom
feat/feed-halt-and-resume
Sep 11, 2026
Merged

serviceability: halt and resume a feed#4307
bgm-malbeclabs merged 4 commits into
mainfrom
feat/feed-halt-and-resume

Conversation

@bgm-malbeclabs

Copy link
Copy Markdown
Contributor

Implements D1. FeedStatus and the gate that reads it landed with the subscriber work, but nothing could move the status: a staked feed sat in Pending for life, and no feed had a way to stop publishing.

The transition table

From HaltFeed ResumeFeed
Pending refused, nothing is publishing yet refused
Active to Halted refused, already active
Halted refused, already halted to Active
Retired refused, terminal refused, terminal

Each refusal says which way it is wrong, FeedNotHaltable (124) or FeedNotResumable (125), rather than sharing one error. A caller that halts twice should learn it did not stop something a second time, not read a generic argument failure.

Pending to Active is deliberately absent. That step re-reads the stake mirror, which is the only thing that catches a mirror corrected after it admitted a feed, so it belongs with the code that does it (G1). Leaving it out here keeps the two apart rather than half-implementing it.

Who can sign

The feed's own builder may sign either instruction. No other feed instruction consults feed.builder at all; they authorize on FEED_AUTHORITY | FOUNDATION and stop there.

Both paths are needed, for different reasons:

  • The builder, because RFC-28 makes halt the builder's lever and doubles it as a way to rotate the upstream source without redeploying. A builder that cannot halt its own feed cannot rotate either.
  • A FEED_AUTHORITY or FOUNDATION key, because a feed whose builder has gone quiet must still be stoppable, and every other feed instruction already works that way.

A catalog feed has no builder, so only the second path applies to it. The check compares against the default pubkey explicitly rather than relying on nobody being able to sign as it.

Testing Verification

Five tests in a new feed_lifecycle_test.rs. A new file rather than helpers bolted onto feed_test.rs, because its init_staked hardcodes a builder the test cannot sign as, and other tests depend on that.

  • Halt then resume, asserting the status at each step and that the feed's name and groups are untouched. Halt is a status change, not an edit.
  • Halting a halted feed is refused, so a no-op cannot be mistaken for having stopped something.
  • Resuming an active feed is refused, for the same reason.
  • A Pending feed neither halts nor resumes. This also guards G1's territory: nothing here moves a feed out of Pending.
  • The builder is authorized on its own feed, and a stranger is not. Both calls fail, because no Pending feed can be halted by anyone and G1 does not exist yet to make a staked feed Active. The errors are what distinguish them: the builder reaches the status check and gets FeedNotHaltable, the stranger does not get past authorization and gets NotAllowed.

That last one is the honest shape of the test available today. A staked feed cannot reach Active until G1, so the builder's successful halt cannot be tested yet, and asserting on which check stopped the call is the strongest available evidence that the authorization path works.

Full suite passes: 327 serviceability tests plus the 5 new ones, 86 in the instruction crate, clippy --all-targets -Dwarnings and fmt --check clean.

One thing the compiler could not catch, worth knowing for the next variant: DoubleZeroInstruction::unpack matches the leading byte by hand with a catch-all, and From<ProgramError> for DoubleZeroError matches the code the same way. Both compile fine with a variant missing. The first showed up as InvalidInstructionData at runtime and the second as a round-trip test failure.

`FeedStatus` and the gate that reads it landed with the subscriber work, but
nothing could move the status. A staked feed sat in `Pending` for life and no
feed had a way to stop publishing.

Two instructions and the transitions they refuse. Halt goes only from
`Active`, resume only from `Halted`, and each refusal says which way it is
wrong rather than sharing one error. `Pending` to `Active` is deliberately
absent: that step re-reads the stake mirror, so it belongs with the code that
does, and leaving it out here keeps the two apart.

The feed's own builder may sign either one, which no other feed instruction
allows. RFC-28 makes halt the builder's lever and doubles it as a way to
rotate the upstream source without redeploying, so a builder that cannot halt
its own feed cannot do either. A `FEED_AUTHORITY` or `FOUNDATION` key may
sign as well, because a feed whose builder has gone quiet must still be
stoppable.
@bgm-malbeclabs
bgm-malbeclabs requested review from a team and a lite review from Copilot September 9, 2026 23:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The on-chain lifecycle transitions, authorization rules, instruction wiring, error mappings, and new integration tests are consistent and complete for the stated D1 scope.

Pull request overview

This pull request adds feed lifecycle controls to the DoubleZero serviceability on-chain program so an authorized signer can halt an Active feed and resume a Halted feed, unblocking status transitions now that FeedStatus gating exists.

Changes:

  • Add HaltFeed (variant 120) and ResumeFeed (variant 121) instruction variants, entrypoint routing, and processors that enforce the transition table via FeedNotHaltable (124) and FeedNotResumable (125).
  • Add a shared require_feed_writer authorization helper that allows either the feed builder or a FEED_AUTHORITY | FOUNDATION signer to perform lifecycle changes.
  • Add a new SVM test suite covering allowed transitions and refusal cases, including authorization-path differentiation.
File summaries
File Description
smartcontract/programs/doublezero-serviceability/src/processors/feed/halt.rs New processor to transition Active -> Halted with explicit refusal error otherwise.
smartcontract/programs/doublezero-serviceability/src/processors/feed/resume.rs New processor to transition Halted -> Active with explicit refusal error otherwise.
smartcontract/programs/doublezero-serviceability/src/processors/feed/mod.rs Expose halt/resume modules and add require_feed_writer to support builder-or-permission authorization.
smartcontract/programs/doublezero-serviceability/src/instructions.rs Add instruction variants 120/121 plus unpack/name/debug wiring.
smartcontract/programs/doublezero-serviceability/src/entrypoint.rs Route HaltFeed / ResumeFeed to the new processors.
smartcontract/programs/doublezero-serviceability/src/error.rs Add FeedNotHaltable/FeedNotResumable and map them to custom codes 124/125.
smartcontract/programs/doublezero-serviceability/tests/feed_lifecycle_test.rs New integration tests for lifecycle transitions and refusal/authorization behavior.
crates/doublezero-serviceability-instruction/src/feed.rs Add instruction builders for halt_feed and resume_feed.
CHANGELOG.md Document the new instructions, transition constraints, and signer rules.
Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@bgm-malbeclabs
bgm-malbeclabs enabled auto-merge (squash) September 10, 2026 00:30

@armcconnell armcconnell left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Checked against edge-feed-spec GLOSSARY.md (a5f5cdf): conforms.
See inline comments.

Comment thread smartcontract/programs/doublezero-serviceability/src/processors/feed/resume.rs Outdated
Comment thread crates/doublezero-serviceability-instruction/src/feed.rs
Three review comments, and the first one is a hole.

Resume took the same signers as halt, so a builder undid an operator's halt
the moment it landed. `Retired` is unreachable until D2 and `DeleteFeed`
refuses a staked feed, so that halt is the only lever an operator has, and a
builder that can reverse it leaves no lever at all. `Feed` now records
`halted_by`, and a halt by anyone other than the builder takes an operator to
lift. A builder's own halt is still the builder's to lift, which is the
source rotation the whole instruction exists for.

Resume also re-proves the stake. A mirror can be corrected downward while a
feed sits halted, so resuming on the check made at creation would let a feed
publish at a rate its stake no longer backs. A staked feed that omits its
mirror is refused rather than read as having no stake to check.

The builders had no test. They now pin the tag byte and the account metas,
which is what catches a wrong variant: `unpack` matches the leading byte by
hand with a catch-all, so the compiler never sees it.

@armcconnell armcconnell left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Checked against edge-feed-spec GLOSSARY.md (a5f5cdf): conforms.
See inline comments.

Comment thread smartcontract/programs/doublezero-serviceability/src/state/feed.rs
Comment thread crates/doublezero-serviceability-instruction/src/feed.rs Outdated
Adding a field to `Feed` broke the fixture generator's exhaustive literal and
left the Python and TypeScript decoders reading a feed that ends one field
early. The Rust compiler caught the first and could not catch the other two.

`halted_by` is appended after `status`, so a feed written before it reads as
halted by nobody. That is right rather than merely convenient: such a feed
cannot have been halted.

`resume_feed` also never appended the stake mirror, so resuming a staked feed
through this builder always failed `StakeMirrorMissing`. It now takes the
mirror, `None` for a catalog feed, and the test pins where it lands.
`CreateFeed` reads the tier from the mirror and writes the feed's key onto it
to claim the stake, and the builder never sent it, so a staked create through
this crate always failed. Its callers passed the account by hand.

Derived rather than taken as a parameter. Unlike `resume_feed`, the args
already carry the stake the mirror is seeded on, so asking a caller for it
would only add a way to get it wrong. A catalog feed sends none, so a caller
that sent neither before is unaffected.

@armcconnell armcconnell left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Checked against edge-feed-spec GLOSSARY.md (887c705): conforms apart from one banned-word use, noted inline.
See inline comments.

Comment thread sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs
@bgm-malbeclabs
bgm-malbeclabs merged commit 681c5bd into main Sep 11, 2026
37 checks passed
@bgm-malbeclabs
bgm-malbeclabs deleted the feat/feed-halt-and-resume branch September 11, 2026 15:42
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.

3 participants