Skip to content

fix(sequencer): stop signalling already-executed governance payloads - #24764

Merged
PhilWindle merged 2 commits into
merge-train/spartan-v5from
spl/a-1422-sequencer-keeps-signalling-a-governance-payload
Jul 27, 2026
Merged

fix(sequencer): stop signalling already-executed governance payloads#24764
PhilWindle merged 2 commits into
merge-train/spartan-v5from
spl/a-1422-sequencer-keeps-signalling-a-governance-payload

Conversation

@spalladino

@spalladino spalladino commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Context

A mainnet sequencer kept casting governance signals for a payload whose proposal had already been executed: the outer Multicall3 tx succeeds but the inner signalWithSig reverts (swallowed by allowFailure), wasting ~100k gas per slot. Two client-side bugs allowed this: executed proposals were treated like any other terminal state, so the payload was re-signalled every round; and the TS ProposalState enum was missing Droppable (present in IGovernance.sol since July 2025), so decoding an Expired proposal threw, and the publisher failed open and signalled anyway. Additionally, since the executed payload upgraded the canonical rollup, proposers of the old rollup kept signalling on a GovernanceProposer now keyed to a different instance, where their signals always revert.

Approach

  • Fix the ProposalState enum to match IGovernance.sol (9 states including Droppable) and drive the sweep from an explicit LIVE_PROPOSAL_STATES set.
  • Replace the boolean hasActiveProposalWithPayload sweep with getPayloadProposalStatus returning 'live' | 'executed' | 'none'. The publisher stops signalling a payload whose proposal was executed, unless the new GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE config is set (L1 permits re-proposing an executed payload, and some payloads are designed to be re-executed). Live proposals take precedence over executed ones, executed verdicts are memoized in-process, and the sweep remains a bounded lookback capped by the protocol-wide proposal lifetime.
  • Add a canonicality guard to enqueueCastSignalHelper: resolve the canonical rollup via getRollupAddress() and skip the signal when the configured rollup is not canonical. Only the canonical rollup's current proposer can signal, so a non-canonical node would otherwise waste gas on a reverting signal. Because the guard returns early on a mismatch, round accounting and the EIP-712 digest use the configured (verified-canonical) rollup address.
  • The proposal-status guard fails open on RPC errors — at worst a duplicate signal the contract simply counts alongside others in the round — so a flaky L1 endpoint cannot silence governance participation. A failure to resolve the canonical rollup instead skips the signal for that slot and is retried on the next one.

API changes

ReadOnlyGovernanceContract.hasActiveProposalWithPayload and the GovernanceProposerContract wrapper are replaced by getPayloadProposalStatus(payload). The redundant GovernanceProposerContract.getInstance() wrapper is removed in favour of the existing getRollupAddress(). New optional sequencer config governanceProposerForcePayloadVote (env GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE, default false).

To be forward-ported to merge-train/spartan after landing on the v5 line.

Fixes A-1422

…s executed

A mainnet sequencer kept signalling a governance payload whose proposal had
already been executed, wasting ~100k gas per slot on a signal the canonical
rollup rejected. Three fixes:

- Fix the `ProposalState` enum, which was missing `Droppable` (Solidity
  `IGovernance` has 9 states). The mismatch made Solidity `Expired` decode as
  out-of-range and throw in `asProposalState`, so the publisher failed open and
  signalled anyway. Add an explicit `LIVE_PROPOSAL_STATES` set for the sweep.

- Replace `hasActiveProposalWithPayload` (boolean) with `getPayloadProposalStatus`
  returning `'live' | 'executed' | 'none'`, matching a proposal by its stored
  payload directly or via its GSEPayload wrapper. The publisher now stops
  signalling an executed payload (memoized in-process, live takes precedence over
  executed) unless `GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE` is set, for payloads
  designed to be re-executed.

