Skip to content

feat(sidecar): gov-software-upgrade handler (PR-C of #163)#177

Merged
bdchatham merged 2 commits into
mainfrom
feat/gov-submit-proposal
May 12, 2026
Merged

feat(sidecar): gov-software-upgrade handler (PR-C of #163)#177
bdchatham merged 2 commits into
mainfrom
feat/gov-submit-proposal

Conversation

@bdchatham
Copy link
Copy Markdown
Contributor

@bdchatham bdchatham commented May 12, 2026

Software-upgrade proposal handler. Single-purpose task type, mirroring gov-vote's shape — no Type discriminator, no dispatch switch. Future proposal types will be separate task types in separate files.

(Previously shipped as a meta-handler with a Type field; refactored after user pushback on the one-way-door risk and the audit-trail weakness of an extensible switch for non-idempotent broadcasts.)

Summary

  • New `TaskGovSoftwareUpgrade` task type + `sidecar/tasks/gov_software_upgrade.go` handler that builds Cosmos gov v1beta1 `MsgSubmitProposal` wrapping a `SoftwareUpgradeProposal`.
  • `GovSoftwareUpgradeTask` Go client SDK helper with matching server-side validation.
  • `InitialDeposit` denom-whitelisted to `usei` (symmetric with `Fees`) — catches wrong-denom typos at the sidecar layer.

⚠️ Rehydration risk shipped knowingly

`MsgSubmitProposal` is NOT chain-idempotent. A crash between `BroadcastSync` and task-result persist re-runs this handler on restart and broadcasts a SECOND proposal with identical content. The operator pays `InitialDeposit` (~10 SEI) twice. For software-upgrade the chain-level execution damage is bounded (upgrade module applies once at the named height); the pattern MUST NOT be reused for non-MVP proposal types without first wiring the pre-broadcast txHash marker from #174.

Risk is documented at file-header scope. Each future proposal-type handler is a separate file deliberately — that file MUST evaluate its own rehydration impact before shipping. The diff shape itself is the audit signal: adding a new proposal type means a new file + a new test file + a new client task type + a new engine registration, not a one-line switch-case PR.

User-confirmed scope call (better than blocking the workstream on #174's hardening lift).

Why per-type handlers instead of one meta-handler

  • Forced per-handler idempotency review. Each new proposal-type handler is a new file with a fresh REHYDRATION WARNING. A meta-handler with a Type discriminator would let `case "community-pool-spend":` slip in as a one-line PR with no structural review signal.
  • Audit-trail clarity. The diff shape forces reviewer attention on the per-Msg analysis.
  • Consistency with gov-vote. Single-purpose Handler/buildXxxMsg shape.

(Note: kube-rbac-proxy in #165 issues one coarse SAR — `create seinodetasks.sei.io` — independent of task body type, so per-handler files do NOT buy authz granularity at the proxy boundary today. The audit-trail argument is the real benefit.)

Follow-ups tracked in #176

Items 1 and 2 in #176 (nested wire-format shape, shared client/server validation) are moot after the refactor.

Test plan

  • `TestBuildSoftwareUpgradeMsg` table — happy path (locks SDK `MsgSubmitProposal.ValidateBasic` and `SoftwareUpgradeProposal` content type) + 10 validation mutations (missing fields, deposit parse failures, zero/non-usei/mixed-denom rejection, nil keyring, missing key)
  • `TestGovSoftwareUpgradeHandler_HappyPath` — end-to-end via fake txClient; locks `SoftwareUpgradeProposal` InterfaceRegistry registration (Any-packed content would fail at `txCfg.TxEncoder` if dropped)
  • `go build ./...` clean
  • `go test ./...` clean
  • `go test -race ./sidecar/tasks/` clean
  • `gofmt -s -l .` clean
  • `golangci-lint run --new-from-rev=origin/main ./...` reports 0 issues
  • Coral trio cross-review on the refactor — all clean from security lens; platform + k8s findings synthesized into final commit

🤖 Generated with Claude Code

bdchatham and others added 2 commits May 12, 2026 14:43
Software-upgrade proposals only in MVP. Composes on the sign-tx
foundation (#170 + #173) and mirrors the gov-vote shape (#175).

REHYDRATION RISK SHIPPED KNOWINGLY: MsgSubmitProposal is NOT
chain-idempotent. A crash between BroadcastSync and task-result
persist re-runs this handler on pod restart, which re-signs at
sequence+1 and broadcasts a SECOND proposal with identical content.
The operator pays InitialDeposit (~10 SEI) twice and two duplicate
proposals appear on chain. For software-upgrade the chain-level
damage is bounded — the upgrade module applies once at the named
height — but the pattern propagates to non-MVP proposal types
(community-pool-spend = double-spend) which MUST NOT reuse this
shape without first wiring the pre-broadcast txHash marker from #174.

The risk is documented loudly: file-header warning, Handler()
docstring, client-side GovSubmitProposalTask doc. Engine UUID
dedupe protects same-UUID API resubmission but not the
crash-mid-broadcast window.

InitialDeposit is denom-whitelisted to usei (symmetric with Fees from
#170) — catches a wrong-denom typo at the sidecar layer instead of
waiting for the chain CheckTx rejection after sign + broadcast cost.

Six follow-ups tracked in #176: nested wire-format shape for typed
proposal params, shared client/server validation when a second
proposal type lands, UpgradeHeight lower-bound vs current chain height
(post-#165), title/description length caps (post-#165), run-value
ctx plumbing for forensic correlation, process-start banner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rename gov-submit-proposal to gov-software-upgrade as a single-purpose
task. Drop the Type discriminator and the buildProposalContent dispatch
switch. Future proposal types (param-change, community-pool-spend, ...)
will be separate task types in separate files.

Load-bearing reasons:
- Forced per-handler idempotency analysis. The meta-shape made adding
  a non-idempotent proposal type a one-line switch-case PR with no
  structural review signal. Per-type files force a fresh PR with a
  fresh REHYDRATION WARNING block — the diff shape itself is the
  audit signal.
- Consistency with gov-vote. Both gov handlers now follow the same
  Handler/buildXxxMsg single-purpose shape.

(Kube-rbac-proxy issues ONE coarse SAR — create seinodetasks.sei.io —
independent of task body type, so per-handler files do NOT buy authz
granularity at the proxy boundary today. The audit-trail argument is
the real benefit.)

Renames:
- sidecar/tasks/gov_submit_proposal.go → gov_software_upgrade.go
- GovSubmitProposalRequest → GovSoftwareUpgradeRequest (no Type field)
- GovProposer → GovSoftwareUpgrader
- buildSubmitProposalMsg + buildProposalContent → buildSoftwareUpgradeMsg
- engine.TaskGovSubmitProposal → engine.TaskGovSoftwareUpgrade
- client.GovSubmitProposalTask → client.GovSoftwareUpgradeTask (no Type field)

Dropped ProposalTypeSoftwareUpgrade constant (no consumer).

#176 items 1 and 2 (nested wire-format shape, shared client/server type
dispatch validation) are moot after the refactor — closing those.
Item 3+ (UpgradeHeight floor, length caps, run-ctx, banner) still
binds future handlers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bdchatham bdchatham changed the title feat(sidecar): gov-submit-proposal handler (PR-C of #163) feat(sidecar): gov-software-upgrade handler (PR-C of #163) May 12, 2026
@bdchatham bdchatham merged commit 1fe93dc into main May 12, 2026
3 checks passed
@bdchatham bdchatham deleted the feat/gov-submit-proposal branch May 12, 2026 21:59
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