Skip to content

feat(rpc): two-tier admin/master keys for pause RPC, authenticate isPaused - #475

Merged
matthias-wright merged 2 commits into
mainfrom
m/pause-unpause
Oct 28, 2026
Merged

matthias-wright merged 2 commits into
mainfrom
m/pause-unpause

Conversation

@matthias-wright

Copy link
Copy Markdown
Collaborator

Split pause-control authorization into two roles so a compromised lower-privilege key can only halt a node, never resume it:

  • admin: pause, isPaused
  • master: pause, unpause, isPaused

The signed message format (summit-pause-v1:{scope}:{action}:{ts}) is unchanged; the role is determined by the recovered address. A valid signature from a known signer whose role does not permit the action is rejected with a distinct Forbidden (4004) error rather than InvalidSignature. An address configured for both roles gets master.

isPaused now takes (timestamp_secs, signature) like the mutations and is bound to its own action string, so a captured pause signature cannot be used to read state.

Accepted and rejected pause-control calls are logged with action, role and timestamp for an audit trail.

Both addresses remain placeholders (TODO) pending real keys. All changes are behind the permissioned feature.

…aused

Split pause-control authorization into two roles so a compromised
lower-privilege key can only halt a node, never resume it:

- admin:  pause, isPaused
- master: pause, unpause, isPaused

The signed message format (summit-pause-v1:{scope}:{action}:{ts}) is
unchanged; the role is determined by the recovered address. A valid
signature from a known signer whose role does not permit the action is
rejected with a distinct `Forbidden` (4004) error rather than
`InvalidSignature`. An address configured for both roles gets master.

`isPaused` now takes (timestamp_secs, signature) like the mutations and
is bound to its own action string, so a captured `pause` signature
cannot be used to read state.

Accepted and rejected pause-control calls are logged with action, role
and timestamp for an audit trail.

Both addresses remain placeholders (TODO) pending real keys. All
changes are behind the `permissioned` feature.
@matthias-wright
matthias-wright merged commit e8eb8ec into main Oct 28, 2026
4 checks passed
@matthias-wright
matthias-wright deleted the m/pause-unpause branch October 28, 2026 12:19
samlaf added a commit that referenced this pull request Nov 5, 2026
Refs https://linear.app/seismic-systems/issue/SEI-1053

#474 gave
WithdrawalAddressBound code 4004, which Forbidden already had from
#475. Release builds enable
permissioned, so both are live there, and a client can't tell "this node
is bound to another withdrawal address" from "signer not authorized" by
code. Move WithdrawalAddressBound to 4005. Forbidden keeps 4004, since
it has been released since v0.1.2 and WithdrawalAddressBound hasn't been
released yet.

To stop this from happening again, each RpcError code is now its
variant's discriminant, under #[repr(i32)], so the compiler rejects a
duplicate code (E0081). RpcError::code() reads it back, and the From
impl uses that instead of repeating each number. The variants are listed
in code order.

## Possible follow-up: ungate the permissioned variants

The duplicate check only covers variants compiled into the build. With
the four permissioned variants behind #[cfg(feature = "permissioned")],
a clash with one of them only fails an all-features build. CI runs one,
so it's still caught, but a plain cargo check isn't enough.

They're gated because RpcError is private to the crate, so without the
feature they'd never be constructed and -D warnings would fail on
dead_code. Ungating them with
#[cfg_attr(not(feature = "permissioned"), allow(dead_code))] would make
every build check every code, and would remove the cfgs from the From
match. Left out to keep this change scoped.
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