- Add a canonicality guard: resolve the canonical rollup instance once and skip
  the signal when the configured rollup is not canonical, reusing that instance
  for round accounting and the EIP-712 digest so the read cannot race a
  canonical-rollup change.
…ser API

Resolve the canonical rollup via getRollupAddress() instead of the getInstance()
wrapper, dropping the now-unused instance parameter threaded through
createSignalRequestWithSignature. Since the guard returns early when the configured
rollup is not canonical, round accounting and the EIP-712 digest can just use the
configured rollup address. Also drop the redundant sawExecuted flag from
getPayloadProposalStatus (the executed verdict is already memoized in the set).
@PhilWindle PhilWindle added the port-to-next Forward-port this merged PR into next label Jul 27, 2026
@PhilWindle
PhilWindle enabled auto-merge July 27, 2026 10:18
@PhilWindle
PhilWindle merged commit c5c4993 into merge-train/spartan-v5 Jul 27, 2026
28 checks passed
@PhilWindle
PhilWindle deleted the spl/a-1422-sequencer-keeps-signalling-a-governance-payload branch July 27, 2026 10:18
AztecBot pushed a commit that referenced this pull request Jul 27, 2026
…24764)

## Context

A mainnet sequencer kept casting governance signals for a payload whose
proposal had already been executed: the outer Multicall3 tx succeeds but
the inner `signalWithSig` reverts (swallowed by `allowFailure`), wasting
~100k gas per slot. Two client-side bugs allowed this: executed
proposals were treated like any other terminal state, so the payload was
re-signalled every round; and the TS `ProposalState` enum was missing
`Droppable` (present in `IGovernance.sol` since July 2025), so decoding
an `Expired` proposal threw, and the publisher failed open and signalled
anyway. Additionally, since the executed payload upgraded the canonical
rollup, proposers of the old rollup kept signalling on a
`GovernanceProposer` now keyed to a different instance, where their
signals always revert.

## Approach

- Fix the `ProposalState` enum to match `IGovernance.sol` (9 states
including `Droppable`) and drive the sweep from an explicit
`LIVE_PROPOSAL_STATES` set.
- Replace the boolean `hasActiveProposalWithPayload` sweep with
`getPayloadProposalStatus` returning `'live' | 'executed' | 'none'`. The
publisher stops signalling a payload whose proposal was executed, unless
the new `GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE` config is set (L1
permits re-proposing an executed payload, and some payloads are designed
to be re-executed). Live proposals take precedence over executed ones,
executed verdicts are memoized in-process, and the sweep remains a
bounded lookback capped by the protocol-wide proposal lifetime.
- Add a canonicality guard to `enqueueCastSignalHelper`: resolve the
canonical rollup via `getRollupAddress()` and skip the signal when the
configured rollup is not canonical. Only the canonical rollup's current
proposer can signal, so a non-canonical node would otherwise waste gas
on a reverting signal. Because the guard returns early on a mismatch,
round accounting and the EIP-712 digest use the configured
(verified-canonical) rollup address.
- The proposal-status guard fails open on RPC errors — at worst a
duplicate signal the contract simply counts alongside others in the
round — so a flaky L1 endpoint cannot silence governance participation.
A failure to resolve the canonical rollup instead skips the signal for
that slot and is retried on the next one.

## API changes

`ReadOnlyGovernanceContract.hasActiveProposalWithPayload` and the
`GovernanceProposerContract` wrapper are replaced by
`getPayloadProposalStatus(payload)`. The redundant
`GovernanceProposerContract.getInstance()` wrapper is removed in favour
of the existing `getRollupAddress()`. New optional sequencer config
`governanceProposerForcePayloadVote` (env
`GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE`, default `false`).

To be forward-ported to `merge-train/spartan` after landing on the v5
line.

Fixes A-1422
@AztecBot

Copy link
Copy Markdown
Collaborator

✅ Successfully ported to port-to-next-staging #24964.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

port-to-next Forward-port this merged PR into next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants