Repository navigation
feat(seinode): declarative data reset with a start guard (spec 009, PLT-1342) - #594
Conversation
spec.dataResetGeneration requests one data reset per increase on a nodeConfig SeiNode; status.dataResetGeneration records the last value handled. CEL keeps the counter monotonic and nodeConfig-only. The planner builds the reset plan before any other Running-phase plan: observe-image, mark-not-ready, stop-seid, reset-data, record-data-reset, mark-ready. One sequence serves every case, because a configRef change is not observed as drift. mark-ready is now start-guarded: while a reset is pending, a plan's mark-ready and a MarkReady SeiNodeTask fail terminally and submit nothing. A plan built before the counter bump can no longer release seid onto old data; the planner rebuilds from the current spec. reset-data keeps priv_validator_state.json in place instead of deleting and rewriting it, so a crash mid-wipe cannot zero a validator's sign state. It refuses while a seid or seidb process runs in the pod, and when config.toml moves [priv-validator] state-file. DataResetInProgress reports ResetPending, ResetRunning, ResetComplete, and ResetFailed (sticky until a retry succeeds), with events. Refs: PLT-1342, spec PR #590 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: PLT-1342 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- reset-data reads both sign-state spellings, CometBFT's [priv-validator] state-file and sei-config's [priv_validator] state_file, and refuses on a value it cannot read. The guard read only the first spelling, so the second could move the sign state under the wipe. - The reset plan submits reset-data-keep-sign-state, a wire type only a sidecar that keeps the sign state accepts. Against an older sidecar the submission fails and retries with seid held, instead of zeroing a validator's sign state when the controller deploys first. - A reset plan whose final mark-ready the start guard refused (the counter rose again, or a hold arrived) reads ResetComplete with "start deferred", not ResetFailed: the wipe ran and was recorded. Refs: PLT-1342 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@seidroid review |
There was a problem hiding this comment.
Adds spec.dataResetGeneration, which lets a nodeConfig SeiNode wipe its data/ directory through a dedicated reset plan. A start guard on every mark-ready path keeps seid held while a reset is pending, and a new sidecar wire type keeps priv_validator_state.json through the wipe. I approve because the guard, the counter handling, the plan ordering and the sign-state keep hold up against the tree and nothing blocks; two suggestions and one nit remain. Of codex's two findings I kept neither. sei-tendermint maps the sign state only from [priv-validator] state-file, so seid never reads a root-level priv_validator_state_file. tomlpatch.ReadTOML returns an empty map when the file is missing, so TestResetData_MissingDataDirIsSuccess still passes.
Non-blocking
1 finding on the changed lines, as inline comments.
- While a reset is pending, a MarkReady SeiNodeTask now fails terminally, which changes how that kind behaves. CLAUDE.md requires updating the seinode-task design doc in bdchatham-designs for such a change, and the gov-ops skill cites its headings. Nothing in this PR shows that the doc's MarkReady section records the new refusal.
1 nit, not posted on the code
internal/controller/node/controller.go:336— This compares only the reconcile-start condition with the end state. On a deferred start, handleTerminalPlan sets ResetComplete and markDataResetStarted overwrites it with ResetRunning in the same reconcile. DataResetComplete is therefore never recorded for the reset that did run, although the PR says every transition records an event.
seidroid review · decision approve · session e5b46a97d73e48efa9f57026ab6ebb6b · turn resp_claude_82e1b3eb203b5e9703a9638e59258940 · item 8fc27e85400e56319c3d4a4e5dc3d814
Findings: 0 blocking | 2 non-blocking | 1 posted inline
Addresses seidroid's suggestion and nit on #594. - An update plan whose final mark-ready the start guard refused now sets NodeUpdateInProgress=False/UpdateComplete with "start deferred". The roll landed; UpdateFailed would have stayed until the next update. - A reset plan deferred because the counter rose again reads True/ResetPending for the newer value, not ResetComplete. - A ResetRunning to ResetRunning move for a newer counter records a DataResetStarted event. Refs: PLT-1342 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@seidroid review |
There was a problem hiding this comment.
Since the last review, a stale update plan that the start guard defers now ends with NodeUpdateInProgress=False/UpdateComplete instead of UpdateFailed. A counter that rises during a reset now reads ResetPending, and the move from one ResetRunning to the next now records DataResetStarted; both are tested, so the open thread and the events nit are addressed. Codex repeated its two findings, and I dropped both again. seid's sei-tendermint config reads the sign-state path only from [priv-validator] state-file, never from a root-level priv_validator_state_file. tomlpatch.ReadTOML returns an empty map when config.toml is missing, so TestResetData_MissingDataDirIsSuccess still passes. That leaves one earlier non-blocker and nothing blocking.
Non-blocking
- While a reset is pending, a MarkReady SeiNodeTask now fails terminally, which changes how that kind behaves. CLAUDE.md requires updating the seinode-task design doc in bdchatham-designs for such a change, and the gov-ops skill cites its headings. Nothing in this PR shows that the doc's MarkReady section records the new refusal.
seidroid review · decision approve · session e5b46a97d73e48efa9f57026ab6ebb6b · turn resp_claude_0ae012d2b2c1ad5a520f9ed315d5ad8f · item a602b5c6725955eeaddba2db8b32c098
Findings: 0 blocking | 1 non-blocking | 0 posted inline
|
On the design-doc note: sei-protocol/bdchatham-designs#140 records the new |
…t branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR SummaryHigh Risk Overview The controller runs a reset plan before any other Running-phase work (observe-image → hold/stop → Sidecar behavior changes: Reviewed by Cursor Bugbot for commit 865c4e8. Bugbot is set up for automated code reviews on this repo. Configure here. |
…390) (#595) * feat(seinode): declarative data reset with a start guard (spec 009) spec.dataResetGeneration requests one data reset per increase on a nodeConfig SeiNode; status.dataResetGeneration records the last value handled. CEL keeps the counter monotonic and nodeConfig-only. The planner builds the reset plan before any other Running-phase plan: observe-image, mark-not-ready, stop-seid, reset-data, record-data-reset, mark-ready. One sequence serves every case, because a configRef change is not observed as drift. mark-ready is now start-guarded: while a reset is pending, a plan's mark-ready and a MarkReady SeiNodeTask fail terminally and submit nothing. A plan built before the counter bump can no longer release seid onto old data; the planner rebuilds from the current spec. reset-data keeps priv_validator_state.json in place instead of deleting and rewriting it, so a crash mid-wipe cannot zero a validator's sign state. It refuses while a seid or seidb process runs in the pod, and when config.toml moves [priv-validator] state-file. DataResetInProgress reports ResetPending, ResetRunning, ResetComplete, and ResetFailed (sticky until a retry succeeds), with events. Refs: PLT-1342, spec PR #590 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(sidecarapi): ResetDataTask keeps the sign state Refs: PLT-1342 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(seinode): maintenance hold for nodeConfig nodes (spec 010) spec.maintenance.hold parks seid at the sidecar start gate with the pod alive, for seid rollback and chain-halt work through kubectl exec. status.maintenanceHold records the hold in effect; every plan that changes it ends with record-maintenance-hold. - Immediate: mark-not-ready, stop-seid. - AfterExit on a running seid: mark-not-ready only, so seid parks when it exits, for example at halt-height. - AfterExit on a parked seid: start once (start-seid-once, the new read-only sidecar task await-seid-start, mark-not-ready), so one commit can stage a recovery binary and halt-height and run to it. - Release: mark-ready. The start guard now also refuses mark-ready while a hold is set; the hold's own start-once step obeys only the reset half. While held, the reset plan and the init plan park instead of releasing, the update plan carries no mark-ready, and no readiness reapproval is built. The StatefulSet still applies, so a template change rolls the pod parked. MaintenanceInProgress reports NotHeld, HoldPending, Held, Armed, and NotApplicable, with MaintenanceHeld/Armed/Released events. Refs: PLT-1390, spec PR #590 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(seinode): close review findings on the data reset - reset-data reads both sign-state spellings, CometBFT's [priv-validator] state-file and sei-config's [priv_validator] state_file, and refuses on a value it cannot read. The guard read only the first spelling, so the second could move the sign state under the wipe. - The reset plan submits reset-data-keep-sign-state, a wire type only a sidecar that keeps the sign state accepts. Against an older sidecar the submission fails and retries with seid held, instead of zeroing a validator's sign state when the controller deploys first. - A reset plan whose final mark-ready the start guard refused (the counter rose again, or a hold arrived) reads ResetComplete with "start deferred", not ResetFailed: the wipe ran and was recorded. Refs: PLT-1342 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(seinode): close review findings on the maintenance hold - The hold half of the start guard acts only on a Running node. A hold set while an init plan ran made the guard refuse that plan's mark-ready, and an init plan failure is terminal. Now seid starts once and the hold plan stops it when the node reaches Running. - await-seid-start fails after two minutes. Unbounded, a pod roll between start-seid-once and seid starting held the node's only plan slot forever; now the plan fails and is built again. Refs: PLT-1390 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(seinode): hold never reports Held while seid can start Addresses seidroid's two blocking findings on #595. - start-seid-once now refuses unless the requested hold is still AfterExit. A start-once plan already running when the operator changed the hold back to Immediate no longer starts seid. - A hold in effect is level-triggered against the observed start gate: when the sidecar reports ready (gate open) under a hold, the planner rebuilds the hold plan. A failed start-once plan that left the gate open is closed again even though want == have. - MaintenanceInProgress reads HoldPending, never Held or Armed, while a hold plan runs or while the gate is open under a hold. - A held init plan keeps its "init" metric label (nit). Refs: PLT-1390 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(seinode): a deferred start is not a failed update or reset Addresses seidroid's suggestion and nit on #594. - An update plan whose final mark-ready the start guard refused now sets NodeUpdateInProgress=False/UpdateComplete with "start deferred". The roll landed; UpdateFailed would have stayed until the next update. - A reset plan deferred because the counter rose again reads True/ResetPending for the newer value, not ResetComplete. - A ResetRunning to ResetRunning move for a newer counter records a DataResetStarted event. Refs: PLT-1342 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(seinode): a hold arriving mid-reset completes the reset Refs: PLT-1390 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(seinode): close an open gate before retrying start-once Addresses seidroid's follow-up suggestion and nit on #595. Open-gate recovery now covers any requested hold, not only want == have. When start-once fails after opening the gate, want stays AfterExit and the hold in effect stays Immediate; the planner now applies Immediate again first (mark-not-ready, stop-seid), so a crash-looping seid cannot keep the gate open across retries. The next plan retries start-once with the gate closed. The registry comment for start-seid-once now names both of its guards. Refs: PLT-1390 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(seinode): record MaintenanceReleased when a release passes through HoldPending Found by the harbor e2e: the release plan runs before NotHeld, so the condition moves HoldPending -> NotHeld and the old Held/Armed-only check never recorded the event spec 010 Req 5.6 requires. Any True state before NotHeld now counts as a release. Refs: PLT-1390 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(planner): clear the hold in effect when a reset releases it A hold removed in the same commit that raises dataResetGeneration ran the plain reset plan. That plan ends in mark-ready but never recorded the release, so status.maintenanceHold stayed set and MaintenanceInProgress read Held while the wipe ran and seid started. - The reset plan for a node whose hold is in effect but no longer requested ends mark-ready -> record-maintenance-hold(""). The plan now counts as a hold plan, so the condition reads HoldPending while it runs. - startDeferred reads only the tasks before mark-ready, so the trailing record step keeps a refused start a deferred start, not ResetFailed. - ResolveMaintenance reports a pending release as HoldPending, as the ReasonHoldPending API comment already states. - Spec 010 Req 4.2, 5.2, and 5.3 now say so. Refs: PLT-1390 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
This PR implements spec 009 (#590). A
nodeConfigSeiNode can now reset its data with one GitOps commit, and nothing can start seid while a reset is pending.API.
spec.dataResetGenerationis the request.status.dataResetGenerationis the last value the controller handled. CEL rejects a lowered or removed counter, and a counter on a node withoutnodeConfig. While the node is notRunning, the handled counter follows the spec, so creating a node never wipes it.Reset plan. It comes before any other Running-phase plan:
observe-image, which waits for the rolloutmark-not-readystop-seidreset-data, with 5 retries and backoffrecord-data-resetmark-readyThe same sequence runs whether or not the pod rolled. The controller does not treat a
configRefchange as drift (planner.go:852-854).Start guard (
internal/task/start_guard.go). Themark-readydeserializer checks the target SeiNode at submission. The plan executor and theMarkReadySeiNodeTask both reach that deserializer, with the SeiNode ascfg.Resource. While a reset is pending,mark-readyfails as terminal and submits nothing. A plan built before the counter bump fails, and the planner builds the reset plan next.Sign state (
sidecar/tasks/reset_data.go).priv_validator_state.json, so a crash during the wipe cannot leave a rerun that writes a zero state.seidorseidbprocess runs in the pod, found through/proc/*/comm.config.tomlsets a non-default[priv-validator] state-file, or cannot be parsed.Status.
DataResetInProgressis always present. Its reasons areNotApplicable,NoResetRequested,ResetPending,ResetRunning,ResetComplete, andResetFailed.ResetFailedstays through retries until a reset succeeds.DataResetStarted,DataResetComplete, orDataResetFailed.Behaviour change
reset-dataused to replace the sign state with a zero state. Now it keeps an existing file. This also applies to theSeiNodeTaskWorkflowStateSync recipe, which only targets non-validators, where the file is unused.Needs sign-off
The new API fields are listed in #590. Please approve their shape before this PR merges.
Tests (each names its spec requirement)
internal/controller/node/envtest/dataresetgeneration_validation_test.goTestDataReset_NotRunningAbsorbsCounterTestDataReset_PlanOrder,TestDataReset_PrecedesUpdateAndReapprovalTestDataReset_StaleUpdatePlanCannotReleaseGate,TestMarkReady_StartGuardTestReconcile_MarkReady_RefusedWhileResetPendingTestDataReset_NoPlanWhenHandledTestResetData_KeepsSignStateAndKeys,TestResetData_RerunAfterPartialWipeKeepsSignState,TestResetData_StateFilePath,TestResetData_RefusesWhileDataUserRunsTestDataReset_ConditionTransitions,TestDataReset_FailureStaysReadableUntilSuccessTestDataReset_CounterRisesDuringResetSC-009 (harbor state sync) and SC-010 (runbook) are judgement checks. They are not run yet. The runbook is sei-protocol/runbooks#162.
Independent review: fixes in 4c8dc77
[priv-validator] state-file. sei-config also writes[priv_validator] state_file.reset-datachecks both spellings, and refuses a value it cannot read (TestResetData_StateFileSpellingsAndShapes).reset-dataand zero the sign state, if the controller shipped first.reset-data-keep-sign-state, which an older sidecar rejects.mark-readyread asResetFailed, although the wipe ran.ResetCompletewith "start deferred" (TestDataReset_DeferredStartIsNotAFailure).mark-readyafter a crash.reset-data.Deploy and rollback order
reset-data-keep-sign-state, a wire type that only this sidecar accepts. If the controller ships first, an older sidecar rejects that type. The reset then retries with seid held, instead of running the oldreset-data, which zeroes the sign state.record-data-resetorreset-data-keep-sign-state, so a persisted reset plan fails withUnknownTaskTypeError.Verification
All commands ran with
env -u GOROOT GOTOOLCHAIN=go1.26.0, because gvm on this machine pinsGOROOTto 1.25.6.gofmt -l(3 modules)go vet ./...(3 modules)golangci-lintv2.12.1--new-from-merge-base=origin/main(3 modules)0 issues.eachmake testmake tidy-check,make verify-generated,make buildstaticcheck ./...govulncheck ./...go.modis unchanged.Refs: PLT-1342, PLT-1389
🤖 Generated with Claude Code